← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.69+0.9.70
CONTRIBUTING.md+7−7
@@ -1,6 +1,6 @@ # Contributing -This bridge is part of the [Adom Desktop](https://github.com/adom-inc/adom-desktop) bridge ecosystem. It lives in its own repo (split from the main monorepo in 2026-05) specifically so the community can fork and improve it without coordinating with Adom Desktop's release cycle.+This bridge is part of the [Adom Bridge](https://github.com/adom-inc/adom-desktop) bridge ecosystem. It lives in its own repo (split from the main monorepo in 2026-05) specifically so the community can fork and improve it without coordinating with Adom Bridge's release cycle.  ## Two-way street @@ -9,22 +9,22 @@ This bridge is part of the [Adom Desktop](https://github.com/adom-inc/adom-deskt  ## How shipping works -1. **Develop locally.** Clone this repo, edit, test. The bridge runs alongside an installed [Adom Desktop](https://github.com/adom-inc/adom-desktop); use `adom-desktop bridge_install` with a local `file://` manifest URL to test your changes against a real GUI.+1. **Develop locally.** Clone this repo, edit, test. The bridge runs alongside an installed [Adom Bridge](https://github.com/adom-inc/adom-desktop); use `adom-desktop bridge_install` with a local `file://` manifest URL to test your changes against a real GUI. 2. **Open a PR** against `main`. The CI in `.github/workflows/validate.yml` checks that `bridge.json` is valid + verb prefixes look right + the BRIDGE_VERSION was bumped vs. main. 3. **Maintainer reviews + merges.** No squash-merge — we preserve your authored commits.-4. **Maintainer tags + ships.** A maintainer bumps the version in lockstep (`bridge.json` / `BRIDGE_VERSION` / the manifest), builds the runtime zip and publishes it as a **Release asset** with `adom-wiki release create` + `release upload`, alongside the manifest on [the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges). Anyone running Adom Desktop v1.7.19+ picks it up within 4 hours or on the next `adom-desktop refresh_bridges`.+4. **Maintainer tags + ships.** A maintainer bumps the version in lockstep (`bridge.json` / `BRIDGE_VERSION` / the manifest), builds the runtime zip and publishes it as a **Release asset** with `adom-wiki release create` + `release upload`, alongside the manifest on [the Adom Wiki](https://wiki.adom.inc/adom/adom-bridge-sdk). Anyone running Adom Bridge v1.7.19+ picks it up within 4 hours or on the next `adom-desktop refresh_bridges`.  We DON'T auto-ship from GitHub Actions today. Reason: publishing needs an Adom Wiki API token (`adom-wiki` auth), and we keep that secret on maintainer machines rather than in GH Actions secrets. The community trust model around auto-publishing community PRs to a shared wiki space is part of a separate effort ("Chunk D" in adom-desktop's TODO.md) — comment on an issue if you want to help design that.  ## Bridge.json schema -See [the SDK guide on the Adom Wiki](https://wiki.adom.inc/adom/adom-desktop-bridges) for the authoritative schema. Quick summary:+See [the SDK guide on the Adom Wiki](https://wiki.adom.inc/adom/adom-bridge-sdk) for the authoritative schema. Quick summary:  - `manifest_version: 1` - `name` — must match the directory name in adom-desktop's `plugins/` (when bundled) - `version` — semver, bumped on every PR that ships-- `spawn.port: 0` — **always use 0** as of Adom Desktop v1.8.31+. Adom Desktop picks an OS-assigned ephemeral port and passes it via `--port <N>` to your bridge process. Don't hardcode.-- `spawn.healthEndpoint: "/status"` — path-only (recommended). Adom Desktop builds the URL using the runtime port.+- `spawn.port: 0` — **always use 0** as of Adom Bridge v1.8.31+. Adom Bridge picks an OS-assigned ephemeral port and passes it via `--port <N>` to your bridge process. Don't hardcode.+- `spawn.healthEndpoint: "/status"` — path-only (recommended). Adom Bridge builds the URL using the runtime port. - `verbPrefixes: ["<name>_"]` — the CLI verb prefixes this bridge owns.  ## Versioning@@ -61,7 +61,7 @@ bash scripts/demo-thirdparty-bridge.sh  # in adom-desktop repo When adom-desktop was a monorepo, all bridges shipped on adom-desktop's NSIS release cycle — meaning a community contributor couldn't release a KiCad fix without waiting for an adom-desktop maintainer to cut a new desktop installer. As of 2026-05 the bridges live in independent repos so:  - Community can release at their own cadence-- Adom Desktop's installer ships a "known-good" snapshot but auto-updates from the wiki for newer versions+- Adom Bridge's installer ships a "known-good" snapshot but auto-updates from the wiki for newer versions - A maintainer outside Adom Industries can take over a bridge (e.g. a KiCad 11 expert maintains the KiCad bridge) without commit access to adom-desktop itself  See [adom-desktop TODO.md → Chunks C/D](https://github.com/adom-inc/adom-desktop/blob/main/TODO.md) for the broader design.
README.md+55−27
@@ -1,28 +1,14 @@-# Adom Desktop — KiCad Bridge+# Adom Bridge — KiCad Bridge -A **reverse bridge** that lets Adom Desktop (AD) — and the AI driving it — control the user's own+A **reverse bridge** that lets Adom Bridge (AD) — and the AI driving it — control the user's own KiCad on Windows: open and drive every editor, install symbol/footprint libraries, place parts, run DRC/ERC, export manufacturing files, and screenshot any window back to the AI. -> ### Two artifacts on this page — which one do you want?->-> | | **Release ZIP** (`adom-bridge-kicad-v*.zip`) | **Package** (`adom-wiki pkg install adom/adom-desktop-kicad-bridge`) |-> |---|---|---|-> | What it is | The **bridge runtime** — the actual Python server that drives KiCad | **Skills + docs only**, for a container/agent |-> | Who loads it | **Adom Desktop** downloads it automatically via `adom-bridge-kicad-manifest.json` | You, into a container, to teach an agent the `kicad_*` verbs |-> | Contains | code, certs, UIA scripts, board template | `SKILL.md` files (no binaries, no images) |-> | You install it manually? | **No** — AD handles it | Yes |->-> **The package is not the bridge.** Installing the package does not give you a working-> bridge; AD loading the release zip does. Both are kept deliberately small — the zip is-> auto-bundled into AD, so it ships only what the runtime needs.-- KiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if none is present, *installs* one for them — it never asks the user to download or click through anything by hand. -- **Page / docs:** https://wiki.adom.inc/adom/adom-desktop-kicad-bridge-- **Bridge SDK:** https://wiki.adom.inc/adom/adom-desktop-bridges+- **Page / docs:** https://wiki.adom.inc/adom/kicad-bridge+- **Bridge SDK:** https://wiki.adom.inc/adom/adom-bridge-sdk - **Verb namespace:** `kicad_*` &nbsp;·&nbsp; **Status verb:** `kicad_bridge_status` &nbsp;·&nbsp; **Health:** `GET /status`  > Every screenshot in this README was captured on **ADOMBASELINE**, a stock Hyper-V VM with **no@@ -33,10 +19,52 @@ KiCad is the user's **host app**. The bridge *detects* an existing KiCad and, if  ## Demo — narrated live editor tour (real screen recording) -[![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)+[![KiCad bridge live demo](https://wiki.adom.inc/blob/app/kicad-bridge/kicad-bridge-demo-poster.png)](https://wiki.adom.inc/blob/app/kicad-bridge/kicad-bridge-demo.mp4)++**▶ [Watch the demo](https://wiki.adom.inc/blob/app/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. -**▶ [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. +---++## The demo verb — `kicad_demo`++One verb that shows the whole bridge off, built for the AI that demos Adom Bridge+during Hydrogen's install. Six beats, in the order a hardware person thinks:++| Beat | Shows |+|---|---|+| 1 | the **schematic symbol** in the user's own Symbol Editor |+| 2 | the **footprint** for that same part |+| 3 | that part in **3D** |+| 4 | a **schematic** using it (U1 + two 10k resistors) |+| 5 | the **2D board layout** |+| 6 | the finished **board in 3D** |++```bash+adom-desktop kicad_demo '{}'                      # start: prepares, warms KiCad, opens beat 1+adom-desktop kicad_demo '{"step":"footprint"}'    # ...then follow data.nextCall each time+adom-desktop kicad_demo '{"all":true}'            # bulk (non-interactive; slower than one request budget)+adom-desktop kicad_close '{"force":true}'         # tear down+```++**It narrates itself.** Every beat returns `say` (a line to speak), `pointOut`+(what's actually on screen), `window` (match it in `kicad_screenshot_all`) and+`nextCall`. One beat per call is the default precisely so the AI can talk, screenshot,+then move on.++**It ships nothing.** The two demo parts, the schematic and the board are all+generated in code, and the 3D bodies come from KiCad's *own* bundled+`3dmodels/*.3dshapes` — so the runtime zip stays small and the 3D views are real.+Parts install into the user's real `Adom` library; the project lands in+`Documents/adom-kicad-demo`.++**No KiCad? It offers to install it.** `kicad_demo` returns an offer instead of an+error; `kicad_demo '{"installKiCad": true}'` silently installs the official build+(per-user, no UAC) and then runs the tour. Watching the AI install your EDA tool is+itself a good demo beat.++**It never takes your screen.** Every window opens in the background and all input is+window-targeted (UIA / `PostMessage`), so you can keep working while it runs.  --- @@ -47,7 +75,7 @@ server.py`, `port: 0` — AD picks the port and passes `ADOM_BIND_HOST`). It spe (`POST /command`, `GET /status`) and reaches KiCad through **four control surfaces**, picking the lightest one that can do the job. -![Control surfaces](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/control-surfaces.svg)+![Control surfaces](https://wiki.adom.inc/blob/app/kicad-bridge/control-surfaces.svg)  | # | Surface | Used for | Where | |---|---------|----------|-------|@@ -67,33 +95,33 @@ generated by `tour-pack-rp2040/` (an `AdomRP2040` symbol + `QFN-56_AdomRP2040` f  The RP2040 breakout: the MCU (U1), USB-C, a 12 MHz crystal, decoupling, and mounting pins. -![Schematic editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/schematic-editor.png)+![Schematic editor](https://wiki.adom.inc/blob/app/kicad-bridge/schematic-editor.png)  ### Symbol editor — `kicad_open_symbol_editor`  Opens straight to a symbol (`kicad_install_symbol` puts it in a user library first). -![Symbol editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/symbol-editor.png)+![Symbol editor](https://wiki.adom.inc/blob/app/kicad-bridge/symbol-editor.png)  ### Footprint editor — `kicad_open_footprint_editor`  `{"footprintName":"QFN-56_AdomRP2040","library":"AdomRP2040"}` loads the part directly — 56 pins + thermal pad, 7×7 mm, 0.4 mm pitch, courtyard and silkscreen. -![Footprint editor](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/footprint-editor.png)+![Footprint editor](https://wiki.adom.inc/blob/app/kicad-bridge/footprint-editor.png)  ### PCB editor (2D) — `kicad_open_board`  The routed breakout — copper, silkscreen, the QFN-56 land pattern, mounting holes. -![PCB editor 2D](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/pcb-editor-2d.png)+![PCB editor 2D](https://wiki.adom.inc/blob/app/kicad-bridge/pcb-editor-2d.png)  ### 3D viewer — `kicad_open_3d_viewer {"editor":"pcb"}`  The full board in 3D — board body, the USB-C connector's 3D model, the QFN chip body, SMD parts, plated through-holes. **Rendered on the CPU via the software-OpenGL fallback (below); reload 1.7 s.** -![3D viewer](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/3d-viewer.png)+![3D viewer](https://wiki.adom.inc/blob/app/kicad-bridge/3d-viewer.png)  --- @@ -129,7 +157,7 @@ Auto-expired benign dialogs include: - **"Welcome to KiCad — starting for the first time"** first-run wizard (dismissing accepts defaults). - **"This file was created by an older version of KiCad"** conversion notice. -![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/dialog-firstrun-wizard.png)+![First-run wizard, auto-dismissed](https://wiki.adom.inc/blob/app/kicad-bridge/dialog-firstrun-wizard.png)  Verbs: 
SKILL.md+144−144
@@ -1,144 +1,144 @@----
-name: adom-desktop-kicad
-public: true
-description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-desktop CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge."
----
-
-# adom-desktop — KiCad bridge
-
-All commands dispatch to the KiCad Python bridge running alongside the desktop app. Invoke from this container via Bash:
-
-```bash
-adom-desktop kicad_<action> '<json_args>'
-```
-
-The leading `kicad_` routes the call to the KiCad plugin; the `<action>` names below are the plugin-side command names.
-
-## FIRST-TIME / COLD-START — read before you panic
-
-A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs on your first call. These states are **EXPECTED** — don't declare defeat. Branch on the stable `errorCode` (not the prose), and relay the returned `_hint` to the user verbatim.
-
-| Signal (`errorCode` / field) | What it means | What YOU do |
-|---|---|---|
-| `host_app_not_installed` (generic; `hostApp:"KiCad"`. Legacy alias `kicad_not_installed` in `errorCodeLegacy`) | KiCad (the user's host app) isn't on this machine. AD detects host apps but never auto-installs them. | **Don't send the user to download it.** Offer to install it, then run `kicad_upgrade '{}'` — the agent installs the official build from scratch. Then retry. |
-| `kicad_version_not_installed` | The `kicadVersion` you asked for isn't installed. | Run `kicad_list_versions '{}'`; retry with an installed version or omit `kicadVersion` to use the newest. |
-| `pluginAutoInstall` present in a response | Your first `kicad_*` call auto-installed the reverse-bridge plugin into KiCad (idempotent). | Nothing — expected. Subsequent calls are near-zero cost. |
-| bridge not responding on the first call | AD is lazy-spawning the bridge (+ ~9s KiCad detection). | Wait a beat and retry; it binds once and stays up. |
-
-**Readiness before you drive:**
-- `kicad_readiness '{}'` — read-only, zero side effects: is KiCad installed/ready? (`{detected, ready, versionCount, defaultVersion}`.) There's **no prewarm** — KiCad is a host app you install on request via `kicad_upgrade`, not a downloaded runtime asset.
-- `kicad_describe '{}'` — the full machine-readable verb catalog (the completeness escape hatch; the per-call `_hint`s are what carry you in practice).
-- For **"what EDA tools do I have"** across all bridges, use AD's `bridge_readiness` (see below).
-
-## Installing KiCad on a fresh machine (hard-won, v0.9.26+)
-
-`kicad_upgrade` downloads the official downloads.kicad.org installer and silent-installs it. Two things that WILL bite on a truly fresh machine, both handled for you now:
-
-- **Install scope is mandatory in silent mode.** KiCad 10's installer is NsisMultiUser: bare `/S` exits instantly with **`rc=666660`** ("invalid command-line parameters") and installs nothing. The bridge auto-picks the scope: **`/allusers`** when the AD process is elevated (installs to Program Files), else **`/currentuser`** (installs to `%LOCALAPPDATA%\Programs\KiCad` — **no admin, no UAC prompt at all**). Force it with `kicad_upgrade '{"scope":"user"}'` — that's your answer to *"install KiCad without a UAC prompt."* (Elevation note: on a VM/kiosk the AD process may already be elevated even though nobody clicked UAC — an elevated installer that launches the app hands down its admin token. `kicad_upgrade '{"diagnoseOnly":true}'` reports the real token/UAC facts.)
-- **Library installs work before KiCad's first launch.** `%APPDATA%/kicad/<ver>/` (the per-user lib tables) doesn't exist until KiCad's first GUI run, so `kicad_install_library`/`_footprint` used to fail *"config directory not found."* The bridge now bootstraps that config (seeding the lib tables from the install template) automatically — installs just work on a never-launched KiCad.
-
-**GPU-less hosts (VM/RDP/server):** KiCad pops a blocking *"Could not use OpenGL"* dialog and the heavy editors may not render. The bridge auto-expires that dialog and switches KiCad to the Cairo software canvas — see **[kicad-interaction](skills/kicad-interaction/SKILL.md)** for the dialog/canvas details.
-
-## Multi-version (KiCad 9 + KiCad 10 side-by-side)
-
-Users can have multiple KiCad versions installed at the same time. The bridge auto-discovers everything under `C:/Program Files/KiCad/<version>/` and **defaults to the newest installed version** if you don't specify one.
-
-```bash
-# Discover what's installed (run this first if unsure)
-adom-desktop kicad_list_versions '{}'
-# → {"versions":[{"version":"10.0", "default":true, ...}, {"version":"9.0", ...}], "default":"10.0", "count":2}
-```
-
-Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional **`kicadVersion`** arg to pin a specific install:
-
-```bash
-# Open in KiCad 10 (newest — default if you omit kicadVersion)
-adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}'
-
-# Open in KiCad 9 explicitly (e.g. to check that an older project still works)
-adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}'
-```
-
-If you pass a version that's not installed, the bridge returns a hint listing the installed versions. The success response includes `kicadVersionUsed` so you can confirm which install actually ran.
-
-> **Note on libraries / config**: each KiCad version has its own `sym-lib-table` and `fp-lib-table` (in `%APPDATA%/kicad/<version>/`). `kicad_install_library` writes to the table of whichever version was selected — so if the user installs a library and then asks "open it in KiCad 9", you may need to install it under both versions.
-
-## "What EDA tools do I have?" — readiness & host-app detection
-
-**KiCad is the user's HOST APP. Adom Desktop *detects* it — it won't silently auto-install it as a prewarm/dependency** (AD v1.9.47+). But if KiCad isn't found, **do NOT hand the user a manual download** — offer to install it for them and run `kicad_upgrade '{}'`. That verb installs the latest stable from the official downloads.kicad.org installer (silent `/S`, Windows only; may raise a UAC prompt) and **installs from scratch, not just upgrades**. The distinction: AD-core never auto-installs a host app, but a user-requested, agent-run install is exactly right.
-
-- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the AD-level verb **`adom-desktop bridge_readiness '{}'`** — AD aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question.
-- For **KiCad specifically**, `adom-desktop kicad_readiness '{}'` is a zero-side-effect read-only probe: `{hostApp, detected, ready, versionCount, defaultVersion, versions[]}`. It does not install the plugin or spawn subprocesses, and works even when KiCad is absent (`detected:false`). Use `kicad_list_versions` when you need the full per-version detail (paths, IPC API, lib tables).
-
-## Commands
-
-| CLI form | Action | Purpose | Key args |
-|---|---|---|---|
-| `kicad_list_versions` | `list_versions` | Enumerate every installed KiCad version with paths and which one is the default | — |
-| `kicad_readiness` | `readiness` | Read-only host-app detection: is KiCad installed/ready? Zero side effects (no plugin load, no subprocess); works even when KiCad is absent. For cross-bridge "what do I have", use AD's `bridge_readiness` | — |
-| `kicad_check_for_updates` | `check_for_updates` | Read-only: installed KiCad vs latest STABLE (from downloads.kicad.org / KiCad GitLab tags); flags when the install predates the modern IPC/kipy automation API and should be upgraded | — |
-| `kicad_upgrade` | `upgrade` | Download the **official** KiCad installer from downloads.kicad.org and silent-install it (`/S`) — **not winget** (winget lags the real release). May raise a UAC prompt to approve | optional `version`, `force` |
-| `kicad_open_schematic` | `open_schematic` | Open a `.kicad_sch` in the Schematic Editor | `filePath`, optional `kicadVersion` |
-| `kicad_open_board` | `open_board` | Open a `.kicad_pcb` in the PCB Editor | `filePath`, optional `kicadVersion` |
-| `kicad_open_symbol_editor` | `open_symbol_editor` | Launch Symbol Editor (optional: open specific library/symbol) | `libraryName`, `symbolName`, optional `kicadVersion` |
-| `kicad_open_footprint_editor` | `open_footprint_editor` | Launch Footprint Editor | `libraryName`, `footprintName`, optional `kicadVersion` |
-| `kicad_open_3d_viewer` | `open_3d_viewer` | Show the board (or footprint) in 3D viewer | `editor` ("auto"/"pcb"/"fp" — "auto" prefers the board when both editors are open, no need to close sub-editors), optional `kicadVersion` |
-| `kicad_close_symbol_editor` | `close_symbol_editor` | Close Symbol Editor | — |
-| `kicad_close_footprint_editor` | `close_footprint_editor` | Close Footprint Editor | — |
-| `kicad_close_3d_viewer` | `close_3d_viewer` | Close 3D viewer | — |
-| `kicad_close` | `close` | Close KiCad (all editors) | — |
-| `kicad_window_info` | `window_info` | Enumerate open KiCad windows (HWND, title, bounds, editor type) | — |
-| `kicad_install_library` | `install_library` | Register a `.kicad_sym` + `.pretty/` pair in sym-lib-table + fp-lib-table | `libraryPath`, `libraryType`, `libraryName`, optional `kicadVersion` |
-| `kicad_install_symbol` | `install_symbol` | Add a single symbol to an existing library | `fileName`, `fileContent`, optional `kicadVersion` |
-| `kicad_install_footprint` | `install_footprint` | Add a single footprint to an existing library | `library`, `footprint_path`, optional `kicadVersion` |
-| `kicad_run_drc` | `run_drc` | Run DRC on the current board, return violations | `filePath`, optional `kicadVersion` |
-| `kicad_fix_keyboard` | `fix_keyboard` | Workaround stuck-modifier bug after screen lock | — |
-| `kicad_screenshot_all` | `screenshot_all` | Capture every open KiCad window (lossless PNGs) | — |
-| `kicad_send_key` | `send_key` | Single key to an EXACT `hwnd` (e.g. Enter/Escape to a dialog). For modifier CHORDS ("ctrl+s", "alt+3") use AD's `desktop_press_key` — don't route chords here. | `hwnd`, `key` |
-| `kicad_click` | `click` | Send a click at (x, y) in a KiCad window | `hwnd`, `x`, `y` |
-| `kicad_adom_library_status` | `adom_library_status` | Report whether the Adom shared library is registered | — |
-
-## Quick examples
-
-```bash
-# Open a schematic
-adom-desktop kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}'
-
-# List open KiCad windows
-adom-desktop kicad_window_info '{}'
-
-# Screenshot every KiCad window
-adom-desktop kicad_screenshot_all '{}'
-
-# Run DRC
-adom-desktop kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}'
-
-# Install a library pair
-adom-desktop kicad_install_library '{
-  "name":"my-lib",
-  "sym_path":"C:/libs/my-lib.kicad_sym",
-  "fp_path":"C:/libs/my-lib.pretty"
-}'
-```
-
-## Error shape
-
-Every failing response includes a `_hint` field with a human-readable next step — surface it to the user verbatim when a KiCad command fails.
-
-## Bridge architecture (v1.8.31+) — you don't need to know any port
-
-Earlier versions of the KiCad bridge listened on hardcoded port `8772`. **As of v1.8.31, bridges use OS-assigned ephemeral ports** — every spawn gets a new free port. You never need to know it.
-
-- The CLI (`adom-desktop kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-desktop then forwards to the bridge's actual runtime port.
-- The bridge port is internal plumbing and can change every spawn. `bridge_list` reports it as `spawn.runtimePort` for debugging only — do NOT hardcode that anywhere.
-- Why: this lets adom-desktop coexist with Hydrogen Desktop (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide.
-- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-desktop kicad_*` verbs, same JSON shape.
-
-If you ever find yourself wanting to talk directly to the KiCad bridge process: don't. Go through the verb dispatch. If the verb you need doesn't exist, request a feature in `adom-inc/adom-desktop`.
-
-## Related
-
-- `adom-desktop-discovery` (gallia) — ensures the CLI + relay are installed.
-- `adom-desktop-fusion` — sibling bridge for Fusion 360.
-- `adom-desktop-direct-api` — the direct API contract (port 47200 + 47201-47209 fallback, discovery file at `~/.adom/direct-api-port`).
-- Repo: `adom-inc/adom-desktop/plugins/kicad/`
+---+name: adom-desktop-kicad+public: true+description: "Launch and drive KiCad on the user's laptop (KiCad 7/8/9/10 supported, multi-version side-by-side) from this container via the adom-desktop CLI. Open schematics/boards/symbols/footprints, run DRC, install sym-lib-table / fp-lib-table libraries, show the 3D viewer, capture KiCad window screenshots, send keyboard shortcuts and click coordinates. Trigger words: kicad, open schematic, open board, open pcb, open symbol, open footprint, KiCad symbol editor, KiCad footprint editor, KiCad 3D viewer, run DRC, install KiCad library, sym-lib-table, fp-lib-table, KiCad screenshot, kicad_send_key, kicad_click, kicad_window_info, kicad_screenshot_all, close KiCad, kicad versions, kicad_list_versions, KiCad bridge."+---++# adom-desktop — KiCad bridge++All commands dispatch to the KiCad Python bridge running alongside the desktop app. Invoke from this container via Bash:++```bash+adom-desktop kicad_<action> '<json_args>'+```++The leading `kicad_` routes the call to the KiCad plugin; the `<action>` names below are the plugin-side command names.++## FIRST-TIME / COLD-START — read before you panic++A fresh machine can be missing KiCad, or the reverse-bridge plugin auto-installs on your first call. These states are **EXPECTED** — don't declare defeat. Branch on the stable `errorCode` (not the prose), and relay the returned `_hint` to the user verbatim.++| Signal (`errorCode` / field) | What it means | What YOU do |+|---|---|---|+| `host_app_not_installed` (generic; `hostApp:"KiCad"`. Legacy alias `kicad_not_installed` in `errorCodeLegacy`) | KiCad (the user's host app) isn't on this machine. AD detects host apps but never auto-installs them. | **Don't send the user to download it.** Offer to install it, then run `kicad_upgrade '{}'` — the agent installs the official build from scratch. Then retry. |+| `kicad_version_not_installed` | The `kicadVersion` you asked for isn't installed. | Run `kicad_list_versions '{}'`; retry with an installed version or omit `kicadVersion` to use the newest. |+| `pluginAutoInstall` present in a response | Your first `kicad_*` call auto-installed the reverse-bridge plugin into KiCad (idempotent). | Nothing — expected. Subsequent calls are near-zero cost. |+| bridge not responding on the first call | AD is lazy-spawning the bridge (+ ~9s KiCad detection). | Wait a beat and retry; it binds once and stays up. |++**Readiness before you drive:**+- `kicad_readiness '{}'` — read-only, zero side effects: is KiCad installed/ready? (`{detected, ready, versionCount, defaultVersion}`.) There's **no prewarm** — KiCad is a host app you install on request via `kicad_upgrade`, not a downloaded runtime asset.+- `kicad_describe '{}'` — the full machine-readable verb catalog (the completeness escape hatch; the per-call `_hint`s are what carry you in practice).+- For **"what EDA tools do I have"** across all bridges, use AD's `bridge_readiness` (see below).++## Installing KiCad on a fresh machine (hard-won, v0.9.26+)++`kicad_upgrade` downloads the official downloads.kicad.org installer and silent-installs it. Two things that WILL bite on a truly fresh machine, both handled for you now:++- **Install scope is mandatory in silent mode.** KiCad 10's installer is NsisMultiUser: bare `/S` exits instantly with **`rc=666660`** ("invalid command-line parameters") and installs nothing. The bridge auto-picks the scope: **`/allusers`** when the AD process is elevated (installs to Program Files), else **`/currentuser`** (installs to `%LOCALAPPDATA%\Programs\KiCad` — **no admin, no UAC prompt at all**). Force it with `kicad_upgrade '{"scope":"user"}'` — that's your answer to *"install KiCad without a UAC prompt."* (Elevation note: on a VM/kiosk the AD process may already be elevated even though nobody clicked UAC — an elevated installer that launches the app hands down its admin token. `kicad_upgrade '{"diagnoseOnly":true}'` reports the real token/UAC facts.)+- **Library installs work before KiCad's first launch.** `%APPDATA%/kicad/<ver>/` (the per-user lib tables) doesn't exist until KiCad's first GUI run, so `kicad_install_library`/`_footprint` used to fail *"config directory not found."* The bridge now bootstraps that config (seeding the lib tables from the install template) automatically — installs just work on a never-launched KiCad.++**GPU-less hosts (VM/RDP/server):** KiCad pops a blocking *"Could not use OpenGL"* dialog and the heavy editors may not render. The bridge auto-expires that dialog and switches KiCad to the Cairo software canvas — see **[kicad-interaction](skills/kicad-interaction/SKILL.md)** for the dialog/canvas details.++## Multi-version (KiCad 9 + KiCad 10 side-by-side)++Users can have multiple KiCad versions installed at the same time. The bridge auto-discovers everything under `C:/Program Files/KiCad/<version>/` and **defaults to the newest installed version** if you don't specify one.++```bash+# Discover what's installed (run this first if unsure)+adom-desktop kicad_list_versions '{}'+# → {"versions":[{"version":"10.0", "default":true, ...}, {"version":"9.0", ...}], "default":"10.0", "count":2}+```++Every `kicad_open_*` (and `install_*`, `run_drc`) command accepts an optional **`kicadVersion`** arg to pin a specific install:++```bash+# Open in KiCad 10 (newest — default if you omit kicadVersion)+adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb"}'++# Open in KiCad 9 explicitly (e.g. to check that an older project still works)+adom-desktop kicad_open_board '{"filePath":"C:/designs/foo.kicad_pcb","kicadVersion":"9.0"}'+```++If you pass a version that's not installed, the bridge returns a hint listing the installed versions. The success response includes `kicadVersionUsed` so you can confirm which install actually ran.++> **Note on libraries / config**: each KiCad version has its own `sym-lib-table` and `fp-lib-table` (in `%APPDATA%/kicad/<version>/`). `kicad_install_library` writes to the table of whichever version was selected — so if the user installs a library and then asks "open it in KiCad 9", you may need to install it under both versions.++## "What EDA tools do I have?" — readiness & host-app detection++**KiCad is the user's HOST APP. Adom Bridge *detects* it — it won't silently auto-install it as a prewarm/dependency** (AD v1.9.47+). But if KiCad isn't found, **do NOT hand the user a manual download** — offer to install it for them and run `kicad_upgrade '{}'`. That verb installs the latest stable from the official downloads.kicad.org installer (silent `/S`, Windows only; may raise a UAC prompt) and **installs from scratch, not just upgrades**. The distinction: AD-core never auto-installs a host app, but a user-requested, agent-run install is exactly right.++- To answer **"what EDA tools / host apps do I have?"** across every bridge, call the AD-level verb **`adom-desktop bridge_readiness '{}'`** — AD aggregates each bridge's host-app detection (KiCad, Fusion 360, …) into one report. Start here for any "do I have X installed?" question.+- For **KiCad specifically**, `adom-desktop kicad_readiness '{}'` is a zero-side-effect read-only probe: `{hostApp, detected, ready, versionCount, defaultVersion, versions[]}`. It does not install the plugin or spawn subprocesses, and works even when KiCad is absent (`detected:false`). Use `kicad_list_versions` when you need the full per-version detail (paths, IPC API, lib tables).++## Commands++| CLI form | Action | Purpose | Key args |+|---|---|---|---|+| `kicad_list_versions` | `list_versions` | Enumerate every installed KiCad version with paths and which one is the default | — |+| `kicad_readiness` | `readiness` | Read-only host-app detection: is KiCad installed/ready? Zero side effects (no plugin load, no subprocess); works even when KiCad is absent. For cross-bridge "what do I have", use AD's `bridge_readiness` | — |+| `kicad_check_for_updates` | `check_for_updates` | Read-only: installed KiCad vs latest STABLE (from downloads.kicad.org / KiCad GitLab tags); flags when the install predates the modern IPC/kipy automation API and should be upgraded | — |+| `kicad_upgrade` | `upgrade` | Download the **official** KiCad installer from downloads.kicad.org and silent-install it (`/S`) — **not winget** (winget lags the real release). May raise a UAC prompt to approve | optional `version`, `force` |+| `kicad_open_schematic` | `open_schematic` | Open a `.kicad_sch` in the Schematic Editor | `filePath`, optional `kicadVersion` |+| `kicad_open_board` | `open_board` | Open a `.kicad_pcb` in the PCB Editor | `filePath`, optional `kicadVersion` |+| `kicad_open_symbol_editor` | `open_symbol_editor` | Launch Symbol Editor (optional: open specific library/symbol) | `libraryName`, `symbolName`, optional `kicadVersion` |+| `kicad_open_footprint_editor` | `open_footprint_editor` | Launch Footprint Editor | `libraryName`, `footprintName`, optional `kicadVersion` |+| `kicad_open_3d_viewer` | `open_3d_viewer` | Show the board (or footprint) in 3D viewer | `editor` ("auto"/"pcb"/"fp" — "auto" prefers the board when both editors are open, no need to close sub-editors), optional `kicadVersion` |+| `kicad_close_symbol_editor` | `close_symbol_editor` | Close Symbol Editor | — |+| `kicad_close_footprint_editor` | `close_footprint_editor` | Close Footprint Editor | — |+| `kicad_close_3d_viewer` | `close_3d_viewer` | Close 3D viewer | — |+| `kicad_close` | `close` | Close KiCad (all editors) | — |+| `kicad_window_info` | `window_info` | Enumerate open KiCad windows (HWND, title, bounds, editor type) | — |+| `kicad_install_library` | `install_library` | Register a `.kicad_sym` + `.pretty/` pair in sym-lib-table + fp-lib-table | `libraryPath`, `libraryType`, `libraryName`, optional `kicadVersion` |+| `kicad_install_symbol` | `install_symbol` | Add a single symbol to an existing library | `fileName`, `fileContent`, optional `kicadVersion` |+| `kicad_install_footprint` | `install_footprint` | Add a single footprint to an existing library | `library`, `footprint_path`, optional `kicadVersion` |+| `kicad_run_drc` | `run_drc` | Run DRC on the current board, return violations | `filePath`, optional `kicadVersion` |+| `kicad_fix_keyboard` | `fix_keyboard` | Workaround stuck-modifier bug after screen lock | — |+| `kicad_screenshot_all` | `screenshot_all` | Capture every open KiCad window (lossless PNGs) | — |+| `kicad_send_key` | `send_key` | Single key to an EXACT `hwnd` (e.g. Enter/Escape to a dialog). For modifier CHORDS ("ctrl+s", "alt+3") use AD's `desktop_press_key` — don't route chords here. | `hwnd`, `key` |+| `kicad_click` | `click` | Send a click at (x, y) in a KiCad window | `hwnd`, `x`, `y` |+| `kicad_adom_library_status` | `adom_library_status` | Report whether the Adom shared library is registered | — |++## Quick examples++```bash+# Open a schematic+adom-desktop kicad_open_schematic '{"path":"C:/designs/foo.kicad_sch"}'++# List open KiCad windows+adom-desktop kicad_window_info '{}'++# Screenshot every KiCad window+adom-desktop kicad_screenshot_all '{}'++# Run DRC+adom-desktop kicad_run_drc '{"board_path":"C:/designs/foo.kicad_pcb"}'++# Install a library pair+adom-desktop kicad_install_library '{+  "name":"my-lib",+  "sym_path":"C:/libs/my-lib.kicad_sym",+  "fp_path":"C:/libs/my-lib.pretty"+}'+```++## Error shape++Every failing response includes a `_hint` field with a human-readable next step — surface it to the user verbatim when a KiCad command fails.++## Bridge architecture (v1.8.31+) — you don't need to know any port++Earlier versions of the KiCad bridge listened on hardcoded port `8772`. **As of v1.8.31, bridges use OS-assigned ephemeral ports** — every spawn gets a new free port. You never need to know it.++- The CLI (`adom-desktop kicad_*`) automatically routes through adom-desktop's direct API (default port 47200 as of v1.8.33, with auto-fallback to 47201-47209 if 47200 is taken). adom-desktop then forwards to the bridge's actual runtime port.+- The bridge port is internal plumbing and can change every spawn. `bridge_list` reports it as `spawn.runtimePort` for debugging only — do NOT hardcode that anywhere.+- Why: this lets adom-desktop coexist with Hydrogen (which used to claim 8772 itself) and any other tool that grabs ports in the 8000 range. Side-by-side bridges from different apps no longer collide.+- The relay path (Docker → wss proxy → Windows GUI) is unchanged — same `adom-desktop kicad_*` verbs, same JSON shape.++If you ever find yourself wanting to talk directly to the KiCad bridge process: don't. Go through the verb dispatch. If the verb you need doesn't exist, request a feature in `adom-inc/adom-desktop`.++## Related++- `adom-desktop-discovery` (gallia) — ensures the CLI + relay are installed.+- `adom-desktop-fusion` — sibling bridge for Fusion 360.+- `adom-desktop-direct-api` — the direct API contract (port 47200 + 47201-47209 fallback, discovery file at `~/.adom/direct-api-port`).+- Repo: `adom-inc/adom-desktop/plugins/kicad/`
adom-bridge-kicad-manifest.json+7−7
@@ -1,19 +1,19 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.69",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.69/adom-bridge-kicad-v0.9.69.zip",-  "sha256": "9f8f6ae26a034c9bf4e97a30b218659bffa30a8cac9b92c2a0acd0129954b6d8",-  "size": 365869,+  "version": "0.9.70",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.70/adom-bridge-kicad-v0.9.70.zip",+  "sha256": "91f626909c22c745703250b86630182d254c752c2e4369e2aa8ccbbcbe11a702",+  "size": 365778,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-07-25T14:13:50Z",-  "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",+  "released_at": "2026-08-08T23:07:12Z",+  "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",     "PowerShell"   ],-  "updateManifestUrl": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/adom-bridge-kicad-manifest.json"+  "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json" }\ No newline at end of file
adom-bridge-kicad-v0.9.70.zipadded
⋯ 1 unchanged line ⋯
adom_library.py+26−5
@@ -1,6 +1,6 @@ """Manage the single global Adom symbol library (Adom.kicad_sym). -All symbols installed by Adom Desktop live in one KiCad symbol+All symbols installed by Adom Bridge live in one KiCad symbol library file.  This module handles:  - Creating the library file if it doesn't exist@@ -17,7 +17,7 @@ from lib_table import LibTable, LibEntry # The one library name used everywhere ADOM_LIB_NAME = "Adom" ADOM_LIB_FILE = "Adom.kicad_sym"-ADOM_LIB_DESCRIPTION = "Adom Desktop - custom symbols"+ADOM_LIB_DESCRIPTION = "Adom Bridge - custom symbols"  # Regex to extract top-level (symbol "NAME" ...) blocks. # Top-level symbols are indented with exactly one leading whitespace group@@ -38,6 +38,21 @@ def _get_paths(kicad_info: dict) -> tuple[Path, Path] | None:     """Return (sym_lib_table_path, adom_kicad_sym_path) or None."""     config_dir = kicad_info.get("config_dir")     user_dir = kicad_info.get("user_dir")+    # Bootstrap config on a never-launched KiCad (same self-heal install_footprint+    # does). %APPDATA%/kicad/<ver>/ + Documents/KiCad/<ver>/ don't exist until the+    # first GUI launch; without this, symbol installs fail "config/user directory+    # not found" on a fresh install even though KiCad is present. (ADOMBASELINE 2026-07.)+    if not config_dir or not user_dir:+        try:+            import kicad_detect+            c, u = kicad_detect.ensure_win_user_config(+                kicad_info.get("version") or kicad_info.get("activeVersion") or "",+                kicad_info.get("base_dir"),+            )+            if c and u:+                config_dir, user_dir = str(c), str(u)+        except Exception:+            pass     if not config_dir or not user_dir:         return None     table_path = Path(config_dir) / "sym-lib-table"@@ -143,10 +158,16 @@ def _extract_top_level_symbols(text: str) -> list[tuple[str, str]]:     Each block includes the symbol and all its sub-units.     """     results = []-    # Find all top-level symbol openings (indented with exactly 2 spaces)-    for m in re.finditer(r'^  \(symbol "([^"]+)"', text, re.MULTILINE):+    # Find all top-level symbol openings. KiCad indents these one level under+    # (kicad_symbol_lib ...); the indent char varies — the classic pretty-printer+    # used 2 spaces, but modern KiCad (generator "kicad_symbol_editor", file+    # version 20231120+) uses a TAB. Match ANY leading whitespace and rely on the+    # `_N_N` name-suffix filter below to drop the nested sub-unit symbols. Keying+    # on exactly 2 spaces silently extracted ZERO symbols from every tab-indented+    # library → install_symbol failed "No symbols found". (ADOMBASELINE 2026-07.)+    for m in re.finditer(r'^[ \t]+\(symbol\s+"([^"]+)"', text, re.MULTILINE):         name = m.group(1)-        # Skip sub-unit symbols+        # Skip sub-unit symbols (named "Parent_0_1", "Parent_1_1", ...)         if re.search(r"_\d+_\d+$", name):             continue         block = _extract_symbol_block(text, m.start())
bridge.json+5−5
@@ -2,9 +2,9 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.69",+  "version": "0.9.70",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",-  "homepage": "https://wiki.adom.inc/adom/adom-desktop",+  "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",   "license": "MIT",   "spawn": {@@ -94,8 +94,8 @@     "kicad-10",     "kicad-11-ready"   ],-  "docs": "https://wiki.adom.inc/adom/adom-desktop-kicad-bridge",-  "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-kicad-bridge/files/adom-bridge-kicad-manifest.json",+  "docs": "https://wiki.adom.inc/adom/kicad-bridge",+  "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/kicad-bridge/files/adom-bridge-kicad-manifest.json",   "detect": {     "hostApp": "KiCad",     "appPathsExe": "kicad.exe",@@ -144,7 +144,7 @@     ],     "configKey": "project_watch"   },-  "hero": "https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/kicad-hero.png",+  "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",     "PowerShell"
handlers/demo.py+4−4
@@ -1,6 +1,6 @@ """kicad_demo — the bridge's first-class, self-contained showcase. -Adom Desktop demos this bridge during Hydrogen Desktop's install, so an AI with+Adom Bridge demos this bridge during Hydrogen's install, so an AI with no context must be able to run ONE verb and narrate a convincing tour of what the bridge can do. Six steps, in the order a hardware person thinks: @@ -19,7 +19,7 @@ Design constraints that shaped this file: * **Narration, not just status.** Every step returns `say` (a line the AI can   speak), `pointOut` (what is actually on screen worth mentioning) and the   window title to screenshot. The AI reads verb OUTPUT, not our docs.-* **KiCad may not exist yet.** During HD install that is the likely case, so+* **KiCad may not exist yet.** During Hydrogen install that is the likely case, so   the not-installed path is a first-class, actionable answer — not an error. """ @@ -43,7 +43,7 @@ R_SYM, R_FP = "AdomDemo_R", "AdomDemo_R0805" STEPS = ["symbol", "footprint", "footprint3d", "schematic", "board", "board3d"]  # Screenshot labels. `schematic` / `board_2d` / `board_3d` deliberately match the-# labels fusion_demo returns, so an AI driving BOTH bridges (HD's installer does)+# labels fusion_demo returns, so an AI driving BOTH bridges (Hydrogen's installer does) # sees one vocabulary instead of two. STEP_LABEL = {"symbol": "symbol", "footprint": "footprint", "footprint3d": "part_3d",               "schematic": "schematic", "board": "board_2d", "board3d": "board_3d"}@@ -389,7 +389,7 @@ def _run_step(kicad_info: dict, step: str, prep: dict) -> dict:  def handle_demo(kicad_info: dict, args: dict) -> dict:     """Run the bridge's showcase tour (all six steps, or one at a time)."""-    # KiCad absent is the LIKELY case during an HD install demo. Never dead-end the+    # KiCad absent is the LIKELY case during an Hydrogen install demo. Never dead-end the     # user with "install it yourself" — OFFER, and install it for them on the spot.     if not kicad_info.get("installed"):         if not args.get("installKiCad"):
handlers/install_footprint.py+9−2
@@ -22,7 +22,7 @@ from handlers.open_footprint_editor import handle_open_footprint_editor  ADOM_FP_LIB_NAME = "Adom" ADOM_FP_DIR_NAME = "Adom.pretty"-ADOM_FP_LIB_DESCRIPTION = "Adom Desktop - custom footprints"+ADOM_FP_LIB_DESCRIPTION = "Adom Bridge - custom footprints"   def handle_install_footprint(kicad_info: dict, args: dict) -> dict:@@ -86,7 +86,14 @@ def handle_install_footprint(kicad_info: dict, args: dict) -> dict:     user_dir = kicad_info.get("user_dir")     config_dir = kicad_info.get("config_dir")     if not user_dir or not config_dir:-        return {"success": False, "error": "KiCad user/config directory not found"}+        import kicad_detect+        c, u = kicad_detect.ensure_win_user_config(+            kicad_info.get("version"), kicad_info.get("base_dir")+        )+        if c and u:+            config_dir, user_dir = str(c), str(u)+    if not user_dir or not config_dir:+        return {"success": False, "error": "KiCad user/config directory not found (bootstrap failed)"}      fp_dir = Path(user_dir) / "footprints"     fp_dir.mkdir(parents=True, exist_ok=True)
handlers/install_library.py+40−7
@@ -29,7 +29,7 @@ def _install_footprint(kicad_info: dict, args: dict) -> dict:     """Install a footprint library (.kicad_mod → .pretty directory)."""     lib_path = args.get("libraryPath", "")     lib_name = args.get("libraryName", "")-    description = args.get("description", "Installed by Adom Desktop")+    description = args.get("description", "Installed by Adom Bridge")     model_3d_path = args.get("model3dPath", "")      source = Path(lib_path)@@ -43,7 +43,21 @@ def _install_footprint(kicad_info: dict, args: dict) -> dict:     config_dir = kicad_info.get("config_dir")     user_dir = kicad_info.get("user_dir")     if not config_dir or not user_dir:-        return {"success": False, "error": "KiCad config/user directory not found"}+        # Fresh install / KiCad never launched — bootstrap the per-user config+        # (seeds lib tables from the install template) the way first-run does.+        import kicad_detect+        c, u = kicad_detect.ensure_win_user_config(+            kicad_info.get("version"), kicad_info.get("base_dir")+        )+        if c and u:+            config_dir, user_dir = str(c), str(u)+    if not config_dir or not user_dir:+        return {+            "success": False,+            "error": "KiCad config/user directory not found and could not be bootstrapped",+            "_hint": "Is KiCad installed? Run kicad_list_versions. If it's freshly installed and "+                     "never launched, this bootstrap should have created %APPDATA%/kicad/<ver>/.",+        }      # Footprint libraries are .pretty directories containing .kicad_mod files     footprints_dir = Path(user_dir) / "footprints"@@ -184,7 +198,7 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict:      # --- Symbol library install (original logic) ---     lib_name = args.get("libraryName", "")-    description = args.get("description", "Installed by Adom Desktop")+    description = args.get("description", "Installed by Adom Bridge")      source = Path(lib_path)     if not source.exists():@@ -205,8 +219,22 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict:         }      config_dir = kicad_info.get("config_dir")+    user_dir = kicad_info.get("user_dir")+    if not config_dir or not user_dir:+        # Fresh install / KiCad never launched — bootstrap the per-user config.+        import kicad_detect+        c, u = kicad_detect.ensure_win_user_config(+            kicad_info.get("version"), kicad_info.get("base_dir")+        )+        if c and u:+            config_dir = config_dir or str(c)+            user_dir = user_dir or str(u)     if not config_dir:-        return {"success": False, "error": "KiCad config directory not found"}+        return {+            "success": False,+            "error": "KiCad config directory not found and could not be bootstrapped",+            "_hint": "Run kicad_list_versions to confirm KiCad is installed.",+        }      user_dir = kicad_info.get("user_dir")     if not user_dir:@@ -241,10 +269,15 @@ def handle_install_library(kicad_info: dict, args: dict) -> dict:     )      if table.has_library(lib_name):+        # Idempotent: an already-registered library is SUCCESS, not an error — the+        # desired end state (library present + registered) already holds, and the+        # .kicad_sym/.pretty file was just (re)copied above. Re-running install+        # should be a no-op-ish success so callers/retries don't treat it as failure.         return {-            "success": False,-            "error": f"Library '{lib_name}' already exists in {type_config['table_file']}",-            "_hint": "Choose a different libraryName, or call list_symbol_libraries / list_footprint_libraries to see existing entries.",+            "success": True,+            "alreadyRegistered": True,+            "output": f"Library '{lib_name}' already registered in {type_config['table_file']} (file refreshed at {dest}).",+            "_hint": "Library was already present — nothing to change. This is a successful no-op.",         }      table.add_library(entry)
handlers/software_opengl.py+2−2
@@ -23,7 +23,7 @@ from pathlib import Path  from handlers.upgrade import _ssl_context  # certifi-backed TLS (portable Python has no CA store) -_MESA_URL = "https://wiki.adom.inc/download/adom/adom-desktop-kicad-bridge/0.9.60/mesa-llvmpipe-x64.zip"+_MESA_URL = "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.60/mesa-llvmpipe-x64.zip" _MESA_DLLS = ("opengl32.dll", "libgallium_wgl.dll", "dxil.dll") _KICAD_EXES = (     "kicad.exe", "pcbnew.exe", "eeschema.exe", "gerbview.exe",@@ -62,7 +62,7 @@ def handle_enable_software_opengl(kicad_info: dict, args: dict) -> dict:         }     print(f"[KiCad Bridge] enable_software_opengl: downloading Mesa llvmpipe...", flush=True)     try:-        req = urllib.request.Request(_MESA_URL, headers={"User-Agent": "adom-desktop-kicad-bridge"})+        req = urllib.request.Request(_MESA_URL, headers={"User-Agent": "kicad-bridge"})         with urllib.request.urlopen(req, timeout=180, context=_ssl_context()) as r:             data = r.read()         zf = zipfile.ZipFile(io.BytesIO(data))
handlers/upgrade.py+267−25
@@ -13,7 +13,9 @@ bridge fetches THAT and installs it silently (`/S`). """  import json+import os import re+import ssl import sys import tempfile import urllib.error@@ -23,13 +25,48 @@ from pathlib import Path import kicad_detect import proc +# A CA bundle shipped WITH the bridge (certifi's cacert.pem). This is the durable+# trust source: AD's provisioned portable Python (Runtime contract v1.9.63+) has+# no usable system CA store, so create_default_context() verifies against nothing+# and the Windows cert enumeration proved unreliable on it — every https fetch+# died with CERTIFICATE_VERIFY_FAILED. Bundling the roots guarantees working TLS+# regardless of the interpreter. Verification stays ON (no MITM window).+_BUNDLED_CACERT = os.path.join(+    os.path.dirname(os.path.abspath(__file__)), "..", "certs", "cacert.pem"+)+++def _ssl_context() -> ssl.SSLContext:+    """A verifying SSL context that works even on AD's certless portable Python."""+    ctx = ssl.create_default_context()+    # Primary: the bundled certifi roots (works on any interpreter).+    try:+        if os.path.exists(_BUNDLED_CACERT):+            ctx.load_verify_locations(cafile=_BUNDLED_CACERT)+    except Exception:+        pass+    # Secondary (belt-and-suspenders): the Windows system ROOT/CA store, if usable.+    if sys.platform == "win32":+        try:+            pem = "".join(+                ssl.DER_cert_to_PEM_cert(cert_der)+                for store in ("ROOT", "CA")+                for cert_der, enc, _trust in ssl.enum_certificates(store)+                if enc == "x509_asn"+            )+            if pem:+                ctx.load_verify_locations(cadata=pem)+        except Exception:+            pass+    return ctx+ # Baked fallback if the GitLab tag query fails (network blip). Bump on new stable. _FALLBACK_LATEST = "10.0.4" _TAGS_URL = (     "https://gitlab.com/api/v4/projects/kicad%2Fcode%2Fkicad/repository/tags?per_page=30" ) _DL_URL = "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-{v}-x86_64.exe"-_UA = "adom-desktop-kicad-bridge"+_UA = "kicad-bridge"   def _is_stable(tag: str) -> bool:@@ -45,7 +82,7 @@ def latest_stable_version() -> tuple[str, str]:     """Return (version, source) where source is 'gitlab' or 'fallback'."""     try:         req = urllib.request.Request(_TAGS_URL, headers={"User-Agent": _UA})-        with urllib.request.urlopen(req, timeout=10) as r:+        with urllib.request.urlopen(req, timeout=10, context=_ssl_context()) as r:             tags = [t["name"] for t in json.loads(r.read().decode())]         stables = sorted(             (t for t in tags if _is_stable(t)),@@ -104,6 +141,85 @@ def handle_check_for_updates(kicad_info: dict, args: dict) -> dict:     }  +def _elevation_facts() -> dict:+    """Hard, verifiable elevation/UAC facts for THIS process — no guessing.++    Settles "is the bridge actually elevated, and why didn't a UAC prompt appear":+    - tokenElevationType: 1=Default (no split token — UAC disabled OR built-in+      Administrator), 2=Full (elevated via UAC), 3=Limited (normal user token).+    - tokenElevated: the token's TokenElevation flag (the ground truth).+    - uacEnableLUA: HKLM ...\\Policies\\System EnableLUA — 0 means UAC is OFF+      machine-wide, so admin accounts get a full token with NO prompt ever.+    """+    facts: dict = {"platform": sys.platform}+    if sys.platform != "win32":+        return facts+    import ctypes+    import ctypes.wintypes as wt+    import getpass+    facts["user"] = getpass.getuser()+    try:+        facts["isUserAnAdmin"] = bool(ctypes.windll.shell32.IsUserAnAdmin())+    except Exception as e:+        facts["isUserAnAdmin"] = f"error: {e}"+    try:+        advapi32 = ctypes.WinDLL("advapi32", use_last_error=True)+        kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)+        TOKEN_QUERY = 0x0008+        token = wt.HANDLE()+        if not advapi32.OpenProcessToken(+            kernel32.GetCurrentProcess(), TOKEN_QUERY, ctypes.byref(token)+        ):+            raise OSError(f"OpenProcessToken failed: {ctypes.get_last_error()}")+        for name, cls in (("tokenElevationType", 18), ("tokenElevated", 20)):+            val = wt.DWORD()+            ret_len = wt.DWORD()+            ok = advapi32.GetTokenInformation(+                token, cls, ctypes.byref(val), ctypes.sizeof(val), ctypes.byref(ret_len)+            )+            facts[name] = int(val.value) if ok else f"error: {ctypes.get_last_error()}"+        kernel32.CloseHandle(token)+        facts["tokenElevationTypeMeaning"] = {+            1: "Default — no split token (UAC disabled machine-wide, or built-in Administrator): full admin, NO UAC prompt ever",+            2: "Full — elevated via an approved UAC prompt (or elevated parent)",+            3: "Limited — standard filtered token, NOT admin",+        }.get(facts.get("tokenElevationType"), "unknown")+    except Exception as e:+        facts["tokenError"] = str(e)+    try:+        import winreg+        with winreg.OpenKey(+            winreg.HKEY_LOCAL_MACHINE,+            r"SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System",+        ) as k:+            facts["uacEnableLUA"] = winreg.QueryValueEx(k, "EnableLUA")[0]+            # 0 = elevate silently WITHOUT a prompt (common on VMs) — explains+            # "I never clicked UAC" even when a process ends up elevated.+            try:+                facts["uacConsentPromptBehaviorAdmin"] = winreg.QueryValueEx(+                    k, "ConsentPromptBehaviorAdmin"+                )[0]+            except OSError:+                facts["uacConsentPromptBehaviorAdmin"] = "not set (default 5: prompt)"+    except Exception as e:+        facts["uacEnableLUA"] = f"error: {e}"+    return facts+++def _remote_installer_size(url: str) -> int | None:+    """The server's Content-Length for the installer (follows redirects). Lets us+    verify a downloaded/cached file isn't silently TRUNCATED — a dropped+    connection reads as EOF, logs 'download complete', passes the MZ+50MB sanity+    checks, and then the NSIS integrity check fails instantly at install time."""+    try:+        req = urllib.request.Request(url, method="HEAD", headers={"User-Agent": _UA})+        with urllib.request.urlopen(req, timeout=30, context=_ssl_context()) as r:+            cl = r.headers.get("Content-Length")+            return int(cl) if cl else None+    except Exception:+        return None++ def handle_upgrade(kicad_info: dict, args: dict) -> dict:     """Download the official KiCad installer and run it silently (/S)."""     if sys.platform != "win32":@@ -113,6 +229,33 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict:             "errorCode": "unsupported_platform",         } +    # diagnoseOnly: return the elevation/UAC ground truth + cached-installer state+    # WITHOUT downloading or installing anything. Used to settle "is this process+    # actually elevated / why did no UAC prompt appear / why rc=666660".+    if args.get("diagnoseOnly"):+        facts = _elevation_facts()+        t = args.get("version") or latest_stable_version()[0]+        cached = Path(tempfile.gettempdir()) / f"kicad-{t}-x86_64.exe"+        facts["cachedInstaller"] = str(cached) if cached.exists() else None+        facts["cachedInstallerBytes"] = cached.stat().st_size if cached.exists() else 0+        expected = _remote_installer_size(download_url(t))+        facts["serverInstallerBytes"] = expected+        facts["cachedInstallerTruncated"] = (+            bool(cached.exists() and expected and cached.stat().st_size != expected)+        )+        return {+            "success": True,+            "diagnoseOnly": True,+            **facts,+            "_hint": (+                "Read tokenElevationTypeMeaning + uacEnableLUA together: EnableLUA=0 means UAC is "+                "disabled machine-wide, so an admin account always runs with a full token and NO UAC "+                "prompt ever appears — IsUserAnAdmin=True without anyone clicking anything. "+                "tokenElevated=1 with an rc=666660 install failure means elevation is NOT the cause "+                "(look at SmartScreen/AV or the installer's silent flag instead)."+            ),+        }+     target = args.get("version") or latest_stable_version()[0]     current = kicad_info.get("version") if kicad_info else None     if (@@ -131,24 +274,86 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict:     url = download_url(target)     dest = Path(tempfile.gettempdir()) / f"kicad-{target}-x86_64.exe" -    # Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.-    try:-        req = urllib.request.Request(url, headers={"User-Agent": _UA})-        total = 0-        with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:-            while True:-                chunk = r.read(1 << 20)-                if not chunk:-                    break-                f.write(chunk)-                total += len(chunk)-    except (urllib.error.URLError, OSError) as e:-        return {-            "success": False,-            "error": f"download failed: {e}",-            "errorCode": "download_failed",-            "url": url,-        }+    # Reuse an already-downloaded installer. The KiCad installer is ~1 GB, and a+    # prior attempt that succeeded at DOWNLOAD but failed at INSTALL (e.g. UAC not+    # approved / AD not elevated) leaves a valid .exe in Temp. Skipping the+    # re-download makes a post-elevation retry near-instant.+    def _valid_installer(p: Path) -> bool:+        try:+            if not p.exists() or p.stat().st_size < 50 * 1024 * 1024:+                return False+            with open(p, "rb") as fh:+                return fh.read(2) == b"MZ"+        except OSError:+            return False++    # The server's true size — the anti-truncation ground truth. A dropped+    # connection reads as EOF ("download complete"), passes MZ+50MB checks, then+    # NSIS's integrity check fails instantly at install time (the rc=666660+    # failure mode). Compare every cached/downloaded file against this.+    expected_bytes = _remote_installer_size(url)+    print(f"[KiCad Bridge] kicad_upgrade: server reports installer size "+          f"{expected_bytes if expected_bytes else 'unknown'} bytes", flush=True)++    total = dest.stat().st_size if dest.exists() else 0+    cache_ok = (+        _valid_installer(dest)+        and not args.get("reDownload")+        and (expected_bytes is None or total == expected_bytes)+    )+    if _valid_installer(dest) and expected_bytes and total != expected_bytes:+        print(f"[KiCad Bridge] kicad_upgrade: cached installer is TRUNCATED "+              f"({total} of {expected_bytes} bytes) — discarding and re-downloading", flush=True)+    if cache_ok:+        print(f"[KiCad Bridge] kicad_upgrade: reusing cached installer {dest} "+              f"({total // (1024*1024)} MB, size verified) — pass reDownload:true to force a fresh fetch", flush=True)+    else:+        # Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.+        # Progress is printed so `bridge_log_read` reveals where a slow/stuck+        # install is. timeout is the per-read blocking timeout (300s for slow VMs).+        print(f"[KiCad Bridge] kicad_upgrade: downloading {url}", flush=True)+        try:+            req = urllib.request.Request(url, headers={"User-Agent": _UA})+            total = 0+            next_log = 25 * 1024 * 1024+            with urllib.request.urlopen(req, timeout=300, context=_ssl_context()) as r, open(dest, "wb") as f:+                if expected_bytes is None:+                    cl = r.headers.get("Content-Length")+                    expected_bytes = int(cl) if cl else None+                while True:+                    chunk = r.read(1 << 20)+                    if not chunk:+                        break+                    f.write(chunk)+                    total += len(chunk)+                    if total >= next_log:+                        print(f"[KiCad Bridge] kicad_upgrade: downloaded {total // (1024*1024)} MB...", flush=True)+                        next_log += 25 * 1024 * 1024+            print(f"[KiCad Bridge] kicad_upgrade: download finished ({total} of "+                  f"{expected_bytes if expected_bytes else 'unknown'} bytes) -> {dest}", flush=True)+        except (urllib.error.URLError, OSError) as e:+            print(f"[KiCad Bridge] kicad_upgrade: DOWNLOAD FAILED: {e}", flush=True)+            return {+                "success": False,+                "error": f"download failed: {e}",+                "errorCode": "download_failed",+                "url": url,+            }+        if expected_bytes and total != expected_bytes:+            print(f"[KiCad Bridge] kicad_upgrade: TRUNCATED download ({total} of {expected_bytes}) — deleting", flush=True)+            try:+                dest.unlink()+            except OSError:+                pass+            return {+                "success": False,+                "error": f"download truncated: got {total} of {expected_bytes} bytes "+                         f"(connection dropped mid-transfer). The file was deleted.",+                "errorCode": "download_truncated",+                "url": url,+                "_hint": "Re-run kicad_upgrade — it will re-download. If this repeats, the mirror "+                         "or the VM's link is flaky; try again later.",+            }      # Sanity: a real KiCad installer is a >50 MB Windows PE (MZ header).     if total < 50 * 1024 * 1024:@@ -170,14 +375,44 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict:      # Silent install (NSIS /S). Installing into Program Files needs elevation, so     # the user may get a UAC prompt to approve. Block until the installer exits.+    # If AD isn't elevated, this is where it STALLS (a UAC prompt no one approves)+    # until the timeout — the log line below is the last thing you'll see.+    # Is THIS process (hence AD, its parent) elevated? Definitive answer to+    # "did the elevated relaunch actually take" — the KiCad installer needs admin,+    # so if we're not elevated it exits fast with a failure code (no UAC prompt to+    # click) and nothing installs.+    def _is_admin():+        if sys.platform != "win32":+            return None+        try:+            import ctypes+            return bool(ctypes.windll.shell32.IsUserAnAdmin())+        except Exception:+            return None+    elevated = _is_admin()+    # KiCad 10's installer is built on NsisMultiUser: in SILENT mode it REQUIRES an+    # explicit install scope — bare "/S" is "invalid command-line parameters" and+    # exits instantly with rc=666660 (NsisMultiUser's documented error level; its+    # sibling 666661 = elevation restricted). THE root cause of the fast-fail.+    #   /allusers  /S  → per-machine (Program Files; needs elevation)+    #   /currentuser /S → per-user (%LOCALAPPDATA%\Programs\KiCad; NO admin, NO UAC)+    # scope arg: "machine" | "user" | default auto (machine when elevated, else the+    # zero-UAC per-user install — kicad_detect scans both locations).+    scope = args.get("scope") or ("machine" if elevated else "user")+    scope_flag = "/allusers" if scope == "machine" else "/currentuser"+    print(f"[KiCad Bridge] kicad_upgrade: elevated={elevated}; launching installer "+          f"{scope_flag} /S (scope={scope})...", flush=True)     try:-        result = proc.run([str(dest), "/S"], timeout=900)+        result = proc.run([str(dest), scope_flag, "/S"], timeout=900)         rc = result.returncode+        print(f"[KiCad Bridge] kicad_upgrade: installer exited rc={rc}", flush=True)     except Exception as e:+        print(f"[KiCad Bridge] kicad_upgrade: INSTALLER LAUNCH/RUN FAILED: {e}", flush=True)         return {             "success": False,             "error": f"installer launch failed: {e}",             "errorCode": "install_failed",+            "elevated": elevated,             "path": str(dest),             "_hint": "If a UAC prompt appeared, approve it; or run the downloaded installer manually.",         }@@ -195,15 +430,22 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict:         "targetVersion": target,         "previousVersion": current,         "installerExit": rc,+        "elevated": elevated,+        "installScope": scope,         "installedVersions": installed,         "downloadUrl": url,         "_hint": (-            f"KiCad {target} installed."+            f"KiCad {target} installed ({scope} scope)."             if now_has             else (-                f"Installer ran (exit {rc}) but {target} wasn't detected — it may have needed a "-                f"UAC approval, or the bridge needs a restart to re-scan. Installed: "-                f"{', '.join(installed) or 'none'}."+                f"Installer ran (exit {rc}) but {target} wasn't detected. elevated={elevated}, scope={scope}. "+                + ("rc=666660 is NsisMultiUser 'invalid command-line parameters' and 666661 is 'elevation "+                   "restricted' — retry with the other scope: "+                   '{"scope":"user"} needs NO admin/UAC (installs to %LOCALAPPDATA%\\Programs\\KiCad); '+                   '{"scope":"machine"} needs elevation. '+                   if rc in (666660, 666661) else+                   "Check SmartScreen/AV blocking the .exe or a pending reboot. ")+                + f"Installed: {', '.join(installed) or 'none'}."             )         ),     }
proc.py+1−1
@@ -1,6 +1,6 @@ """Safe subprocess wrapper — defensive against host spawn environments. -When a supervisor (Adom Desktop) spawns this bridge, the bridge's own child+When a supervisor (Adom Bridge) spawns this bridge, the bridge's own child processes must NOT inherit a broken/null console handle or an open stdin, or calls like `subprocess.run(... capture_output=True)` can deadlock during startup (observed 2026-06-25: KiCad detection hung forever under AD's spawn, but ran fine
server.py+4−4
@@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Adom KiCad Bridge Server — localhost HTTP server for KiCad integration. -Receives commands from the Adom Desktop (Tauri app) and controls+Receives commands from the Adom Bridge (Tauri app) and controls KiCad via kicad-cli, direct executable launch, and library table manipulation.  Usage:@@ -59,7 +59,7 @@ from handlers.export import ( # v0.9.61+: one-shot desktop half of the Adom molecule pipeline (STEP + silk # renders + sibling sch/pro discovery + container next-steps manifest). from handlers.export_molecule import handle_export_molecule-# v0.9.63+: first-class showcase tour (AD demos this bridge during HD install).+# v0.9.63+: first-class showcase tour (AD demos this bridge during Hydrogen install). from handlers.demo import handle_demo # v0.9.10+: check/upgrade KiCad via the OFFICIAL downloads.kicad.org installer # (NOT winget, which lags the real release). check_for_updates is read-only;@@ -490,7 +490,7 @@ COMMAND_HANDLERS = {     "export_bom_csv": handle_export_bom_csv,     # v0.9.10+: KiCad version check + upgrade via the official downloads.kicad.org     # installer (read-only check; silent /S install). Exposed as kicad_check_for_updates-    # / kicad_upgrade through Adom Desktop's `kicad_` verb prefix.+    # / kicad_upgrade through Adom Bridge's `kicad_` verb prefix.     "check_for_updates": handle_check_for_updates,     "upgrade": handle_upgrade, }@@ -873,7 +873,7 @@ class KiCadBridgeHandler(BaseHTTPRequestHandler): def main():     global kicad_info, all_kicad_versions -    # Line-buffer stdout/stderr. When a supervisor (Adom Desktop) redirects our+    # Line-buffer stdout/stderr. When a supervisor (Adom Bridge) redirects our     # output to a log FILE, Python block-buffers it by default — so a startup     # hang shows a 0-byte log and is undebuggable. Line buffering makes every     # startup step land in ~/.adom/bridge-logs/<name>.log in real time, which is
skills/kicad-bridge-dev/SKILL.md+4−4
@@ -1,12 +1,12 @@ --- name: kicad-bridge-dev user-invocable: false-description: DEVELOPER-only skill (user-invocable:false — installed for maintainers, never offered to everyday users). Building/debugging the Adom Desktop KiCad bridge: architecture, the 4 control surfaces, how to add a verb, and every fresh-machine gotcha this bridge has hit. Trigger words — kicad bridge dev, edit kicad bridge, add kicad verb, kicad bridge architecture, debug kicad bridge, kicad control surfaces, kicad fresh machine.+description: DEVELOPER-only skill (user-invocable:false — installed for maintainers, never offered to everyday users). Building/debugging the Adom Bridge KiCad bridge: architecture, the 4 control surfaces, how to add a verb, and every fresh-machine gotcha this bridge has hit. Trigger words — kicad bridge dev, edit kicad bridge, add kicad verb, kicad bridge architecture, debug kicad bridge, kicad control surfaces, kicad fresh machine. ---  # kicad-bridge-dev -Developer notes for the `adom-desktop-kicad-bridge`. Source-only (belongs in the repo, never shipped to a user install). Read `adom-desktop-bridge-sdk` first for the AD-core-vs-author boundary.+Developer notes for the `kicad-bridge`. Source-only (belongs in the repo, never shipped to a user install). Read `adom-desktop-bridge-sdk` first for the AD-core-vs-author boundary.  ## What it is @@ -142,7 +142,7 @@ subprocess (like kicad-cli) — NOT AD's gated `run_script`.  ### ⚖️ Every file you add to this repo may ship to every AD user -The release zip is **auto-bundled into Adom Desktop**, so repo weight is user-facing weight. Before+The release zip is **auto-bundled into Adom Bridge**, so repo weight is user-facing weight. Before adding a file, know which artifact it belongs to:  | Adding… | Goes in | Ships in the zip? |@@ -187,7 +187,7 @@ threads (withdrawn in the 1.9.183 notice); the per-call mechanism is  ## 🎬 The demo verb (`kicad_demo`) — match the ecosystem contract -HD's installer demos BOTH bridges, so `kicad_demo` deliberately returns the SAME+Hydrogen's installer demos BOTH bridges, so `kicad_demo` deliberately returns the SAME shape as `fusion_demo`. Do not invent fields: an AI driving both should not have to special-case them. 
skills/kicad-bridge-publish/SKILL.md+11−11
@@ -1,12 +1,12 @@ --- name: kicad-bridge-publish user-invocable: false-description: DEVELOPER-only PUBLISH skill (user-invocable:false — for maintainers, never offered to everyday users). The exact recipe to ship a new version of the Adom Desktop KiCad bridge to wiki.adom.inc: version lockstep, the zip build, adom-wiki repo push + release create, the skills pkg, pushing to a target AD, and verify steps. Trigger words — publish kicad bridge, release kicad bridge, ship kicad bridge, bump kicad bridge, kicad bridge manifest, kicad bridge pkg.+description: DEVELOPER-only PUBLISH skill (user-invocable:false — for maintainers, never offered to everyday users). The exact recipe to ship a new version of the Adom Bridge KiCad bridge to wiki.adom.inc: version lockstep, the zip build, adom-wiki repo push + release create, the skills pkg, pushing to a target AD, and verify steps. Trigger words — publish kicad bridge, release kicad bridge, ship kicad bridge, bump kicad bridge, kicad bridge manifest, kicad bridge pkg. ---  # kicad-bridge-publish -How to cut and publish a KiCad-bridge release. **Use the `adom-wiki` CLI for ALL wiki interactions — never raw curl.** Two independent artifacts: the RUNTIME (zip + manifest, on the page `/files`) and the SKILLS PKG (`adom/adom-desktop-kicad-bridge`, container-side docs).+How to cut and publish a KiCad-bridge release. **Use the `adom-wiki` CLI for ALL wiki interactions — never raw curl.** Two independent artifacts: the RUNTIME (zip + manifest, on the page `/files`) and the SKILLS PKG (`adom/kicad-bridge`, container-side docs).  ## 1. Version lockstep (bump ALL three together) @@ -18,7 +18,7 @@ They must match. `bridge.json` also carries `updateManifestUrl` (durable auto-up  ## 2. Build the zip — KEEP IT TINY (it is auto-bundled into AD) -The runtime zip ships inside Adom Desktop for **every** user, so it carries ONLY what the+The runtime zip ships inside Adom Bridge for **every** user, so it carries ONLY what the bridge needs to RUN. No docs, no images, no skills, no samples. (Audited 2026-07-15: the zip had grown to 488.9 KB carrying 219 KB of hero *fonts*, five `skills/` trees, demo scripts and READMEs — none referenced by runtime code. Slimming it to 335.7 KB was a 31% cut with zero@@ -49,10 +49,10 @@ Manifest fields: `manifest_version:1, name:"kicad", version, url` (the `/blob/ap ## 3. Publish the runtime (CLI)  ```bash-adom-wiki repo push adom/adom-desktop-kicad-bridge \+adom-wiki repo push adom/kicad-bridge \   --files adom-bridge-kicad-v<ver>.zip adom-bridge-kicad-manifest.json bridge.json \   -m "v<ver>: <what changed>"-adom-wiki release create adom/adom-desktop-kicad-bridge <ver> \+adom-wiki release create adom/kicad-bridge <ver> \   --title "KiCad bridge v<ver>" --changelog "..." \   --binary-path adom-bridge-kicad-v<ver>.zip ```@@ -74,14 +74,14 @@ description."* Cut to 145 chars.) The full explanation belongs in the README bod Canonical description — copy this shape:  > Skills for your container so your AI knows how to drive the KiCad bridge.-> The bridge runtime itself is the release zip — Adom Desktop loads that.+> The bridge runtime itself is the release zip — Adom Bridge loads that.  The longer version (the "Two artifacts on this page — which one do you want?" comparison table: what it is / who loads it / contains / do you install it manually, closing with **"The package is not the bridge."**) lives near the TOP of `README.md`, where there's room for it.  **No images in the pkg.** The README references page art by ABSOLUTE-`https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/<name>.png` URLs, so the PNGs render on+`https://wiki.adom.inc/blob/app/kicad-bridge/<name>.png` URLs, so the PNGs render on the page without riding in the install tarball. (Audited 2026-07-15: `files[]` listed `hero.png` + `arch.png` + `verbs.png` + `outcomes.png` = **496 KB** downloaded by every `pkg install`, referenced by nothing. Removing them took the tarball to 29.5 KB.) Push page art@@ -104,7 +104,7 @@ The runtime SKILL.md (`adom-desktop-kicad`, ships in the zip) and the pkg's root adom-desktop --target <name> refresh_bridges '{"name":"kicad"}'   # pulls new cache adom-desktop --target <name> bridge_kill '{"name":"kicad"}'       # force fresh spawn (old instance keeps old code) adom-desktop --target <name> bridge_check_updates '{}'            # current == latest?-adom-wiki repo show adom/adom-desktop-kicad-bridge adom-bridge-kicad-manifest.json  # served version matches?+adom-wiki repo show adom/kicad-bridge adom-bridge-kicad-manifest.json  # served version matches? ```  Verify the served bridge.json (`repo show ... bridge.json`) has the fields you expect and the zip's sha matches. A fresh verb call after a kill returns `bridge_starting` — retry.@@ -112,7 +112,7 @@ Verify the served bridge.json (`repo show ... bridge.json`) has the fields you e ## 6. Refreshing the README screenshots (window tour)  The README embeds screenshots hosted on the page `/files` (referenced as-`https://wiki.adom.inc/blob/app/adom-desktop-kicad-bridge/<name>.png`). Regenerate them on a **real+`https://wiki.adom.inc/blob/app/kicad-bridge/<name>.png`). Regenerate them on a **real VM** (e.g. ADOMBASELINE) whenever the UI/flow changes — never hand-mock them:  1. On the VM: `kicad_enable_software_opengl` (if GPU-less) → open each window with sample data from@@ -122,7 +122,7 @@ VM** (e.g. ADOMBASELINE) whenever the UI/flow changes — never hand-mock them: 3. Save into `docs/img/` with the canonical names the README uses: `schematic-editor.png`,    `symbol-editor.png`, `footprint-editor.png`, `pcb-editor-2d.png`, `3d-viewer.png`,    `dialog-firstrun-wizard.png`, `control-surfaces.svg`.-4. `adom-wiki repo push adom/adom-desktop-kicad-bridge --files docs/img/*.png ...` then+4. `adom-wiki repo push adom/kicad-bridge --files docs/img/*.png ...` then    `curl -so /dev/null -w '%{http_code}'` each blob URL to confirm 200. 5. `shotlog paste <png> -c kicad-bridge -d "<caption>"` to show John the live captures. @@ -147,7 +147,7 @@ times because the window was open, correct and maximized — but behind everythi  ## 8. Upstream: what the SDK still owes every bridge -Two issues are open on `adom/adom-desktop-bridges` (filed 2026-07-19) — if you hit these problems,+Two issues are open on `adom/adom-bridge-sdk` (filed 2026-07-19) — if you hit these problems, they're known, and the fix belongs in the SDK, not just here:  1. **SDK should require every bridge to explain pkg-vs-zip** (one line, on the page + in
skills/kicad-tour/SKILL.md+1−1
@@ -38,7 +38,7 @@ The packs are mounted by the bridge at:  - `C:/Github/adom-desktop/plugins/kicad/<PACK>/` - (When AD is installer-deployed, look under-  `%LOCALAPPDATA%/Adom Desktop/bridges-cache/kicad/<PACK>/` — adjust paths.)+  `%LOCALAPPDATA%/Adom Bridge/bridges-cache/kicad/<PACK>/` — adjust paths.)  ### Step 1 — install the custom symbol + footprint libraries