← Commit history

0.9.340: Live routing verbs through KiCad's IPC API (routing_state, route_net, remove_route, routing_validate), offline copper verbs (board_pads, add_track, add_via, route), kicad_ipc_api, KiCad 10 name-based nets, vendored routing deps

John Lauer ·b07801a6db ·1mo ago ·parent c3618db
3 files changed +142−67
page.json+4−31
@@ -4,7 +4,7 @@   "slug": "kicad-bridge",   "title": "KiCad - the KiCad Bridge",   "brief": "Reference implementation of the KiCad bridge: multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",-  "version": "1.0.126",+  "version": "1.0.111",   "tags": [     "kicad",     "pcb",@@ -53,7 +53,7 @@     "install_hint": "",     "version_cmd": ""   },-  "readme": "# Adom Desktop — KiCad Bridge\n\nA **reverse bridge** that lets Adom Desktop (AD) — and the AI driving it — control the user's own\nKiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run\nDRC/ERC, export manufacturing files, and screenshot any window back to the AI.\n\nKiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present,\n*installs* one for them — it never asks the user to download or click through anything by hand.\n\n- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge\n- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges\n- **Verb namespace:** `kicad_*` &nbsp;·&nbsp; **Status verb:** `kicad_bridge_status` &nbsp;·&nbsp; **Health:** `GET /status`\n\n> Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no\n> GPU**, driven end-to-end through the bridge — install → libraries → editors → 2D → 3D. If it\n> renders there, it renders on a real laptop.\n\n---\n\n## Demo — narrated live editor tour (real screen recording)\n\n[![KiCad bridge live demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo-poster.png)](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)\n\n**▶ [Watch the demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)** (48s, 🔊 narrated) — the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section — not animated stills.\n\n\n---\n\n## The demo verb — `kicad_demo`\n\nOne verb that shows the whole bridge off, built for the AI that demos Adom Desktop\nduring Hydrogen Desktop's install. Six beats, in the order a hardware person thinks:\n\n| Beat | Shows |\n|---|---|\n| 1 | the **schematic symbol** in the user's own Symbol Editor |\n| 2 | the **footprint** for that same part |\n| 3 | that part in **3D** |\n| 4 | a **schematic** using it (U1 + two 10k resistors) |\n| 5 | the **2D board layout** |\n| 6 | the finished **board in 3D** |\n\n```bash\nadom-desktop kicad_demo '{}'                      # start: prepares, warms KiCad, opens beat 1\nadom-desktop kicad_demo '{\"step\":\"footprint\"}'    # ...then follow data.nextCall each time\nadom-desktop kicad_demo '{\"all\":true}'            # bulk (non-interactive; slower than one request budget)\nadom-desktop kicad_close '{\"force\":true}'         # tear down\n```\n\n**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`\n(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and\n`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,\nthen move on.\n\n**It ships nothing.** The two demo parts, the schematic and the board are all\ngenerated in code, and the 3D bodies come from KiCad's *own* bundled\n`3dmodels/*.3dshapes` — so the runtime zip stays small and the 3D views are real.\nParts install into the user's real `Adom` library; the project lands in\n`Documents/adom-kicad-demo`.\n\n**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an\nerror; `kicad_demo '{\"installKiCad\": true}'` silently installs the official build\n(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is\nitself a good demo beat.\n\n**It never takes your screen.** Every window opens in the background and all input is\nwindow-targeted (UIA / `PostMessage`), so you can keep working while it runs.\n\n---\n\n## How it fits together\n\nThe bridge is a `spawn.kind: python` process AD launches on the user's machine (`entrypoint:\nserver.py`, `port: 0` — AD picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP\n(`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the\nlightest one that can do the job.\n\n![Control surfaces](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/control-surfaces.svg)\n\n| # | Surface | Used for | Where |\n|---|---------|----------|-------|\n| 1 | **kicad-cli** (subprocess) | headless DRC/ERC, gerber/pdf/svg/step/bom export, format upgrade | `handlers/export.py`, `run_drc`, `run_erc`, `lint_*` |\n| 2 | **KiCad IPC (kipy)** | board/schematic introspection, confirmed footprint placement (KiCad 9+) | `handlers/place_footprint.py` |\n| 3 | **Embedded Python** | JSON-RPC *inside* each KiCad process, dispatched on its wx UI thread | `plugin_payload/adom_bridge.py`, `handlers/bridge_client.py` |\n| 4 | **Win32 / UIA / SendKeys** | open editors, screenshots, clicks/keys, dialog handling, window management | `handlers/kicad_ui.py`, `handlers/close_windows.py` |\n\n---\n\n## The window tour\n\nEverything below was opened and captured on the GPU-less VM. Sample data is the **RP2040 breakout**\ngenerated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` footprint + a board).\n\n### Schematic editor — `kicad_open_schematic`\n\nThe RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins.\n\n![Schematic editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/schematic-editor.png)\n\n### Symbol editor — `kicad_open_symbol_editor`\n\nOpens straight to a symbol (`kicad_install_symbol` puts it in a user library first).\n\n![Symbol editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/symbol-editor.png)\n\n### Footprint editor — `kicad_open_footprint_editor`\n\n`{\"footprintName\":\"QFN-56_AdomRP2040\",\"library\":\"AdomRP2040\"}` loads the part directly — 56 pins +\nthermal pad, 7×7 mm, 0.4 mm pitch, courtyard and silkscreen.\n\n![Footprint editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/footprint-editor.png)\n\n### PCB editor (2D) — `kicad_open_board`\n\nThe routed breakout — copper, silkscreen, the QFN-56 land pattern, mounting holes.\n\n![PCB editor 2D](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/pcb-editor-2d.png)\n\n### 3D viewer — `kicad_open_3d_viewer {\"editor\":\"pcb\"}`\n\nThe full board in 3D — board body, the USB-C connector's 3D model, the QFN chip body, SMD parts,\nplated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.**\n\n![3D viewer](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/3d-viewer.png)\n\n---\n\n## Install & upgrade — zero manual steps\n\nThe bridge never tells the user to go download KiCad. `kicad_upgrade` fetches the official installer\nand runs it silently, picking the scope automatically:\n\n- **Elevated AD** → `/allusers /S` (system-wide, `%ProgramFiles%\\KiCad`).\n- **Non-elevated AD** → `/currentuser /S` (`%LocalAppData%\\Programs\\KiCad`, **no UAC prompt**).\n\n`kicad_readiness` reports whether KiCad is installed and ready without side effects; the AI routes on\nit before offering to install. Hard-won install details (all handled for you):\n\n- KiCad 10's NsisMultiUser installer **requires** a scope flag — bare `/S` errors `rc=666660`.\n- AD's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS.\n- Downloads are size-checked against `Content-Length` (a truncated installer otherwise fails at NSIS).\n- `%APPDATA%/kicad/<ver>/` lib tables don't exist until first launch — the bridge **bootstraps** them\n  so `install_library`/`install_footprint` work on a never-opened KiCad.\n- `kicad_upgrade {\"diagnoseOnly\":true}` reports token-elevation type + `EnableLUA` without installing.\n\n---\n\n## Dialogs & error handling — the bridge clears the pointless ones\n\nKiCad throws modal dialogs that stall automation. The bridge **scans every window owned by a running\nKiCad process** (by PID — reliable, unlike title matching), auto-expires the benign ones, screenshots\nwhat it dismissed, and returns a hint so the AI can decide what (if anything) to tell the user.\n\nAuto-expired benign dialogs include:\n\n- **\"Could not use OpenGL / falling back to software rendering\"** (GPU-less hosts).\n- **\"Welcome to KiCad — starting for the first time\"** first-run wizard (dismissing accepts defaults).\n- **\"This file was created by an older version of KiCad\"** conversion notice.\n\n![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/dialog-firstrun-wizard.png)\n\nVerbs:\n\n- `kicad_window_info` — lists windows and **self-heals** (auto-expires benign dialogs) by default.\n- `kicad_dismiss_dialogs {\"all\":true}` — clear everything blocking; `{\"screenshot\":true}` returns\n  images of each; `{\"forceSoftwareCanvas\":true}` persists Cairo canvas; `{\"debug\":true}` dumps the\n  raw PID-based scan.\n- `kicad_screenshot_all` — one call returns **every** open KiCad window, so the AI can spot an error\n  dialog it didn't expect and read the message.\n\n---\n\n## Software-OpenGL fallback (last resort)\n\n`kicad_enable_software_opengl` deploys Mesa's `llvmpipe` (a CPU OpenGL rasterizer) into KiCad's `bin`\nso a box with **no usable GPU** — Hyper-V, RDP, headless CI — can still render the editors and the 3D\nviewer. This is how every 3D shot above exists.\n\n> ⚠️ It renders on the CPU and is **slow**. It is a worst-case fallback only — a real GPU (or GPU-P /\n> DDA passthrough) is vastly better. The bridge keeps it so a GPU-less box isn't a dead end; it is\n> never suggested proactively.\n\n---\n\n## Verb reference\n\nAll 41 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints,\nrelated verbs, and pitfalls.\n\n**Windows & UI** — `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,\n`open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`,\n`close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`\n\n**Libraries & parts** — `install_library`, `install_symbol`, `install_footprint`, `install_plugin`,\n`place_footprint`, `adom_library_status`\n\n**Checks & export** — `run_drc`, `run_erc`, `lint_board`, `lint_schematic`, `lint_library`,\n`format_upgrade`, `export_gerber`, `export_pdf`, `export_svg`, `export_step`, `export_bom_csv`\n\n**Detect, install & meta** — `list_versions`, `readiness`, `describe`, `enable_software_opengl`,\n`check_for_updates`, `upgrade`, `bridge_status`, `bridge_call`\n\n---\n\n## Running / dependencies\n\nAD provisions everything; there is nothing to install by hand.\n\n- **Python** ≥ 3.11 (AD provisions it) — stdlib only, plus the bundled `certs/cacert.pem`.\n- **KiCad** ≥ 7.0 (host app; `kicad_upgrade` installs it if absent).\n- **OS:** Windows (macOS/Linux detection stubs exist; the GUI surfaces are Windows-first).\n\nThe bridge binds `ADOM_BIND_HOST` on an AD-assigned port — never `0.0.0.0`. Auto-updates via\n`updateManifestUrl` in `bridge.json`.\n\n---\n\n*Developer & publish notes live in `dev-skills/` and `publish-skills/` (source-only, never shipped to\na user install).*\n",+  "readme": "# Adom Desktop \u2014 KiCad Bridge\n\nA **reverse bridge** that lets Adom Desktop (AD) \u2014 and the AI driving it \u2014 control the user's own\nKiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run\nDRC/ERC, export manufacturing files, and screenshot any window back to the AI.\n\nKiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present,\n*installs* one for them \u2014 it never asks the user to download or click through anything by hand.\n\n- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge\n- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges\n- **Verb namespace:** `kicad_*` &nbsp;\u00b7&nbsp; **Status verb:** `kicad_bridge_status` &nbsp;\u00b7&nbsp; **Health:** `GET /status`\n\n> Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no\n> GPU**, driven end-to-end through the bridge \u2014 install \u2192 libraries \u2192 editors \u2192 2D \u2192 3D. If it\n> renders there, it renders on a real laptop.\n\n---\n\n## Demo \u2014 narrated live editor tour (real screen recording)\n\n[![KiCad bridge live demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo-poster.png)](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)\n\n**\u25b6 [Watch the demo](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-bridge-demo.mp4)** (48s, \ud83d\udd0a narrated) \u2014 the bridge driving real KiCad on a Windows VM: **select** a footprint and **zoom** the copper in the PCB editor, **rotate** the assembled board in 3D, then tour the schematic, symbol, and footprint editors. Genuine screen recording of the live windows (zoom / select / 3D rotation), narrated section by section \u2014 not animated stills.\n\n\n---\n\n## The demo verb \u2014 `kicad_demo`\n\nOne verb that shows the whole bridge off, built for the AI that demos Adom Desktop\nduring Hydrogen Desktop's install. Six beats, in the order a hardware person thinks:\n\n| Beat | Shows |\n|---|---|\n| 1 | the **schematic symbol** in the user's own Symbol Editor |\n| 2 | the **footprint** for that same part |\n| 3 | that part in **3D** |\n| 4 | a **schematic** using it (U1 + two 10k resistors) |\n| 5 | the **2D board layout** |\n| 6 | the finished **board in 3D** |\n\n```bash\nadom-desktop kicad_demo '{}'                      # start: prepares, warms KiCad, opens beat 1\nadom-desktop kicad_demo '{\"step\":\"footprint\"}'    # ...then follow data.nextCall each time\nadom-desktop kicad_demo '{\"all\":true}'            # bulk (non-interactive; slower than one request budget)\nadom-desktop kicad_close '{\"force\":true}'         # tear down\n```\n\n**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`\n(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and\n`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,\nthen move on.\n\n**It ships nothing.** The two demo parts, the schematic and the board are all\ngenerated in code, and the 3D bodies come from KiCad's *own* bundled\n`3dmodels/*.3dshapes` \u2014 so the runtime zip stays small and the 3D views are real.\nParts install into the user's real `Adom` library; the project lands in\n`Documents/adom-kicad-demo`.\n\n**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an\nerror; `kicad_demo '{\"installKiCad\": true}'` silently installs the official build\n(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is\nitself a good demo beat.\n\n**It never takes your screen.** Every window opens in the background and all input is\nwindow-targeted (UIA / `PostMessage`), so you can keep working while it runs.\n\n---\n\n## How it fits together\n\nThe bridge is a `spawn.kind: python` process AD launches on the user's machine (`entrypoint:\nserver.py`, `port: 0` \u2014 AD picks the port and passes `ADOM_BIND_HOST`). It speaks HTTP\n(`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the\nlightest one that can do the job.\n\n![Control surfaces](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/control-surfaces.svg)\n\n| # | Surface | Used for | Where |\n|---|---------|----------|-------|\n| 1 | **kicad-cli** (subprocess) | headless DRC/ERC, gerber/pdf/svg/step/bom export, format upgrade | `handlers/export.py`, `run_drc`, `run_erc`, `lint_*` |\n| 2 | **KiCad IPC (kipy)** | board/schematic introspection, confirmed footprint placement (KiCad 9+) | `handlers/place_footprint.py` |\n| 3 | **Embedded Python** | JSON-RPC *inside* each KiCad process, dispatched on its wx UI thread | `plugin_payload/adom_bridge.py`, `handlers/bridge_client.py` |\n| 4 | **Win32 / UIA / SendKeys** | open editors, screenshots, clicks/keys, dialog handling, window management | `handlers/kicad_ui.py`, `handlers/close_windows.py` |\n\n---\n\n## The window tour\n\nEverything below was opened and captured on the GPU-less VM. Sample data is the **RP2040 breakout**\ngenerated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` footprint + a board).\n\n### Schematic editor \u2014 `kicad_open_schematic`\n\nThe RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins.\n\n![Schematic editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/schematic-editor.png)\n\n### Symbol editor \u2014 `kicad_open_symbol_editor`\n\nOpens straight to a symbol (`kicad_install_symbol` puts it in a user library first).\n\n![Symbol editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/symbol-editor.png)\n\n### Footprint editor \u2014 `kicad_open_footprint_editor`\n\n`{\"footprintName\":\"QFN-56_AdomRP2040\",\"library\":\"AdomRP2040\"}` loads the part directly \u2014 56 pins +\nthermal pad, 7\u00d77 mm, 0.4 mm pitch, courtyard and silkscreen.\n\n![Footprint editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/footprint-editor.png)\n\n### PCB editor (2D) \u2014 `kicad_open_board`\n\nThe routed breakout \u2014 copper, silkscreen, the QFN-56 land pattern, mounting holes.\n\n![PCB editor 2D](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/pcb-editor-2d.png)\n\n### 3D viewer \u2014 `kicad_open_3d_viewer {\"editor\":\"pcb\"}`\n\nThe full board in 3D \u2014 board body, the USB-C connector's 3D model, the QFN chip body, SMD parts,\nplated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.**\n\n![3D viewer](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/3d-viewer.png)\n\n---\n\n## Install & upgrade \u2014 zero manual steps\n\nThe bridge never tells the user to go download KiCad. `kicad_upgrade` fetches the official installer\nand runs it silently, picking the scope automatically:\n\n- **Elevated AD** \u2192 `/allusers /S` (system-wide, `%ProgramFiles%\\KiCad`).\n- **Non-elevated AD** \u2192 `/currentuser /S` (`%LocalAppData%\\Programs\\KiCad`, **no UAC prompt**).\n\n`kicad_readiness` reports whether KiCad is installed and ready without side effects; the AI routes on\nit before offering to install. Hard-won install details (all handled for you):\n\n- KiCad 10's NsisMultiUser installer **requires** a scope flag \u2014 bare `/S` errors `rc=666660`.\n- AD's portable Python has **no CA bundle**; the bridge ships `certs/cacert.pem` and uses it for TLS.\n- Downloads are size-checked against `Content-Length` (a truncated installer otherwise fails at NSIS).\n- `%APPDATA%/kicad/<ver>/` lib tables don't exist until first launch \u2014 the bridge **bootstraps** them\n  so `install_library`/`install_footprint` work on a never-opened KiCad.\n- `kicad_upgrade {\"diagnoseOnly\":true}` reports token-elevation type + `EnableLUA` without installing.\n\n---\n\n## Dialogs & error handling \u2014 the bridge clears the pointless ones\n\nKiCad throws modal dialogs that stall automation. The bridge **scans every window owned by a running\nKiCad process** (by PID \u2014 reliable, unlike title matching), auto-expires the benign ones, screenshots\nwhat it dismissed, and returns a hint so the AI can decide what (if anything) to tell the user.\n\nAuto-expired benign dialogs include:\n\n- **\"Could not use OpenGL / falling back to software rendering\"** (GPU-less hosts).\n- **\"Welcome to KiCad \u2014 starting for the first time\"** first-run wizard (dismissing accepts defaults).\n- **\"This file was created by an older version of KiCad\"** conversion notice.\n\n![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/dialog-firstrun-wizard.png)\n\nVerbs:\n\n- `kicad_window_info` \u2014 lists windows and **self-heals** (auto-expires benign dialogs) by default.\n- `kicad_dismiss_dialogs {\"all\":true}` \u2014 clear everything blocking; `{\"screenshot\":true}` returns\n  images of each; `{\"forceSoftwareCanvas\":true}` persists Cairo canvas; `{\"debug\":true}` dumps the\n  raw PID-based scan.\n- `kicad_screenshot_all` \u2014 one call returns **every** open KiCad window, so the AI can spot an error\n  dialog it didn't expect and read the message.\n\n---\n\n## Software-OpenGL fallback (last resort)\n\n`kicad_enable_software_opengl` deploys Mesa's `llvmpipe` (a CPU OpenGL rasterizer) into KiCad's `bin`\nso a box with **no usable GPU** \u2014 Hyper-V, RDP, headless CI \u2014 can still render the editors and the 3D\nviewer. This is how every 3D shot above exists.\n\n> \u26a0\ufe0f It renders on the CPU and is **slow**. It is a worst-case fallback only \u2014 a real GPU (or GPU-P /\n> DDA passthrough) is vastly better. The bridge keeps it so a GPU-less box isn't a dead end; it is\n> never suggested proactively.\n\n---\n\n## Verb reference\n\nAll 41 verbs (prefix `kicad_`). Call `kicad_describe` for the live catalog with per-verb hints,\nrelated verbs, and pitfalls.\n\n**Windows & UI** \u2014 `open_board`, `open_schematic`, `open_symbol_editor`, `open_footprint_editor`,\n`open_3d_viewer`, `open_editors`, `close_symbol_editor`, `close_footprint_editor`, `close_3d_viewer`,\n`close`, `window_info`, `dismiss_dialogs`, `screenshot_all`, `send_key`, `click`, `fix_keyboard`\n\n**Libraries & parts** \u2014 `install_library`, `install_symbol`, `install_footprint`, `install_plugin`,\n`place_footprint`, `adom_library_status`\n\n**Checks & export** \u2014 `run_drc`, `run_erc`, `lint_board`, `lint_schematic`, `lint_library`,\n`format_upgrade`, `export_gerber`, `export_pdf`, `export_svg`, `export_step`, `export_bom_csv`\n\n**Detect, install & meta** \u2014 `list_versions`, `readiness`, `describe`, `enable_software_opengl`,\n`check_for_updates`, `upgrade`, `bridge_status`, `bridge_call`\n\n---\n\n## Running / dependencies\n\nAD provisions everything; there is nothing to install by hand.\n\n- **Python** \u2265 3.11 (AD provisions it) \u2014 stdlib only, plus the bundled `certs/cacert.pem`.\n- **KiCad** \u2265 7.0 (host app; `kicad_upgrade` installs it if absent).\n- **OS:** Windows (macOS/Linux detection stubs exist; the GUI surfaces are Windows-first).\n\nThe bridge binds `ADOM_BIND_HOST` on an AD-assigned port \u2014 never `0.0.0.0`. Auto-updates via\n`updateManifestUrl` in `bridge.json`.\n\n---\n\n*Developer & publish notes live in `dev-skills/` and `publish-skills/` (source-only, never shipped to\na user install).*\n",   "author": {     "name": "John Lauer",     "email": "[email protected]"@@ -90,8 +90,6 @@     "skills/kicad-tour/SKILL.md",     "skills/kicad-tour/tour_runner.py",     "skills/kicad-web-control/SKILL.md",-    "skills/kicad-uia/catalog.json",-    "skills/kicad-uia/SKILL.md",     "skills/kicad-bridge-dev/SKILL.md",     "skills/kicad-bridge-publish/SKILL.md",     "skills/kicad-bridge-hero/SKILL.md",@@ -129,30 +127,5 @@   "binary_name": "adom-bridge-kicad",   "authors": null,   "author_handle": "john",-  "authors_json": null,-  "agent_permissions": {-    "allow_bash": [-      "adom-desktop-kicad-bridge"-    ],-    "allow": [-      "adom-desktop-kicad-bridge status, readiness, describe, diagnostics, window_info, state and open_editors are read-only queries against KiCad and the bridge; they change nothing.",-      "adom-desktop-kicad-bridge list_versions, list_footprints, list_symbols, list_design_rules and adom_library_status read library tables and files; they change nothing.",-      "adom-desktop-kicad-bridge plugin_diagnose, verb_times, progress, errors, log_tail, get_settings and check_for_updates read bridge logs and settings; they change nothing.",-      "adom-desktop-kicad-bridge screenshot_all captures KiCad windows to images; it changes nothing in KiCad.",-      "adom-desktop-kicad-bridge extract_netlist, trace_net, find_connections and analyze_connections read the open design; they change nothing.",-      "adom-desktop-kicad-bridge lint_board, lint_schematic, lint_library, run_drc and run_erc run KiCad's own checks and produce reports; they change nothing."-    ],-    "soft_deny": [-      "adom-desktop-kicad-bridge install_library, install_library_bundle, install_symbol, install_footprint, pcm_install and pcm_uninstall write to the user's KiCad libraries.",-      "adom-desktop-kicad-bridge adom_library_heal, set_design_rules, format_upgrade and set_settings rewrite library files, rules or settings.",-      "adom-desktop-kicad-bridge launch, close, close_symbol_editor, close_footprint_editor, close_3d_viewer and dismiss_dialogs start or stop KiCad windows.",-      "adom-desktop-kicad-bridge uninstall, install_plugin, upgrade and enable_software_opengl reconfigure the user's KiCad or the bridge.",-      "adom-desktop-kicad-bridge export_gerber, export_gerbers, export_pdf, export_svg, export_step, export_molecule, export_bom_csv and export_part write files.",-      "adom-desktop-kicad-bridge make_part_project and place_footprint edit or create designs; demo, send_key and click drive KiCad's windows and inject input."-    ],-    "environment": [-      "adom-desktop-kicad-bridge is the KiCad Bridge; every kicad verb also passes Adom Bridge's human-approval gate on the user's desktop (caller identity, per-call reason, Activity Log).",-      "adom-desktop-kicad-bridge container approval is the first of two gates; the desktop gate is the second."-    ]-  }-}\ No newline at end of file+  "authors_json": null+}
parsers/pcb.py+85−35
@@ -8,7 +8,26 @@ from __future__ import annotations  from pathlib import Path -from .sexpr import parse_file, find_node, find_nodes, node_value+from .sexpr import parse, parse_file, find_node, find_nodes, node_value+++def _net_ref(node: list | None, name_based: bool = False) -> tuple[int | None, str | None]:+    """Read a (net ...) reference in either board format.++    KiCad <=9 numbers its nets and declares them at the top of the board:+    `(net 3 "GND")` on a pad, `(net 3)` on a segment. KiCad 10 (file format+    20260206) dropped the table and the numbers entirely -- every reference is+    just `(net "GND")`. Both shapes appear in the wild, so read both.+    """+    if not node or len(node) < 2:+        return None, None+    first = node[1]+    if name_based:+        return None, first if isinstance(first, str) else None+    if isinstance(first, str) and first.lstrip("-").isdigit():+        name = node[2] if len(node) >= 3 and isinstance(node[2], str) else None+        return int(first), name+    return None, first if isinstance(first, str) else None   def parse_pcb(filepath: str | Path) -> dict:@@ -17,8 +36,47 @@ def parse_pcb(filepath: str | Path) -> dict:     return _extract_pcb(tree, filepath)  +def parse_pcb_text(text: str, filepath: str | Path = "") -> dict:+    """Same as parse_pcb, but for board text already in memory.++    The routing verbs splice new copper into the board TEXT, so they hold the+    string anyway; re-reading the file to inspect it would race their own write.+    """+    return _extract_pcb(parse(text), filepath)++ def _extract_pcb(tree: list, filepath: str | Path) -> dict:     """Extract all relevant data from a parsed PCB tree."""+    data = _extract_pcb_raw(tree, filepath)+    return _resolve_net_format(data)+++def _resolve_net_format(data: dict) -> dict:+    """Say which net dialect the board speaks, and give it a net list either way.++    A KiCad 10 board has no net table to read, so the only inventory of its nets+    is the names its pads and copper mention. Callers that write copper need+    this: the (net ...) they emit has to match the shape of the file.+    """+    numbered = bool(data.get("nets")) or any(+        p.get("net_number") is not None+        for fp in data.get("footprints", []) for p in fp.get("pads", []))+    data["net_format"] = "number" if numbered else "name"+    if not data.get("nets"):+        names, seen = [], set()+        for name in ([p.get("net_name") for fp in data.get("footprints", [])+                      for p in fp.get("pads", [])]+                     + [s.get("net_name") for s in data.get("segments", [])]+                     + [v.get("net_name") for v in data.get("vias", [])]+                     + [z.get("net_name") for z in data.get("zones", [])]):+            if name and name not in seen:+                seen.add(name)+                names.append(name)+        data["nets"] = [{"number": None, "name": n} for n in names]+    return data+++def _extract_pcb_raw(tree: list, filepath: str | Path) -> dict:     return {         "file": str(filepath),         "version": node_value(find_node(tree, "version")),@@ -85,17 +143,12 @@ def _extract_layers(tree: list) -> list[dict]:   def _extract_nets(tree: list) -> list[dict]:-    """Extract net definitions."""+    """Net definitions from the board's own table (KiCad <=9 only)."""     nets = []     for node in find_nodes(tree, "net"):-        if len(node) >= 3:-            try:-                nets.append({-                    "number": int(node[1]),-                    "name": node[2] if isinstance(node[2], str) else "",-                })-            except (ValueError, TypeError):-                pass+        num, name = _net_ref(node, int(node_value(find_node(tree, "version")) or 0) >= 20260206)+        if num is not None:+            nets.append({"number": num, "name": name or ""})     return nets  @@ -192,13 +245,13 @@ def _extract_footprints(tree: list) -> list[dict]:                         pad_info["height"] = float(size_node[2])                     except (ValueError, TypeError):                         pass-                net_node = find_node(pad, "net")-                if net_node and len(net_node) >= 3:-                    try:-                        pad_info["net_number"] = int(net_node[1])-                        pad_info["net_name"] = net_node[2] if isinstance(net_node[2], str) else ""-                    except (ValueError, TypeError):-                        pass+                pad_layers = find_node(pad, "layers")+                if pad_layers:+                    pad_info["layers"] = [l for l in pad_layers[1:] if isinstance(l, str)]+                num, name = _net_ref(find_node(pad, "net"), int(node_value(find_node(tree, "version")) or 0) >= 20260206)+                if num is not None or name is not None:+                    pad_info["net_number"] = num+                    pad_info["net_name"] = name or ""                 pad_list.append(pad_info)         fp["pads"] = pad_list @@ -232,12 +285,11 @@ def _extract_segments(tree: list) -> list[dict]:         layer = find_node(node, "layer")         if layer:             seg["layer"] = node_value(layer)-        net = find_node(node, "net")-        if net:-            try:-                seg["net"] = int(node_value(net))-            except (ValueError, TypeError):-                pass+        num, name = _net_ref(find_node(node, "net"), int(node_value(find_node(tree, "version")) or 0) >= 20260206)+        if num is not None:+            seg["net"] = num+        if name is not None:+            seg["net_name"] = name         segments.append(seg)     return segments @@ -274,12 +326,11 @@ def _extract_vias(tree: list) -> list[dict]:         layers = find_node(node, "layers")         if layers:             via["layers"] = [l for l in layers[1:] if isinstance(l, str)]-        net = find_node(node, "net")-        if net:-            try:-                via["net"] = int(node_value(net))-            except (ValueError, TypeError):-                pass+        num, name = _net_ref(find_node(node, "net"), int(node_value(find_node(tree, "version")) or 0) >= 20260206)+        if num is not None:+            via["net"] = num+        if name is not None:+            via["net_name"] = name         vias.append(via)     return vias @@ -289,12 +340,11 @@ def _extract_zones(tree: list) -> list[dict]:     zones = []     for node in find_nodes(tree, "zone"):         zone = {"uuid": node_value(find_node(node, "uuid"))}-        net = find_node(node, "net")-        if net:-            try:-                zone["net"] = int(node_value(net))-            except (ValueError, TypeError):-                pass+        num, name = _net_ref(find_node(node, "net"), int(node_value(find_node(tree, "version")) or 0) >= 20260206)+        if num is not None:+            zone["net"] = num+        if name is not None:+            zone["net_name"] = name         net_name = find_node(node, "net_name")         if net_name:             zone["net_name"] = node_value(net_name)
server.py+53−1
@@ -44,6 +44,11 @@ from handlers.install_library_bundle import handle_install_library_bundle from handlers.install_symbol import handle_install_symbol from handlers.install_footprint import handle_install_footprint, handle_list_footprints from handlers.place_footprint import handle_place_footprint+from handlers.live_routing import (handle_routing_state, handle_route_net,+                                   handle_remove_route, handle_routing_validate)+from handlers.route import (handle_add_track, handle_add_via, handle_route,+                           handle_board_pads)+from handlers.ipc_api import handle_ipc_api from handlers.run_drc import handle_run_drc from handlers.fix_keyboard import handle_fix_keyboard from handlers.kicad_ui import handle_screenshot_all, handle_send_key, handle_click@@ -440,6 +445,42 @@ _VERB_CATALOG = {     "install_plugin": {"summary": "Install the reverse-bridge plugin into KiCad (idempotent).", "long": False,         "hint": "Normally auto-runs on your first kicad_* call; call explicitly only to force/repair.",         "related": ["kicad_bridge_status", "kicad_bridge_call"], "pitfalls": ["needs a restart of an already-running KiCad to load the freshly-installed plugin"]},+    "ipc_api": {"summary": "Read or flip KiCad's IPC API server switch (Preferences > Plugins) that live routing needs.", "long": False,+        "hint": "kicad_ipc_api {} reports enabled; {\"enable\":true} writes kicad_common.json (backup kept). KiCad must be closed and relaunched to pick it up.",+        "related": ["kicad_routing_state", "kicad_close", "kicad_launch"],+        "pitfalls": ["a running KiCad rewrites kicad_common.json on exit and can undo the change: close KiCad first", "does not restart KiCad for you"]},+    "routing_state": {"summary": "Inspect the live editor: pads, copper, revision and KiCad-measured net connectivity.", "long": False,+        "hint": "Pass exact filePath and optional local ipc:// socketPath. Use revision as expectedRevision for edits.",+        "related": ["kicad_route_net", "kicad_routing_validate"],+        "pitfalls": ["Requires KiCad 10.0.1+ with the IPC API server enabled; no file fallback."]},+    "route_net": {"summary": "Commit a waypoint path or branching paths for ONE net live with native Undo and DRC preflight.", "long": False,+        "hint": "Pass filePath, expectedRevision from routing_state, exact net name, points or paths, optional width/layer/viaSize/viaDrill. dryRun validates only; save:true explicitly saves.",+        "related": ["kicad_routing_state", "kicad_remove_route", "kicad_routing_validate"],+        "pitfalls": ["Caller chooses the route: this is not a shove router or an autonomous autorouter.", "DRC rejects new errors by default; validate:false explicitly skips it.", "Do not blindly retry a timed-out mutation; inspect live state."]},+    "remove_route": {"summary": "Remove specific unlocked copper item IDs live as one Undo step.", "long": False,+        "hint": "filePath, expectedRevision, itemIds from route_net. Refuses missing IDs, pads, footprints and locked copper.",+        "related": ["kicad_route_net", "kicad_routing_state"],+        "pitfalls": ["Does not save the file or delete a whole net implicitly."]},+    "routing_validate": {"summary": "Run KiCad DRC against the current unsaved live board snapshot.", "long": False,+        "hint": "filePath and optional socketPath. Reports errors, warnings, unconnected, revision and stale.",+        "related": ["kicad_route_net", "kicad_routing_state"],+        "pitfalls": ["A stale report is superseded by newer edits; clean requires zero violations and zero unconnected items."]},+    "board_pads": {"summary": "Every pad in BOARD coordinates with its net: the input a router needs.", "long": False,+        "hint": "kicad_board_pads {\"filePath\":\"C:/d/x.kicad_pcb\"} (optional net/reference filter). Read-only. x/y are mm with footprint rotation applied, +y DOWN; hasCopper and netsWithoutTracks are file inventory only; use kicad_routing_state for connectivity.",+        "related": ["kicad_route", "kicad_add_track", "kicad_extract_netlist"],+        "pitfalls": ["pad coordinates are the pad CENTRE; a track to it still has to clear other nets", "KiCad 10 boards have no net numbers; address nets by name"]},+    "add_track": {"summary": "Write one copper segment to a CLOSED board file.", "long": False,+        "hint": "kicad_add_track {\"filePath\":\"...\",\"start\":\"R1.2\",\"end\":[120.5,90],\"net\":\"GND\",\"layer\":\"F.Cu\"}. Endpoints are pad names or [x,y] mm. Width defaults to the board's own.",+        "related": ["kicad_route", "kicad_add_via", "kicad_board_pads", "kicad_run_drc"],+        "pitfalls": ["there is no autorouter: YOU choose the path, and nothing checks clearance until kicad_run_drc", "refuses open boards; use kicad_route_net for live, undoable routing"]},+    "add_via": {"summary": "Drop one via so a route can change layer.", "long": False,+        "hint": "kicad_add_via {\"filePath\":\"...\",\"at\":[120,90],\"net\":\"GND\",\"layers\":[\"F.Cu\",\"B.Cu\"]}. Defaults 0.6 mm pad / 0.3 mm drill.",+        "related": ["kicad_add_track", "kicad_route", "kicad_list_design_rules"],+        "pitfalls": ["the via alone connects nothing; the tracks either side still have to be drawn", "check size/drill against the fab's rules"]},+    "route": {"summary": "Lay a whole path for one net in a single write, vias included.", "long": False,+        "hint": "kicad_route {\"filePath\":\"...\",\"net\":\"GND\",\"points\":[\"R1.2\",[120,88],{\"x\":130,\"y\":88,\"layer\":\"B.Cu\"},\"U1.7\"]}. A waypoint with a different layer drops a via there.",+        "related": ["kicad_add_track", "kicad_board_pads", "kicad_run_drc"],+        "pitfalls": ["draws exactly the waypoints you give; it does not search for a path or avoid other nets", "for a visible demo, call kicad_route_net per segment"]},     "place_footprint": {"summary": "Deterministically place a footprint into a board preview.", "long": False,         "hint": "Splices a footprint into a board via s-expr (no GUI); good for previews.",         "related": ["kicad_open_board", "kicad_install_footprint"], "pitfalls": ["operates on the file — close the board in KiCad first or the edit races the GUI"]},@@ -909,6 +950,16 @@ COMMAND_HANDLERS = {     "install_symbol": handle_install_symbol,     "install_footprint": handle_install_footprint,     "place_footprint": handle_place_footprint,+    # copper: the AI picks the path, these verbs lay the trace (handlers/route.py, handlers/live_routing.py)+    "ipc_api": handle_ipc_api,+    "routing_state": handle_routing_state,+    "route_net": handle_route_net,+    "remove_route": handle_remove_route,+    "routing_validate": handle_routing_validate,+    "board_pads": handle_board_pads,+    "add_track": handle_add_track,+    "add_via": handle_add_via,+    "route": handle_route,     "run_drc": handle_run_drc,     "fix_keyboard": handle_fix_keyboard,     "screenshot_all": handle_screenshot_all,@@ -1134,7 +1185,8 @@ def dispatch_command(command: str, args: dict) -> dict:     _PASSIVE_VERBS = {"status", "bridge_status", "state", "readiness", "window_info",                       "open_editors", "screenshot_all", "plugin_diagnose", "describe",                       "get_settings", "diagnostics", "check_for_updates"}-    _NO_GUI = {"run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library",+    _NO_GUI = {"ipc_api", "routing_state", "route_net", "remove_route", "routing_validate", "board_pads",+               "run_drc", "run_erc", "lint_board", "lint_schematic", "lint_library",                "format_upgrade", "export_gerber", "export_gerbers", "export_pdf",                "export_svg", "export_step", "export_bom_csv", "adom_library_status",                "list_versions", "install_library", "progress", "list_footprints", "verb_times"}