← Commit history

test stealth mode: keep KiCad behind the user's window during GUI phases (re-raise user's window, not minimize); documented in background+test dev skills

John Lauer ·1280320623 ·2mo ago ·parent 78b7369
3 files changed +89−2
skills/kicad-bridge-background/SKILL.md+38
@@ -96,3 +96,41 @@ John mid-work (missing from the dispatcher focus set); 2026-08-14 the first full verb-suite run took over his foreground repeatedly while he typed in another AI thread — the old guard was too short for 3D-render self-raises and fought deliberate app switches.+++## Keeping KiCad in the background WHILE TESTING (John, 2026-08-14)++The runtime focus-guardian holds for a single verb's guard window (20-45s). A+TEST run fires dozens of GUI verbs back-to-back over many minutes, so there are+gaps between guarded verbs where a late async self-raise (3D render completion)+or a fresh window can surface on the user's screen — the guardian for the+PREVIOUS verb has already expired. The user felt this: KiCad windows popping up+mid-work during a suite run.++**The rule: the windows do NOT need to be minimized or moved — they just must+stay BEHIND the user's active window.** (John, verbatim: "it doesn't have to be+minimized, it just has to be in the background.")++**Mechanism the suite uses (`--no-stealth` to disable):**+- At the start of the GUI phase, capture the user's current top window+  (`desktop_list_windows` → the `z:0` entry, only if it is NOT a KiCad window).+- After EVERY GUI verb, `_keep_background()`: if a KiCad window is now on top,+  `desktop_raise_window` the user's window back over it. If the user has since+  switched to a different non-KiCad window, ADOPT that as the new one to protect+  — never fight the user's own window choices (same principle as the runtime+  guardian).+- This RAISES the user's own window (returning focus to them), it never+  minimizes/moves KiCad. Screenshots still work (the windows are just lower in+  z-order, fully rendered).++**Rejected alternatives and why:**+- *Minimize KiCad between verbs* — screenshots of a minimized window BLANK+  (PrintWindow returns a full-screen fallback rect), and it's more disruptive+  than the user asked for.+- *Park windows off-screen (x:-4000)* — screenshots DO work off-screen (verified),+  but it's more than the user wanted and complicates dependent verbs. Kept as a+  documented fallback if a future need is "never visible at all", not the default.++The passive `fg_assert` (fail the verb if KiCad is z:0) still runs as the+regression check; `_keep_background()` is the active remedy that keeps the+user's screen clean during the run.
skills/kicad-bridge-test/SKILL.md+6−2
@@ -75,8 +75,12 @@ matrix. Any live verb that is neither tested nor on the explicit skip list - Uses `--ai-thread "kicad-bridge daily-test"` so the box's Activity Log   attributes every action. -Prefer running while the box is idle anyway: GUI phases open ~6 KiCad windows-in the background, which is churn the user can feel in the taskbar.+GUI phases open ~6 KiCad windows. By default the suite runs in STEALTH: it+captures the user's top window at the start and re-raises it after every GUI+verb, so KiCad stays BEHIND the user's work the whole run (not minimized, not+moved — just never on top). Pass `--no-stealth` to disable. Details + the+rejected alternatives (minimize/off-screen) are in the kicad-bridge-background+skill.  ## Daily automation (INSTALLED on gallia, 2026-08-14) 
skills/kicad-bridge-test/run_verb_tests.py+45
@@ -211,6 +211,8 @@ def main():     ap.add_argument("--only", default="", help="comma-separated verb subset")     ap.add_argument("--skip-gui", action="store_true", help="phases 1-2 only")     ap.add_argument("--full", action="store_true", help="run all six demo beats")+    ap.add_argument("--no-stealth", action="store_true",+                    help="don't keep KiCad behind the user's window during GUI phases")     ap.add_argument("--out", default="/tmp/kicad-verbtest")     a = ap.parse_args() @@ -279,6 +281,48 @@ def main():      fg_violations = [] +    # STEALTH (John, 2026-08-14: "keep the kicad windows in the background when+    # you test — it doesn't have to be minimized, just background"). Capture the+    # window the user has on top RIGHT NOW; after every GUI verb we re-raise it,+    # which pushes any KiCad window that surfaced back BEHIND the user's work.+    # Not minimized, not moved — just never on top of what the user is doing.+    def _current_top():+        r = ab(a.target, "desktop_list_windows", {}, timeout=30)+        try:+            inner = json.loads(r["output"]) if isinstance(r.get("output"), str) else r+            ws = (inner.get("data") or inner).get("windows") or []+            top = next((x for x in ws if x.get("z") == 0), None)+            return top or {}+        except Exception:+            return {}++    _kw = ("KiCad", "PCB Editor", "Schematic Editor", "Symbol Editor",+           "Footprint Editor", "3D Viewer")+    _user_win = {}+    if not a.skip_gui and not a.no_stealth:+        w = _current_top()+        # only adopt a NON-KiCad window as the one to protect+        if w and not any(k in (w.get("title") or "") for k in _kw):+            _user_win = w+            print(f"[stealth] keeping KiCad behind your window: {(_user_win.get('title') or '')[:50]}")++    def _keep_background():+        """Re-raise the user's window so KiCad sits behind it. If the user has+        since switched to another (non-KiCad) window, adopt THAT — never fight+        the user's own choices."""+        nonlocal _user_win+        if a.skip_gui or a.no_stealth:+            return+        top = _current_top()+        tt = top.get("title") or ""+        if top and not any(k in tt for k in _kw):+            _user_win = top          # user moved on; protect their new window+            return                   # already on top — nothing to do+        # KiCad is on top: raise the user's window back over it+        hw = _user_win.get("hwnd")+        if hw:+            ab(a.target, "desktop_raise_window", {"hwnd": hw}, timeout=20)+     def fg_assert(verb):         """After every GUI verb: the OS foreground window must NOT be KiCad.         This is the enforcement of the ALWAYS-background contract — a verb that@@ -301,6 +345,7 @@ def main():                 print(f"    !! FOREGROUND STEAL after kicad_{verb}: {title[:60]}")         except Exception:             pass+        _keep_background()   # push KiCad behind the user's window either way      def run(verb, spec):         if only and verb not in only: