← Commit history

v0.9.156: geometry-poison prevention (unpark before close verbs, atexit unpark, boot heal of off-screen-stranded windows) + background doctrine skill updated with the ownership contract

John Lauer ·1e76c9f6a0 ·1mo ago ·parent ea54cba
7 files changed +129−10
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.155+0.9.156
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.155",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.155/adom-bridge-kicad-v0.9.155.zip",-  "sha256": "abd0989f6d447c3b3dac9b8f444442880cf5a8a4c1b4f4970e25b3e11f98b003",-  "size": 436974,+  "version": "0.9.156",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.156/adom-bridge-kicad-v0.9.156.zip",+  "sha256": "504ef46dd51e4e7addcc45f4de7cb99a58bae307c827f94640db98d05ac9ed5a",+  "size": 437802,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-17T21:13:28Z",+  "released_at": "2026-08-17T21:17:20Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.156.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.155",+  "version": "0.9.156",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/win_focus.py+43−3
@@ -383,8 +383,8 @@ _park_sweep_started = False # path leaves it completely alone. There is deliberately NO "adopt what exists" # seeding: unknown windows default to user-owned. _owned_pids: set = set()-_KICAD_EXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor",-               "pcb_calculator", "bitmap2component")+_OWNED_EXE_PREFIXES = ("kicad", "eeschema", "pcbnew", "gerbview", "pl_editor",+                       "pcb_calculator", "bitmap2component")   def _ledger_path():@@ -425,7 +425,7 @@ def _pid_exe_base(pid) -> str:  def _pid_is_kicad(pid) -> bool:     base = _pid_exe_base(pid)-    return any(base.startswith(e) for e in _KICAD_EXES)+    return any(base.startswith(e) for e in _OWNED_EXE_PREFIXES)   def _save_ledger() -> None:@@ -549,6 +549,46 @@ def is_bridge_window(hwnd) -> bool:         return False  +def heal_offscreen_windows() -> int:+    """Boot repair (John, 2026-08-17: "when the user goes to open kicad normally+    ... it starts way offscreen and kicad doesn't fix itself"): if a parked+    window was closed (or the bridge died) before unparking, KiCad saved the+    off-screen geometry, and the next launch restores there. Nothing but our+    parking puts a KiCad window at <= -20000, so any window found there — OURS+    OR THE USER'S — is our poison to heal: move it to a sane on-screen spot,+    no activation, size preserved."""+    healed = 0+    if not IS_WINDOWS:+        return healed+    try:+        for i, h in enumerate(sorted(_enum_kicad_hwnds())):+            try:+                if _user32.IsIconic(h):+                    continue+                wp = _WINDOWPLACEMENT()+                wp.length = ctypes.sizeof(_WINDOWPLACEMENT)+                if not _user32.GetWindowPlacement(h, ctypes.byref(wp)):+                    continue+                if wp.rcNormalPosition.left > -20000:+                    continue+                width = wp.rcNormalPosition.right - wp.rcNormalPosition.left+                height = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top+                x, y = 120 + 48 * (i % 5), 90 + 36 * (i % 5)+                wp.rcNormalPosition.left = x+                wp.rcNormalPosition.top = y+                wp.rcNormalPosition.right = x + max(width, 400)+                wp.rcNormalPosition.bottom = y + max(height, 300)+                wp.showCmd = 8  # SW_SHOWNA — visible, no activation+                _user32.SetWindowPlacement(h, ctypes.byref(wp))+                healed += 1+                print(f"[focus] healed off-screen window {h} -> ({x},{y})", file=sys.stderr)+            except Exception:+                pass+    except Exception:+        pass+    return healed++ def _enum_kicad_hwnds() -> set:     out = set()     try:
server.py+23
@@ -770,6 +770,16 @@ def dispatch_command(command: str, args: dict) -> dict:         if command not in _NO_GUI:             from handlers import win_focus as _wfg             _wfg.begin_gui_verb()+            # 0.9.156 (John's catch): a window CLOSED while parked off-screen+            # makes KiCad persist the off-screen geometry, so the user's next+            # normal launch opens invisibly. Restore true positions before any+            # close verb runs.+            if command in ("close", "close_symbol_editor",+                           "close_footprint_editor", "close_3d_viewer"):+                try:+                    _wfg.unpark_all()+                except Exception:+                    pass     except Exception:         pass     try:@@ -1087,6 +1097,19 @@ def main():         win_focus.load_owned_ledger()     except Exception:         pass+    # 0.9.156: heal any window stranded at parking coordinates (a crash or a+    # close-while-parked poisons KiCad's saved geometry, and the next NORMAL+    # launch opens invisibly off-screen — John's catch).+    try:+        win_focus.heal_offscreen_windows()+    except Exception:+        pass+    # Bridge shutdown must never strand parked windows either.+    try:+        import atexit+        atexit.register(win_focus.unpark_all)+    except Exception:+        pass     try:         from handlers.win_taskbar import badge_existing_on_boot         badge_existing_on_boot()
skills/kicad-bridge-background/SKILL.md+56
@@ -252,6 +252,62 @@ BEFORE the window moves. Sentinel bounces hold NOACTIVATE for 10s afterward (10.0.5 prevention is reactive-only; without the hold, repeat-steal storms flicker - the '10 windows blipping' John saw was exactly this during test runs). +## THE OWNERSHIP CONTRACT (0.9.155, John 2026-08-17 — supersedes everything above where they conflict)++Caught live: John launched his own KiCad from the taskbar and the sentinel+bounced him 8 times, including a File Open Warning dialog he never saw. Root+causes: (a) click-on-window intent detection misses taskbar/Start clicks (the+cursor is over the SHELL, not the raised window, when the transition lands);+(b) nothing distinguished HIS windows from OURS.++**The contract: the bridge may only park, bounce, badge, or identity-stamp+windows it OWNS. Ownership is decided by PROCESS TREE, recorded at spawn.**++- Every kicad-family pid the bridge spawns is registered+  (`win_focus.register_owned_pid`, called at every `Popen` site — when you add+  a spawn site, ADD THE CALL). Children count via parent-chain walk (the PM+  spawns eeschema.exe/pcbnew.exe as separate processes).+- The set persists to `%USERPROFILE%/.adom/kicad-bridge-owned.json` and is+  revived at boot (`load_owned_ledger`) with liveness+exe validation, so a+  bridge restart neither orphans our windows nor mis-adopts the user's.+- There is deliberately NO "adopt whatever exists" seeding. Unknown = user's.+- The sentinel, the SetWinEventHook path, the parking sweep, overlay badges,+  and AUMID identity stamps ALL check `_is_owned_window()` first. A user-owned+  window is logged ("user-owned window (left alone)") and never touched.+- User intent now includes clicks on the shell (taskbar / Start / desktop+  classes in `_SHELL_CLASSES`) — clicking a taskbar button IS the canonical+  "bring this window up" gesture.+- Two instances coexist by design: KiCad has no single-instance lock (only+  per-project .lck warnings). The bridge drives its own projects in its own+  processes alongside the user's instance and never opens the user's projects.++## Geometry-poison prevention (0.9.156, John's catch)++Parking moves owned windows off-screen (~-32000). KiCad persists window+geometry on close — so a window CLOSED while parked (or stranded by a bridge+crash) writes the off-screen position into KiCad's config, and the user's next+NORMAL launch opens invisibly. Three defenses, all mandatory:++1. The dispatcher calls `unpark_all()` BEFORE any close verb runs+   (close / close_symbol_editor / close_footprint_editor / close_3d_viewer).+2. `atexit.register(unpark_all)` — bridge shutdown never strands parked windows.+3. Boot runs `heal_offscreen_windows()`: any KiCad window found at+   left <= -20000 (nothing but our parking puts one there) is moved to a sane+   on-screen spot without activation — this heals crash-poisoned geometry even+   on windows the user re-launched themselves.++## Taskbar identity + badges (0.9.153-0.9.155, see handlers/win_taskbar.py)++In-bridge ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY (returns S_OK+cross-process, shell ignores it — pup and fusion both paid for this lesson+first). Badges go through AD-core `desktop_taskbar` via `handlers/ad_client.py`+(ported verbatim from fusion-bridge), using fusion's exact 32px badge asset+(`resources/adom-overlay-32.png` — mark anchored bottom-right of a transparent+canvas; a full-bleed badge renders oversized). Blank white eeschema/pcbnew+buttons = poisoned shell icon cache for those exe paths; bypassed by stamping+per-exe AUMIDs (`desktop_set_window_identity`) with .ico files extracted from+the exes. All of it OWNED WINDOWS ONLY, gated by the `overlayBadges` setting.+ ## The presence gate (0.9.136, John 2026-08-17)  A live incident settles it: the daily verb suite ran while John was working at