# The Rust port plan

Written 2026-09-11 from a full audit of the 0.9.350 source, the Adom Bridge (ab) verb surface, the KiCad 10.0.6 IPC API and the KiCad 11 roadmap. John approved it the same day ("do it"). Progress is logged in the Status section right below; the decisions section at the end records what was decided.

## Status

| Date | What |
|---|---|
| 2026-09-11 | Phase 0 shipped to insiders as 0.9.351: dialog pre-emption seeding, dead PowerShell and unused modules removed. Gate on ConfRoomROG: 71 pass, 1 fail (the known idle-box foreground grant on `kicad_launch`), 15 skip. |
| 2026-09-11 | Phase 1 started. Rust workspace under `rust/` on this page: `kicad-platform` (Windows, macOS, Linux implementations behind one trait), `kicad-core`, `kicad-bridge`. 13 headless verbs (describe, status, readiness with alternatives, list_versions, diagnostics, run_drc, run_erc, the five exports, uninstall). Windows exe is 598 KB with no runtime; smoke-tested on ConfRoomROG: KiCad 10.0.5 found, DRC on a real board in 2.5 s, SVG and Gerber exports. macOS and Linux targets type-check. |
| 2026-09-11 | The four ab-core asks filed as [adom/adom-bridge#184](https://wiki.adom.inc/adom/adom-bridge/issues/184). |
| 2026-09-11 | Phase 2 de-risked before it starts: `kicad-ipc-rs` 0.5.1 (blocking feature) cross-builds for Windows from the container with mingw and cmake, and a probe on ConfRoomROG connected to the running PCB editor and returned KiCad 10.0.5, the open board and its nets in 8 ms. One build note for the record: on the gnu target nng needs `advapi32` appended to the link line (`cargo:rustc-link-arg=-ladvapi32` from a build script), or the IPC listener fails to link. |
| 2026-09-11 | Phase 1, second pass: 33 verbs in the native binary. Added the netlist group (extract_netlist, trace_net, find_connections, analyze_connections), the lint group (lint_board, lint_schematic, lint_library, format_upgrade), libraries and settings (list_symbols, list_footprints, adom_library_status, get_settings, set_settings, list_design_rules, set_design_rules) and maintenance (model_check, pcm_list, pcm_install, pcm_uninstall, check_for_updates). 73 tests in kicad-core, including parser tests on real KiCad 10 DRC and ERC reports produced by service-kicad. The HTTPS client sits behind a `net` cargo feature so the macOS type-check runs from a box without a darwin C toolchain. Smoke on ConfRoomROG: every verb answers with the Python shapes; one fix found there (a kicad-cli plot check that times out on a 500-symbol stock library was reported as a refusal, in Python too). PCM zip handling is std-only (inflate and SHA-256 written in the crate) so the binary carries no zip or crypto dependency. Schematic edit, misc (ipc_api, export_molecule, errors, log_tail) and install groups in progress. |
| 2026-09-11 | Phase 1, third pass: 50 verbs. Schematic edit in place landed (place_symbol, wire, label, move, delete, set_property: lossless s-expression round trip, .bak per write, ERC before and after through kicad-cli with the delta reported; verified on KiCad 10.0.5, and service-kicad ERC accepts the edited sheets in the test suite). Install group landed (install_library, install_symbol, install_footprint, install_library_bundle, adom_library_heal, make_part_project, export_part). Misc group landed (ipc_api, export_molecule, errors, log_tail) plus the first-run seeding in core. Uninstall is the full Python contract (all-artifacts and testResidue modes, verified on ConfRoomROG). 119 tests. The native zip was installed through ab itself on ConfRoomROG (pinned cache, spawn kind exe) and the verb runner drove it over the relay: every ported, runner-visible verb passes (35 of 35, the rest skip by design), the 47 window and routing verbs fail as expected until their phases. One contract fix from that run: ab forwards bare verb names, and the binary now accepts both forms. Phase 2 (routing on the Rust IPC client) in progress. |
| 2026-09-11 | Phase 2 landed in code: the nine routing verbs on `kicad-ipc-rs` (revision is the SHA-256 of the board as KiCad serialises it, one native commit per call, DRC on a disposable snapshot with `--refill-zones`), the four file-based routing verbs on a Rust port of the PCB parser (the routed file is byte-identical to the Python's apart from UUIDs), and `kicad_autoroute` with the ai engine returning a plan request and freerouting reserved for phase 2b. 139 tests; the six-net fixture routed on disk passes service-kicad DRC with zero errors. 59 verbs in the binary. Live IPC verification on ConfRoomROG is pending: the box dropped off the relay right after the phase 2 binary was staged (winvm went with it, which points at the network rather than the binary). Phase 3 started: the Windows and UI Automation crates cross-compile, the window-control surface is on the Platform trait, and the Windows implementation and the window verbs are being written in parallel. |
| 2026-09-11 | Phase 3 landed in code: the Windows layer (window enumeration with hung detection, process queries, background spawn with SW_SHOWNOACTIVATE, posted keys and clicks, WM_SETTEXT, native menu walk plus WM_COMMAND, button clicks through BM_CLICK with a deadline then UI Automation, in-process UIA invoke and set-value, foreground push and the one sanctioned bring-to-front with a measured result, placement, PrintWindow capture for the canvas probe, idle time), every cross-process call gated on a WM_NULL probe so a hung KiCad can never hang the bridge; the ab callback client with forwarded caller identity; the KiCad window model and dialog tables; and the 20 window verbs (launch, the open family with the rungs recorded as `mechanism`, the close family with discardChanges through the save prompt, dismiss_dialogs, window_info, state, screenshot_all through ab, send_key, click, place_footprint, enable_software_opengl). Progress dialogs are never dismissed; SendInput is not in the native build at all. 79 verbs, 160 tests, exe 3.5 MB. Live verification of phases 2 and 3 waits on ConfRoomROG, which has been off the relay since the phase 2 binary was resumed there; winvm came back on its own, so the box needs a look. |
| 2026-09-11 | Phases 2 and 3 verified live. On arav-rog (KiCad 10.0.3): live routing state in 15 ms, dry run with snapshot DRC, commits as native undo steps, stale-revision refusal, vias, a crossing route rejected by DRC with nothing landed, removal verified, and both regression demo modes at zero errors and zero unconnected (the Python gate's numbers). On ConfRoomROG (KiCad 10.0.5) the full verb runner against the phase 3 binary: 61 pass, 18 fail, 16 skip, with every window verb passing and zero foreground steals. The 18 failures were verbs the binary did not carry yet (show family, progress, verb_times, upgrade, demo, the retired plugin verbs, the export_gerbers alias) plus one runner ordering bug, all addressed the same day: alias added, plugin verbs answer `plugin_retired` with the replacement verb, runner phases fixed, show and upgrade ports in progress. The server became multithreaded (six workers, a GUI lock serialising window and IPC verbs) so status and progress polls answer in tens of milliseconds during a long verb. Two facts learned on the boxes: KiCad runs one API server per machine (first process wins; a standalone PCB editor beside the project manager gets none), now reported as `no_pcb_frame` with the remedy; and ab spawned a legacy `bridges\kicad` folder (bridge 0.9.65) on both boxes while its registry claimed the cache build, filed as [adom/adom-bridge#186](https://wiki.adom.inc/adom/adom-bridge/issues/186) and moved aside on both. |
| 2026-09-11 | Phase 3 complete: alpha.3 (94 verbs: show family, progress registry with `kicad_progress`, `kicad_verb_times` and the `operations` block on status, `kicad_upgrade` with the silent NSIS install behind the platform trait, `kicad_rescan_libraries`, the retired plugin verbs answering honestly). Full verb runner on ConfRoomROG against the native binary: 72 pass, 3 fail, 15 skip, zero foreground steals; the Python baseline on the same box was 72, 1, 15. The three failures: `kicad_demo` (phase 5) and the two retired plugin verbs, which returned an honest error where the runner expects a status (now they answer success with `retired:true`). 180 tests. Phase 4 (etiquette loop) and phase 5 (tour) start next, on a trait surface fixed in advance so the two run in parallel. |
| 2026-09-11 | Phase 4 landed and is live on ConfRoomROG: the etiquette loop runs inside the binary (a 120 ms sentinel thread and a WinEventHook thread on EVENT_SYSTEM_FOREGROUND and EVENT_OBJECT_SHOW feeding one tested decision table; the once-per-window ledger; the owned-pid ledger with the parent-chain walk, persisted in the Python's file format; the 60 s spawn watch; sanctioned foregrounds; user-click and alt-tab detection; the idle rule; the 20 s and 60 s heals). Two judgement calls recorded in the code: WS_EX_NOACTIVATE is never set (John's 0.9.181 rule), and the idle rule ranks below the guards so an unattended run still bounces self-raises. First boot on the box: both hooks armed, the launched project manager registered and pushed back exactly once. The tour input primitives (SendInput mouse, wheel, key; MCI audio) exist behind the trait and refuse unless the target window is the measured foreground. |
| 2026-09-11 | Gate with the loop running, ConfRoomROG: 73 pass, 2 fail, 15 skip. The two: `kicad_demo` (phase 5, in progress) and one foreground note on `open_board`. The loop's own event log shows exactly what happened there: the new PCB editor frame took the foreground and was bounced once by the hook within milliseconds, its "Load PCB" progress dialog took it and was bounced once, and the frame's load-complete self-raise a moment later was left alone under the "background once, then hands off" rule, which is the Python's rule and the Python's behaviour on this box. Kept as is. |
| 2026-09-11 | Phase 5 landed and ran on ConfRoomROG: the six-beat tour on the native bridge, twice, background job with the panel, captions and narration, every beat opened and five of six verified on the second run in 187 s. Three fixes came out of the first run: the ab client no longer skips stop and cleanup calls during its backoff and no longer starts a backoff on a timeout; the tour input tolerates a sub-second foreground flicker before refusing a gesture; a beat whose editor sits on an unpainted canvas gets a zoom-to-fit through the native View menu before it is judged (the footprint beat then painted and animated). The one unverified beat on run two was the board: a Chrome window from another session took the foreground during it and the tour, by rule, raises each beat exactly once. Recording: ab's native recorder drops any 4K take longer than about a minute (stop times out, 0-byte file), reproduced from the CLI with no bridge involved and filed as [adom/adom-bridge#189](https://wiki.adom.inc/adom/adom-bridge/issues/189); short takes finalise. 96 verbs, 199 tests. |
| 2026-09-11 | 1.0.0 release candidate built from the exact zip the packager produces (96 verbs, 3.0 MB with the demo audio, no runtime) and gated on ConfRoomROG: 71 pass, 4 fail, 15 skip, `kicad_demo` passing for the first time. The four: three foreground notes whose "before" window was a Chrome window another session was opening and closing on the same box during the run (the two earlier gates today had one and zero such notes), and the runner's own cleanup step, which found two folders held open by KiCad windows still up; the same cleanup ran clean once KiCad was closed. Waiting on John for the insiders flip. |
| 2026-09-12 | Shipped. Native 1.0.0 is the insiders tier (John: "flip it"). The release row and zip live on the kicad-bridge page, the insiders manifest names it with `runtime: native`, the skills package is 1.0.137 with the new verbs documented, and ConfRoomROG and arav-rog pulled it through ab's own installer (sha verified) and answer natively; the other insiders boxes follow on their poll. Public stays on Python 0.9.348 until a human promotes. Rollback is the insiders manifest back to 0.9.351. |
| 2026-09-12 | Freerouting, on the user's terms. `kicad_freerouting {status|install|uninstall}` and the `freerouting` engine of `kicad_autoroute` shipped in 1.0.1. The bridge writes the Specctra DSN itself, runs Freerouting's own self-contained bundle headless (its Java runtime lives inside that folder; nothing installed on the PC, no PATH, no UAC: the MSI is extracted with an administrative extract into a fresh subfolder of the bridge cache), reads the SES back and lands the copper as one native undo step per net through the IPC API, or into the file with a backup, then a DRC. Install runs as a background job (ab's request budget is shorter than an 88 MB download on a slow link) and status reports its progress. Verified on ConfRoomROG: install 6 s, six-net fixture routed in 2 passes and 3 s, file apply DRC clean, live apply six undo steps in 4.6 s at zero errors and zero unconnected, uninstall freed 147 MB. Two Windows facts learned the hard way: msiexec property values with spaces need the quotes inside the token or msiexec waits forever on its usage box, and the administrative image must go to an empty folder that does not hold the package or msiexec returns 1603. The `kicad-autorouting` skill carries the choice: the AI engine is the recommendation, Freerouting is offered every time and installed only when the user says yes. |
| 2026-09-12 | Placement verbs, the same way the routing verbs work. `kicad_placement_state` (every footprint with its courtyard box in board coordinates, side, pose, pads and nets, the outline, per-net footprint clusters and an MST ratsnest estimate), `kicad_move_footprint` (one footprint or a batch through `update_items`, ONE native undo step named "Adom: place U1", after the revision check, a same-side courtyard collision check that names the offending refs, and the outline check; `dryRun` reports without writing; a side change flips through the layer field, which is how KiCad's `FOOTPRINT::Deserialize` flips) and `kicad_placement_validate` (all-pairs courtyard overlaps, outside and parked footprints, the ratsnest total, KiCad DRC on the live snapshot filtered to the placement violation types with the routing verbs' truncation flags). Live when the PCB editor has the board on the IPC API; the reads and a dryRun answer from the file otherwise. Core geometry in `kicad-core/src/placement.rs` (courtyard extraction added to the PCB parser), IPC glue beside the routing operations in `ipc.rs`, the verb group in `verbs_placement.rs`. The demo fixture is the public Adom ESC G431 board: `tools/make_placement_fixture.py` (standard-library Python, a byte-preserving s-expression edit) writes an unplaced copy (149 footprints parked in a grid to the right of the outline, 434 segments, 384 vias, 57 zone fills and 66 teardrop zones removed, the 49 user zones kept) and an unrouted copy, under `demo/placement/`; both pass service-kicad DRC (KiCad 10.0.2: zero courtyard overlaps in the parked copy, 271 unconnected items as expected, the source board's own six malformed courtyards and seven intra-footprint pad clearances inherited) and the bridge's parser (integration test). 241 tests. Runner: placement_state and placement_validate in phase 2, move_footprint dryRun in phase 2.6. Live verification on a desktop is pending (John runs it). |
| 2026-09-12 | Live test of `kicad_move_footprint` on KiCad 10.0.3 found the anchor moving without the body: pads and courtyard stayed in the parking lot while `verified` said true. Root cause in KiCad's `api_handler_pcb.cpp` and `footprint.cpp` (10.0 branch): `UpdateItems` does not edit a footprint in place, it builds a new one from our FootprintInstance proto, removes the old one and adds the new one, and `FOOTPRINT::Deserialize` takes every child (pads, fields, shapes, texts, zones, dimensions) at the absolute board coordinates and angles the proto carries. Fix in 1.0.2's tree: `build_footprint_updates` now applies the move's rigid-body transform to every child client-side (p' = N + R(delta)(p - O), angles + delta, KiCad's y-down RotatePoint), the four mandatory fields through the typed proto and every `definition.items` payload on the protobuf wire format by field number (`placement::transform_child`; the crate keeps the proto types private), covering Pad, BoardText, Field, BoardTextBox, BoardGraphicShape (segment, rectangle, arc, circle, polygon with arc nodes, bezier; an off-cardinal rectangle becomes a polygon as KiCad's own rotate does), Zone (outline and fills), Dimension (all five styles, with KiCad's orthogonal axis rule), ReferenceImage and Barcode; Group and Footprint3DModel pass through; anything else refuses the move with `unsupported_child_item`. Side changes are refused with `side_change_not_supported` until a mirror lands. The post-commit check now compares the read-back courtyard box centre (or the first pad) with the expected transformed value within 0.01 mm and reports `children did not move` with the numbers instead of trusting the anchor. 210 core tests. Live re-test on a desktop pending (John runs it). |
| 2026-09-12 | `kicad_autoroute` as a background job. A real board takes minutes with Freerouting (a 59-net four-layer ESC: 40 passes over several minutes), the ab relay gives up around 130 s, and the reply of a timed-out synchronous call was lost even though the bridge finished the work. Now `kicad_autoroute {"engine":"freerouting","job":true}` validates exactly as the synchronous call does (not installed, bad apply, board open, no ipc feature), starts the same work (DSN, Freerouting, live or file apply, DRC) on its own thread and answers at once with `{status:"ok", job:"<id>", engine, started}`; `kicad_autoroute {"job":"<id>"}` (no engine) answers `running` with the live `stepLabel` and `percent` (mirrored from the progress registry, so `kicad_status.operations` shows the same frame), `done` with the complete routing reply merged in (routed, applied, drc, freerouting, dsn, revision, undoSteps, itemIds) or `failed` with the error reply; finished jobs stay pollable for an hour, then `job_not_found`. One job per board at a time (`job_busy` names the running id; the synchronous path refuses too). The job start and the poll run without GUI_LOCK (dispatch exempts them the way it exempts `kicad_demo`); the job thread takes GUI_LOCK itself around the editor sections (live snapshot, commit, live DRC) and holds nothing while Freerouting runs, so window verbs and polls are never queued behind a routing pass. The synchronous call is unchanged for a direct caller (timeout 900 s); the verb's hint and pitfalls now say to use `job:true` over the relay. Six unit tests on the job table (insert and poll, done and failed replies, expiry, busy per board, job mode parsing, unknown id). Live run on a desktop pending (John runs it). |
| 2026-10-26 | 1.0.59 (kicad-lane thread, not released yet): the EDA Bridge Standard board model. `kicad_routing_state {model:"standard"}` (live or from the file), `kicad_layer_stack`, `kicad_pour_state`, `kicad_bodies` (3D model bounds from STEP and VRML), `kicad_netlist`; the edit verbs take the standard revision and `frame:"standard"`. Unconnected matches KiCad's DRC on six KiCad 10 demo boards; live revision equals file revision; on DESKTOP-BAR6S0F (KiCad 10.0.4) a silk edit kept the revision, a move changed it, the stale one was refused, and 86 of 86 bodies cover their pads. Also: the no-ipc build compiles again (verbs_windows.rs gated), bridge.json lists every served verb. |

## The goal in one sentence

One `kicad-bridge.exe`, no Python, no PowerShell, no vendored wheels, nothing for ab to provision, that drives KiCad through its official API first and its window second, and that is measurably faster and more predictable than what ships today.

## Where we are today

| Fact | Value |
|---|---|
| Language | Python 3, run by ab's managed Python 3.12.13 (astral python-build-standalone) |
| Our code | 38,534 lines of Python, 1,983 lines of PowerShell, JS and HTML, 3,126 lines of skill docs |
| Vendored | 228,552 lines in `routing_deps/` (kicad-python, protobuf, pynng, cffi and friends), three copies for CPython 3.11, 3.12 and 3.13, 22 MB |
| Verbs | 85 in the catalog, plus a handful of hidden maintenance verbs |
| Release zip | 22 MB of routing wheels plus about 1 MB of our code |
| Third-party imports at runtime | Only `kipy` (routing verbs) and an optional `PIL` with a stdlib fallback. Everything else is stdlib plus raw `ctypes` |

The runtime is already close to dependency-free. The pain John has seen (which Python, which ABI, missing CA store, stripped environment, PowerShell crashing on .NET teardown, console windows blipping) all comes from the fact that we ship an interpreter-hosted program into an environment we do not own. A static binary removes the class of problem rather than each instance.

### How each verb reaches KiCad today

| Mechanism | Verbs | Where |
|---|---|---|
| KiCad IPC API (kipy over nng) | 9: routing_state, route_net, remove_route, routing_validate, board_pads, add_track, add_via, route, ipc_api | `handlers/live_routing.py`, `handlers/route.py` |
| kicad-cli subprocess | 16: run_drc, run_erc, lint_board, lint_schematic, lint_library, format_upgrade, extract_netlist, trace_net, find_connections, analyze_connections, export_gerber, export_pdf, export_svg, export_step, export_bom_csv, export_molecule | `handlers/export.py`, `run_drc.py`, `kicad_cli_lint.py`, `netlist.py` |
| File parsing and editing (s-expressions, lib tables, settings JSON) | 18: install_library, install_symbol, install_footprint, install_library_bundle, list_symbols, list_footprints, rescan_libraries, adom_library_status, adom_library_heal, model_check, pcm_install, pcm_list, pcm_uninstall, list_design_rules, set_design_rules, get_settings, set_settings, make_part_project, export_part | `adom_library.py`, `lib_table.py`, `parsers/`, `handlers/pcm.py`, `model_check.py` |
| Detection, install, upgrade, status | 12: readiness, list_versions, diagnostics, status, bridge_status, check_for_updates, upgrade, uninstall, describe, errors, log_tail, plugin_diagnose | `kicad_detect.py`, `handlers/upgrade.py`, `uninstall.py` |
| Windows window control (raw win32 through ctypes) | 20: launch, open_board, open_schematic, open_symbol_editor, open_footprint_editor, open_3d_viewer, open_editors, close, close_symbol_editor, close_footprint_editor, close_3d_viewer, dismiss_dialogs, window_info, state, screenshot_all, send_key, click, fix_keyboard, place_footprint, enable_software_opengl | `handlers/win_focus.py`, `close_windows.py`, `kicad_ui.py`, `win_menu.py`, `open_*.py` |
| Show family (window control plus evidence capture) | 8: show_project, show_symbol, show_footprint, show_3d_chip, show_schematic, show_2d_board, show_3d_board, show_library | `handlers/show.py` |
| Demo tour (ab delegation plus SendInput motion) | 1: demo | `handlers/demo.py`, `demo_motion.py` |
| SWIG plugin inside KiCad's Python (reverse bridge) | Used as tier 0 by open_symbol_editor, open_footprint_editor, open_3d_viewer, rescan_libraries | `plugin_payload/adom_bridge.py` (3,379 lines), `handlers/bridge_client.py` |

The window-control group is where the fragility, the sleeps and the PowerShell live. It is 21,702 lines of handlers, and the five largest files (demo, win_focus, open_symbol_editor, open_footprint_editor, close_windows, kicad_ui) are 9,776 lines between them.

### Every PowerShell use, and its replacement

| Today | Lines | What it does | In the Rust binary |
|---|---|---|---|
| `handlers/uia.py` | 144 | Spawns `powershell` with a .NET UIAutomation script to Invoke, Select or SetValue on a control by name, background | `uiautomation` crate (Windows UIA COM), in process, no subprocess, no 0xC0000409 teardown crash |
| `navigate_symbol.ps1` | 119 | UIA ValuePattern.SetValue on the library tree filter, WM_SETTEXT fallback, posted VK_RETURN | Same three calls through `uiautomation` and `windows` crates |
| `launch_editor.ps1` | 114 | Dead code, nothing calls it. Its fallback moves the real mouse | Delete now, before the port |
| `server.py` Get-Process probe | 20 | `Get-Process kicad` to list KiCad instances with titles | `CreateToolhelp32Snapshot` plus `EnumWindows`, or ab `app_instances` |
| `handlers/win_taskbar.py` .lnk repair | 80 | Inline C# through PowerShell to stamp KiCad's own AUMID on Start Menu shortcuts | `IShellLinkW` plus `IPropertyStore` through the `windows` crate, or drop the repair (John already asked for stock KiCad taskbar behaviour) |
| `handlers/demo.py` narration | 25 | Hidden PowerShell WPF MediaPlayer to play the tour mp3 | `windows::Media::Playback::MediaPlayer` (WinRT, in process), or ask ab for an audio verb so every bridge gets it |

Two `.bat` and `.sh` launch scripts exist only for hand runs and go away with `spawn.kind: "exe"`.

## What ab already does that we re-implement

The ab verb surface has grown a lot since the bridge was started. Several thousand lines of ours now duplicate it.

| Ours | Lines | ab has | Verdict |
|---|---|---|---|
| Local PrintWindow capture fallback in `kicad_ui._screenshot_hwnd`, including moving an iconic window off screen to capture it | ~130 | `desktop_screenshot_window` (PrintWindow, background, owned popups returned as separate images, coordMap) | Delete. Keep only the canvas-painted probe (`_canvas_probe`), which is KiCad-specific |
| Window enumeration (`_find_all_kicad_windows`, `_enum_kicad_hwnds`, `close_windows._win_find_*`) | ~250 | `desktop_list_windows` with pid, owner, z-order, hung flag; `enum_windows`; `find_window`; `wait_for_window` | Delete, keep a thin KiCad filter (exe name set) on top of ab's list |
| UIA through PowerShell | 144 + 119 | `desktop_ui_click` (InvokePattern), `desktop_ui_set` (ValuePattern), `desktop_ui_tree`, `find_controls` | Delete. Call ab for one-shot control actions. Keep an in-process UIA path only for the tight polling loops (library tree navigation) where an ab round trip per step is too slow |
| Taskbar progress and overlay ticker | 625 | `desktop_taskbar {progress, overlay, flash}` | Already delegated. The ticker thread stays, the COM code goes |
| Toast, captions, demo panel | ~200 | `notify_user`, `desktop_caption`, `demo_panel` | Already delegated |
| File fetch of the demo board, PCM zips, design rules | ~150 | none for outbound HTTP; keep | Keep, in Rust with `ureq` (rustls, bundled roots, so the CA store problem disappears) |
| Process kill (`taskkill`) | 30 | `process_kill {pid, tree}` | Delegate |
| Owned-popup dialog scan | ~200 | `screenshot_window` surfaces owned popups; `desktop_ui_click {scope:"popups"}` | Partially delegate. Keep the KiCad title and body pattern tables (benign versus progress versus save prompt); use ab to find and click |
| Focus sentinel, event hook, park sweep, guardian, placement heal | 1,696 | `get_foreground`, `send_to_bottom` (atomic check-and-skip), `raise_window`, `set_window_state`, `set_window_bounds`, `list_monitors` | Not a duplicate today: ab has the primitives but not the policy loop (120 ms sentinel, WinEventHook bounce, once-per-window ledger, idle rule). See the ab asks below |

Estimated deletion once the delegations land: about 3,000 lines of window plumbing, before any Rust is written.

### Asks for the ab-core thread (things every bridge needs, not just KiCad)

1. `desktop_guard_background {pids, seconds}`: the focus guardian as an ab service. Fusion needs the same etiquette and today only KiCad has it.
2. `desktop_menu_invoke {hwnd, path:["View","3D Viewer"]}`: walk a native Win32 menu bar and post WM_COMMAND. This is our most reliable background trigger and it is generic to every wx and MFC app.
3. `desktop_post_key {hwnd, vk}` and `desktop_post_text {hwnd, text}`: PostMessage and WM_SETTEXT to one window, no focus. ab's `type` and `press_key` are SendInput and global.
4. `desktop_play_audio {path}`: for narrated tours.
5. `desktop_menu_ids` is not needed if 2 lands.

If ab declines any of these, the Rust binary carries them itself with the `windows` crate. They are small (a few hundred lines each).

## What KiCad gives us, now and next

Researched against KiCad 10.0.6 (released 2026-08-29, the version on both test boxes) and the 10.99 nightlies that become KiCad 11.

### IPC API in KiCad 10

Served by the PCB editor only. Schematic, symbol editor, footprint editor and project manager are not on the API in any 10.x release. The proto files carry `DocumentType` values for all of them, so the client side is ready when KiCad is.

| Need | KiCad 10.0.6 | KiCad 11 (10.99 today) |
|---|---|---|
| Open a board or project | No. Launch the exe with the path | `kicad-cli api-server` headless plus `OpenDocument` |
| Run DRC and read results | No. `kicad-cli pcb drc --format json` | Same (DRC settings get an API, running it does not yet) |
| Edit a footprint in the editor | No. Edit `.kicad_mod` on disk | `OpenLibraryItem` (footprint only) |
| Edit schematic symbols or wires | No | Read-only hierarchy and netlist |
| 3D viewer or render | No. `kicad-cli pcb render` for an offline image | Same |
| Run a named action | Yes, `RunAction`, marked unstable | Same |
| Install a library into the tables | No. Edit `fp-lib-table` and `sym-lib-table` | Same |
| Reload libraries | No dedicated call. `RunAction` on the refresh action is untested | Same |
| Board items, nets, zones, stackup, commits, undo, selection, save | Yes, 59 commands | Plus design rules, plot settings, page settings, ImportNetlist |

New since we started: 10.0.1 added connectivity queries and dimension items, 10.0.6 added `FlipItems` and a `parent` field on every board item. Our routing verbs pin kicad-python 0.8.0, which already covers these.

### kicad-cli in KiCad 10

Every export we use is there (gerbers, drill, step, glb, svg, pdf, pos, ipc2581, odb, render with `--variant` since 10.0.2). `sch export png` and `gerber convert png` are 11 only. There is no PCM or library-table subcommand in 10 or 11.

### PCM without the GUI

PCM has no CLI, but its on-disk contract is simple and stable: unpack the package archive under `${KICAD10_3RD_PARTY}/<plugins|footprints|symbols|3dmodels|colors>/<identifier_with_underscores>/`, add a row to `installed_packages.json` (or let PCM rebuild it from directory names on next start), and for libraries add the `PCM_<name>` rows to the lib tables yourself, honouring `pcm.lib_auto_add` and `pcm.lib_prefix` from `kicad.json`. Our `pcm_install` verb already does this. So yes, we have full PCM capability today for plugins and libraries, and the port keeps it byte for byte.

More important: PCM can distribute IPC plugins with `"runtime": "ipc"`, and `plugin.json` accepts `runtime.type: "exec"`. KiCad then launches the executable itself with `KICAD_API_SOCKET` and `KICAD_API_TOKEN` in the environment. That means the Rust binary can also be registered as a KiCad API plugin and started by KiCad, which replaces the SWIG plugin and its lazy 30 to 45 second bind on 10.0.5.

### SWIG is ending

The pcbnew SWIG bindings are deprecated since 9.0 and removed in KiCad 11. `plugin_payload/adom_bridge.py` (3,379 lines) runs on them. Everything it does for us has a non-SWIG path already in the codebase (Win32 menu walk for editor and viewer opening, IPC for board state, in-place tree refresh for rescan). The port does not carry the plugin.

## Front-end control policy for the port

The audit found 129 `time.sleep` calls, 28 in `open_footprint_editor.py` alone, and seven hardcoded menu ids kept as fallbacks. The port fixes the pattern, not the instances. Every window verb picks the first rung it can reach on this ladder and records which rung it used in the response (`mechanism: "ipc" | "cli" | "file" | "menu" | "uia" | "post" | "input"`):

1. **IPC API.** Anything the running PCB editor can do for us. Add `RunAction` for zoom-fit, refill, 3D viewer toggle and library refresh once the action names are verified on 10.0.6.
2. **kicad-cli.** Every export, check and render that does not need the window.
3. **File edit.** Libraries, tables, settings, project files, PCM.
4. **Native menu walk plus WM_COMMAND.** Background, no focus, no cursor, version-proof (ids resolved from the live menu bar). This is our best trigger for "open the Footprint Editor" and "open the 3D Viewer" and stays.
5. **UI Automation.** Invoke and ValuePattern by control name. Needs no focus, but wx activates its own top-level window whenever a control's state changes (measured in `handlers/foreground_guard.py`), so every UIA action must be paired with the guardian's once-per-window demote.
6. **PostMessage keys and text to one hwnd.** For the library tree filter and Enter. Chords are unreliable this way and are never attempted.
7. **SendInput.** Only with an explicit opt-in argument and a reason, and only while our window is measured foreground. Today that is the tour's orbit and pan, the opt-in cursor click, and Ctrl+Shift+E after `bring_to_user`.

Waits become event-driven: `wait_for_window` (ab) or a WinEvent hook for window creation, `SendMessageTimeout(WM_NULL)` for responsiveness, and the canvas-painted probe for "ready", with a single deadline per verb instead of nested sleep ladders.

## Dialog pre-emption

Today we seed `kicad_common.json` (so the first-run wizard never appears), force the software canvas after an OpenGL failure, strip our projects from `system.open_projects`, seed lib tables from KiCad's own template, and sweep transient dialogs after every verb with a screenshot of what was dismissed.

The port adds, at first-run seeding time only (never on a settings file the user already has):

| Setting | File | Effect |
|---|---|---|
| `do_not_show_again.zone_fill_warning`, `env_var_overwrite_warning`, `scaled_3d_models_warning`, `data_collection_prompt`, `update_check_prompt`, `migrate_wrl_prompt` | `kicad_common.json` | The six modal nags KiCad 10 can raise on a fresh box |
| `system.check_for_kicad_updates`, `pcm.check_for_updates` | `kicad.json` | No update toasts during automation. Off only on bridge-seeded configs; we do not change a user's choice |
| `system.never_show_rescue_dialog`, `appearance.show_illegal_symbol_lib_dialog`, `show_sexpr_file_convert_warning`, `show_sheet_filename_case_sensitivity_dialog` | `eeschema.json` | Schematic open without prompts |
| `api.enable_server` | `kicad_common.json` | On, so routing works without the restart dance |
| Stale `*.lck` for bridge-owned projects | project dir | No "already open" prompt after a crash |

Two prompts have no setting: "Save changes?" and "3D model not found". The port avoids the first with `SaveDocument` over IPC before any close (or the existing `discardChanges` contract) and the second with the existing `kicad_model_check` pre-flight before opening a footprint in 3D.

## Closing the three gaps in the comparison

The [comparison](comparison.md) marks three rows Y on the strength of this plan rather than of 0.9.350. Here is what each one is.

### Schematic edit in place (phase 1)

KiCad 10 has no schematic API, so every tool that edits a schematic today rewrites `.kicad_sch` on disk. We do the same, with the two things the others skip: validation and reload.

- Verbs: `kicad_sch_place_symbol {libId, ref, at, rotation, unit}`, `kicad_sch_wire {points}`, `kicad_sch_label {text, at, kind: local|global|hierarchical|power}`, `kicad_sch_move {ref, to}`, `kicad_sch_delete {uuid}`, `kicad_sch_set_property {ref, name, value}`.
- Mechanics: lossless s-expression round trip (the same parser the file group uses), UUIDs and symbol instances written the way eeschema writes them, `.bak` before every write, `kicad-cli sch erc` after every write with the delta reported, then a background reload of the open schematic editor (close and reopen through the existing window verbs, which is what KiCad itself requires; the response says so).
- KiCad 11: the moment `schematic_commands.proto` gains write commands, the same verbs switch to the API and the reload step disappears. The read-only hierarchy and netlist commands already in 10.99 get wired first.

### Autorouting (phase 2)

`kicad_autoroute {engine: "ai" | "freerouting", nets?, dryRun?}`.

- **`ai` is the default and the recommendation.** The routing verbs already give the AI what it needs: pads, nets, zones, live revision, one native undo step per trace, DRC per step. GPT-6 Astra in Codex has routed a 94-footprint public board this way to zero unconnected items ([recorded](https://wiki.adom.inc/adom/codex)), Claude Fable 5.1 handles the same verbs well, and newer models arriving in the coming weeks are expected to be better still. The AI sees the board, honours the schematic's intent (current, return paths, thermal), and can explain every trace; an autorouter cannot.
- **`freerouting` stays for the user who wants a deterministic pass, and only on request.** Decided with John on 2026-09-12 after weighing three placements: a shared service (rejected: Adom will not run every user's autorouting), the user's own container (workable), and the desktop. Verified that day on ConfRoomROG: Freerouting 2.4.1 ships every platform build with its own Java runtime, and its Windows MSI extracts with `msiexec /a` in two seconds with no elevation into a 141 MB folder whose `freerouting.exe` runs headless. So the desktop wins, with rules: never installed by default; `kicad_freerouting {status|install|uninstall}` fetches the 88 MB bundle only when the user says yes and removes it on its own; `kicad_uninstall` removes it too; nothing named Java is installed, nothing on PATH, no UAC. The bridge writes a Specctra DSN from the parsed board (own writer; kicad-cli has no DSN export and RunAction names are unstable), runs the bundle headless with a deadline, and applies the SES as native undo steps through the IPC API (or into the file with a backup), then a DRC. Every reply of `kicad_autoroute` names both engines and says why the AI is the recommendation; the `kicad-autorouting` skill carries the choice, the AI recipe and the honesty rules.
- **The hints decide nothing for the user.** The verb's `_hint` and `describe` entry tell the AI to ask the user which engine they want before routing, to say why `ai` is recommended, and to report which engine produced the copper. That is the same pattern as `foregroundReason`: the bridge makes the AI ask.

### Runs with no KiCad installed (already true, made visible in phase 1)

Three paths exist today and readiness should say so instead of reporting `not_installed` and stopping:

1. **The shared `service-kicad` container**: DRC, ERC, gerbers, STEP, GLB, renders and library lookups for any container, no desktop involved.
2. **kicad-cli in your own container**: the `service-kicad` skill installs it, so the headless verb group runs locally.
3. **KiCad on another box**: a second laptop, a conference room machine, or a Grok Computer session driven through the Grok bot, addressed by name through Adom Bridge.

Phase 1 adds a `backend` field to every headless verb (`local | service | target:<name>`) with `local` as default, and a `kicad_readiness.alternatives` block that lists the three paths with a one-line hint each when no local KiCad is found.

## One codebase, three operating systems

Kyle owns the macOS build of Hydrogen and Adom Bridge and will take this codebase to macOS; Barrett was doing the Ubuntu build of the bridge. The port is laid out so each OS compiles its own implementation in at build time and nobody forks:

- **`crates/kicad-platform`** is the only OS-specific crate. It defines one `Platform` trait (install discovery, settings and documents directories, quiet child processes, and later window control, UI automation, dialog sweep, focus etiquette, silent install) and three files: `windows.rs`, `macos.rs`, `linux.rs`. `cfg(target_os)` selects exactly one. Per-OS crates (winreg and later the windows and uiautomation crates; objc2 and accessibility on macOS; x11rb, zbus or atspi on Linux) are declared in that crate's `[target.'cfg(...)'.dependencies]` tables and nowhere else.
- **`crates/kicad-core`** (detection ordering, kicad-cli runner and parsers, file editing, the IPC client) and **`crates/kicad-bridge`** (server, catalog, verbs) never touch the OS directly. They call `kicad_platform::native()`.
- **Honest gaps.** A verb that needs something an OS has not implemented yet returns `errorCode: not_supported_on_platform` with the OS and the owner's name. Readiness and diagnostics carry a `platform.capabilities` block (kicadCli, windowControl, uiAutomation, dialogSweep, focusEtiquette, ipcApi, silentInstall) so an AI can tell what this build can do before it calls anything.
- **Today.** The headless half (kicad-cli, file editing, detection, settings paths) already works on all three because it only needs the paths each OS file provides. `cargo check --target aarch64-apple-darwin` and `--target x86_64-unknown-linux-musl` pass from the container; the Windows binary cross-builds from Linux with mingw-w64.
- **Publishing.** ab manifests are single-zip, so each OS ships from its own build until per-platform manifests exist. Windows first; macOS and Linux follow when their platform files reach parity, gated by the same verb runner.

## Phase 4 specification: the etiquette loop

What the Python `handlers/win_focus.py` learned over a month, restated as the contract the native loop must meet. It runs inside the binary on a dedicated thread pair; if ab-core takes [adom/adom-bridge#184](https://wiki.adom.inc/adom/adom-bridge/issues/184), the same contract moves there.

- **Two detectors, one action.** A WinEventHook on EVENT_SYSTEM_FOREGROUND and EVENT_OBJECT_SHOW (out of context, skip own process) pumping messages on its own thread, plus a 120 ms polling sentinel as the fallback. Both funnel into one decision function.
- **The decision.** A KiCad window took the foreground. Bounce it only if all of these hold: a verb is in flight or a guard is armed; the window is bridge-owned (pid ledger: spawned pids plus their children, persisted to disk so a restart still knows); it is not inside a sanctioned foreground window (`bring_to_front` with a reason); the user did not click it (mouse buttons down with the cursor over it or over the shell), did not alt-tab to it, and is not past the foreground-lock idle timeout (the idle rule: when the user has been idle, Windows grants the foreground and the bridge must not fight it).
- **The action.** Exactly one `push_to_background` per (hwnd, pid), recorded in a ledger with an expiry; never minimize, never move off screen, never touch a hung window. A 3 s no-activate hold after a bounce, then released.
- **Guards.** Every window verb arms a guard for 20 s (120 s for the slow ones: 3D viewer, place footprint, demo, launch, show_3d_*), released early when the verb ends. The park baseline (windows already in the foreground before the verb) is never bounced.
- **Heals.** Every 20 s idle: clear stale no-activate styles the bridge set; drag windows back from off screen (placement heal, wiki #48: only when no part of the window is on any monitor). Every 60 s idle: strip bridge projects from `system.open_projects` in `kicad.json` while no KiCad runs.
- **Introspection.** `kicad_state.focusDebug` and `focusEvents` (ring buffer of every transition with its classification), the same fields the Python exposes.
- **Never.** No SetWindowsHookEx of any kind, no global input listeners, no SendInput in the loop, no style changes on a hung window (the 0.9.165 boot deadlock).

## Target architecture

```
kicad-bridge.exe  (spawn.kind "exe", ADOM_BIND_HOST, --port from ab)
  http      tiny_http or axum, serde_json          /status /command
  catalog   one table: verb, handler, risk, timeout, mechanism, describe entry
  detect    registry + install dirs + kicad-cli version, config seeding
  cli       kicad-cli runner, JSON report parsers (drc, erc, netlist, bom)
  files     s-expression reader/writer (own crate, adom-inc/kicad_lib, or kiutils_kicad),
            lib tables, kicad_common/kicad/eeschema/pcbnew settings, PCM layout
  ipc       kicad-ipc-rs (prost + nng), or our own prost bindings over nng-sys
  win       windows crate: process, EnumWindows, menu walk, PostMessage, WinEventHook,
            SendMessageTimeout, placement, DPI; uiautomation crate for UIA
  ab        typed client for the ab callback API (screenshots, taskbar, notify, ui_*)
  etiquette focus sentinel, once-per-window demote, owned-pid ledger, idle rule
  show      evidence capture (via ab) + canvas-painted probe + thumbnail
  demo      tour beats, narration, motion
  net       ureq with rustls: KiCad installer, Mesa fallback, wiki fetches
```

Crate choices, checked 2026-09-11:

| Need | Crate | State |
|---|---|---|
| KiCad IPC | `kicad-ipc-rs` 0.5.1 (MIT, prost 0.14, nng 1.0.1, 59 of 59 KiCad 10.0.1 commands, used by Konnect) or the official `kicad-api-rs` 0.1.0 (GPL-3, undocumented) | Use kicad-ipc-rs, vendor if needed. Windows is not on its CI, so we test it |
| nng transport | `nng` 1.0.1 (`nng-sys` builds NNG 1.4 from source, static) | Needs CMake and a C compiler at build time only. Verified from the container with mingw-w64; append `-ladvapi32` on the gnu target |
| UIA | `uiautomation` 0.24.1 (2026-05) | Mature |
| Win32 | `windows` crate | Mature |
| Capture | none. ab does it | |
| KiCad files | `kiutils_kicad` 0.3.0 (lossless round trip for pcb, mod, sch, sym, lib tables, pro) or `adom-inc/kicad_lib` | Evaluate both against our parsers' test fixtures |
| glTF check | `gltf` | Mature. STEP stays a header sanity check (Rust STEP parsers are immature) |
| HTTP out | `ureq` with rustls | Mature |
| HTTP in | `tiny_http` (what hello-rust uses) | Mature |

Expected binary: 5 to 15 MB stripped with nng, prost and UIA. Build on winvm or cross-compile from the container with `cargo-xwin` (nng-sys needs clang). Static CRT with `+crt-static`.

## Phased plan

Each phase ships to insiders through the existing release script and is gated by the verb runner (`skills/kicad-bridge-test/run_verb_tests.py`) running against both boxes. The Python bridge keeps shipping until the Rust binary passes the same runner with equal or better numbers; then the manifest flips. Same verb names, same response shapes, same skills package.

| Phase | Scope | Verbs | Effort | Exit test |
|---|---|---|---|---|
| 0. Prune in Python (now) | Delete `launch_editor.ps1`, `tour/` (older standalone tour), unused `foreground_guard.py`; delegate capture, enumeration and taskkill to ab; add the do_not_show_again seeding; record `mechanism` on every window verb | 0 new | 1 week | Runner unchanged, zip smaller, no PowerShell left except uia.py |
| 1. Rust skeleton and headless verbs | HTTP, catalog, describe, status, readiness (with alternatives), detect, diagnostics, kicad-cli group with `backend`, file group, PCM, model_check, upgrade, uninstall, settings, design rules, part projects, the six schematic edit verbs | ~51 | 4 to 5 weeks | Runner parity on those verbs, both boxes, zero Python |
| 2. IPC verbs | The nine routing verbs on kicad-ipc-rs; `kicad_autoroute` with the ai and freerouting engines; routing demo passes; `routing_deps/` deleted | 10 | 3 weeks | `demo/routing/run_demo.py --expect-version` green on 10.0.6 |
| 3. Window verbs | launch, open_*, close_*, show_*, state, screenshot_all, send_key, click, dismiss_dialogs, window_info, place_footprint, software OpenGL | ~28 | 4 to 5 weeks | Runner parity plus the six-beat tour |
| 4. Etiquette | Sentinel, WinEvent hook, ledger, idle rule, placement heal, ported or moved into ab | 0 | 2 weeks | Foreground-steal test from the runner (`fg_before`, `foreground_notes`) at zero steals |
| 5. Demo tour | Beats, narration, motion, evidence | 1 | 2 weeks | Recorded tour on ConfRoomROG and winvm |
| 6. Plugin retirement and KiCad 11 | Drop `plugin_payload`, register the exe as an IPC plugin through PCM, test on 10.99 nightly | 0 | 2 weeks | Runner on 10.0.6 and on the nightly |

Total: roughly 18 to 21 weeks of one AI thread's time with human test time on the boxes, or half that with two threads splitting phases 1+2 and 3+4 after the skeleton lands. The Python line count suggests 25,000 to 30,000 lines of Rust; the window group shrinks the most because ab absorbs the plumbing.

## Risks, stated plainly

- **wx physics do not change with the language.** KiCad raises its own window on UI state changes. The guardian stays necessary; Rust only makes it cheaper and tighter.
- **KiCad renumbers menu ids and renames actions.** The live menu walk handles ids. `RunAction` names are documented as unstable, so every action name gets a probe at readiness time and a fallback rung.
- **IPC is one connection per app, synchronous on the UI thread.** A hung KiCad hangs the call. Every IPC call gets a deadline and the responsiveness probe first.
- **`api.enable_server` needs a KiCad restart** and a running KiCad rewrites the file on exit. The seeding-at-first-run path avoids this for new boxes; existing boxes keep the documented restart.
- **nng-sys is a C build.** It is the one non-Rust piece. If it fights us on cross-compile we build on winvm.
- **Single zip, one OS.** ab manifests have no per-platform assets, so the Rust bridge is Windows-only until per-platform manifests exist. macOS keeps the Python bridge (its window code is `open -a` and osascript, a fraction of the Windows surface).
- **KiCad 11 timing is unannounced.** The plan makes us independent of SWIG before it matters.

## Decisions (recorded 2026-09-11)

1. **Windows first**, with the platform crate ready for Kyle (macOS) and Barrett (Linux) to fill in. Their builds are not part of this thread's work; being ready for them is.
2. **Focus guardian**: proposed to ab-core as [adom/adom-bridge#184](https://wiki.adom.inc/adom/adom-bridge/issues/184); ported into the binary in phase 4 regardless so nothing blocks.
3. **Audio**: asked of ab in the same issue; WinRT MediaPlayer in the binary as the fallback.
4. **Threads**: this thread runs the port; a second thread can take phases 3 and 4 once the skeleton is stable.
5. **Source**: on this page under `rust/`, `target/` ignored.

## Related

- [How the bridge compares](comparison.md): the competitive audit this plan was written alongside.
- [Dashboard](dashboard.md): the dock card and the dashboard.
- Verb reference and the live routing contract: the main [README](../README.md) and [SKILL.md](../SKILL.md).

- 2026-09-12 (later): comparison.md reworked after John's review. The three "where others lead" items are now "three claims we chose not to match" (KiCad reads its own files, permissions live in the AI harness and in ab, discovery is skills plus describe plus wiki auto-discovery). SPICE simulation added as the one open gap with an ecosystem-wide plan (ngspice runner in the container, kicad_export_spice, kicad_simulate); verified kicad-cli spice export and bundled ngspice on ConfRoomROG. PCM proof in progress (sample repository on the page, install, bump, update).
- 2026-09-12 (later): PCM proof passed end to end on ConfRoomROG (KiCad 10.0.5) and is written up in [pcm.md](pcm.md). The sample repository on this page (three anonymous files, `no-cache` JSON) was registered with `kicad_pcm_add_repository`, installed with `kicad_pcm_install` (1.0.0, `PCM_Adom_Sample` registered by KiCad itself on launch), bumped to 1.1.0 with the packer and pushed, seen by `kicad_pcm_list` (`updatesAvailable: 1`) and by KiCad's own PCM dialog after a restart (Update button on the card), updated the user's way through UI Automation (Update All, Apply Pending Changes, Close) with the Symbol Editor showing `R v1.1.0`, then bumped to 1.2.0 and updated headlessly with `kicad_pcm_install {update:true}` (`previousVersion 1.1.0, version 1.2.0, updated true`), which KiCad's dialog showed without a restart. Two fixes came out of it, both in the build that ships as 1.0.2: the automatic dialog sweep no longer closes tool windows opened on purpose (it had pressed Close on "Applying Package Changes" mid-apply and aborted the first update), and `kicad_send_key` gained `menu` (a label substring fired on the window's menu bar as a background menu command; how the PCM dialog is opened). New verbs: `kicad_pcm_add_repository`, `kicad_pcm_remove_repository`, `kicad_pcm_list` with the update check, `kicad_pcm_install {update:true}`; documented in SKILL.md under "PCM repositories on the wiki" and covered by the verb runner (real add in phase 4, conditional remove in phase 6). Three KiCad facts recorded for the port: `installed_packages.json` is written when the PCM dialog closes, Refresh inside an open dialog does not recompute the installed-package update flag (the startup check does), and the per-card Update button is not exposed to UIA while Update All, Apply Pending Changes, Refresh and Close are.

- 2026-09-12 (evening): etiquette decision order fixed after the 1.0.3 gate on arav-rog flagged one steal: with the Windows foreground lock off, a bridge-spawned KiCad window comes forward twice while it loads, and the once-ledger left the second on top of the user's Hydrogen. The "already bounced" row now sits below the rows that mean the bridge is the cause (guard active, verb in flight, fresh spawn), so those always bounce; user-initiated foregrounds stay excluded above. ESC placement round: fixture, planner and Claude Fable 5.1 take recorded (25 commits, 36.5 s, zero overlaps), evidence on the page under demo/placement/fable.

- 2026-09-12 (evening, continued): the gate on ConfRoomROG kept flagging kicad_open_board as a foreground steal after the decision-order fix. Measured cause: a Z-order push (HWND_BOTTOM) does not hold while the editor still owns the activation, and the bridge process's own SetForegroundWindow on the user's window reports success without taking effect (no foreground rights), whereas ab's desktop_bring_to_front sticks. Fix: the bridge now has a Z-order top query (what actually covers the user), spawn verbs wait for the editor's load dialog to go away and then run the restore twice, and the restore hands the activation back to the user's window through ab with its window state preserved; the etiquette loop remembers the user's last non-KiCad foreground and restores it on every bounce. Gate on ConfRoomROG: 88 pass, 0 fail, 16 declared skips, zero steals. 1.0.3 published to insiders with the placement verbs.

- 2026-09-13: ESC routing round recorded on ConfRoomROG. Freerouting 229.5 s, 59 nets, 189 new DRC errors, 7 unconnected (DSN rule mapping, issue #93). Claude Fable 5.1 with its own grid router: 105 commits, 518.5 s, 0 new DRC errors, 7 unconnected. Job-mode autoroute verified live. Noted: ab's monitor recorder on ConfRoomROG delivers frames only when the screen changes (a 519 s take became 125 s of video), so real time is stated on screen from the logs.

- 2026-09-13: John's correction: the headline time on every chart and table is the wall clock from his prompt to the delivered result, not the seconds on the board. Placement: Fable 119.5 min (18:41:42 to 20:41:10 UTC), Astra 15.9 min (20:41:10 to issue filed 20:57:02). Routing: Freerouting 3.8 min (one call), Fable 228 min (20:41:10 to 00:29:15 UTC). Both videos rebuilt with the new charts (?v=2); both Astra prompts now ask for the paste-to-issue wall clock as item (a0).

- 2026-09-13: John rejected the routing take with 7 unconnected ("100 percent or it is pointless"). The loop: signal vias 0.6/0.3 per net (7 to 2), then a placement adjustment of 13 parts (new tools/pack_moves.py and tools/move_footprints.py, courtyard-exact), router 0 failed, offline gate 0 new and 0 unconnected, live take on ConfRoomROG: 1 move batch + 114 commits in 570 s, validate 0 unconnected. New skill kicad-place-route-loop in the package; the autorouting and root skills point at it; Astra told on issue #92 and in the routing prompt. Wall clock 5 h 34 min across both takes; $273.69 with agents.

- 2026-09-13 (evening): copper pours round on the ESC. Bridge 1.0.5: kicad_add_zone (copper zones as one Undo step after a filled DRC preflight on a snapshot, created through CreateItems with hand-encoded Zone protos, then a native fill; KiCad 10 implements RefillZones only for the empty list, so the bridge fills all) and kicad_zone_state (raw zone protos decoded for filled area per layer, the ablation metric). 1.0.6: rule-area keepouts (type keepout, copperPour/tracks/vias/pads/footprints). 1.0.7: kicad_remove_zone, zone types named (copper, keepout, teardrop). All insiders, staged on ConfRoomROG. Skills: kicad-copper-pours (1.0.141, 1.0.142 with the Kelvin recipe and the preflight lessons). Offline: tools/add_pours.py; demo: pours_plan.py, pours_live.py. First pours pass refused for starved_thermal (16 pads) and zones_intersect; the sense amplifier ground input was a plane stub, not a Kelvin tap: shunt and amplifier moved to the return contact, router routes dedicated taps first, keepouts along them. The headless service-kicad DRC does not refill zones, so the live verb preflight is the pours gate.

- 2026-09-13: TODO for the week of 2026-09-14, decided with John: adom-boardflow, the flow app (plan/take negotiation, place, route, pour, analyze, sim, gate, land, measure) as a compiled binary with hints, packaging tonight's Python first, then a Rust router core. Tracked as a feature-request issue on this page; not started.
