app
Altium - the Altium Bridge
Public Made by Adomby adom
Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.
← Commit history
Publish 0.2.21
14 files changed
+336−22
README.md+67−8SKILL.md+19−4dashboard/altium_dashboard.py+1−1install.sh+5page.json+2−1skills/altium-a365/SKILL.md+87skills/altium-boards/SKILL.md+59skills/altium-bridge-plan/SKILL.md+6−6skills/altium-instances/SKILL.md+8skills/altium-libraries/SKILL.md+45skills/altium-licensing/SKILL.md+17−1skills/altium-part-import/SKILL.md+12skills/altium-uia/SKILL.md+7uninstall.sh+1−1README.md+67−8@@ -20,7 +20,7 @@ screenshot any window back to the AI. Altium Designer is the user's **host app**. The bridge *detects* an existing install (`%ProgramFiles%\Altium\AD*\X2.EXE`), launches it in the background, bootstraps a **resident-DelphiScript agent** inside it (the REPL lane), and hands the AI 60 `altium_*` verbs.+DelphiScript agent** inside it (the REPL lane), and hands the AI 120 `altium_*` verbs. - **Page / docs:** https://wiki.adom.inc/adom/altium-bridge - **Bridge SDK:** https://wiki.adom.inc/adom/adom-bridge-sdk@@ -56,7 +56,17 @@ All 16 named pins (GND/TXD/RXD/V3/UD+/UD-/XI/XO · VCC/R232/RTS#/DTR#/DCD#/RI#/D ### Schematic — `altium_place_on_sch` The symbol placed as U1 on a fresh sheet (per-primitive copy — the engine's group-`Replicate` drops children; the bridge handles that for you).+`Replicate` drops children; the bridge handles that for you). `symbolName` takes a `;` list+(from 0.2.13): every part lands on the same new sheet as U1..Un.++Sheet sizing (from 0.2.16 / 0.2.17): the sheet is sized from the parts, not the parts from the+sheet. The agent first measures every symbol's bounding box (pins included, plus 250 mil a side+for the pin numbers), then makes a custom sheet that holds the row with 500 mil between parts+and 600 mil margins, never smaller than A4 landscape (11500 x 7600 mil). Any slack is spread+evenly across the gaps. Each designator sits centred above its body and the Comment (the part+number) centred below it. `title` / `number` / `revision` / `drawnBy` fill the title block and+`description` becomes a text frame inside the drawing frame along the top; the reply's `_hints`+name whichever of those were left empty.  @@ -73,6 +83,13 @@ The LAN7800 **-HV-SB-RR** variant after two live footprint swaps on a wired 15× rounded pads, sixteen through-hole heatsink vias in the EP (all designator 49 → the EP net), the silk pin-1 dot, and the PIN1 / GND_EP stub tracks that survived both swaps untouched. +`altium_test_board` takes a `;` list in `footprintName` (from 0.2.13): the parts are spread+evenly along X on one new board. From 0.2.18 / 0.2.19 the parts are designated U1..Un (so+`component_pins` and `footprint_primitives` can address each one) and each part's name is+written on the Top Overlay under its footprint: 35 mil stroke text, centred on the part from+the text's measured width, its top 2.5 mm below the part centre so it clears the courtyard of+a 3 mm part.+  ### 3D board — the STEP body on a real PCB@@ -109,13 +126,23 @@ A halted engine (DelphiScript runtime errors are modal) recovers with `altium_ag **Board & schematic introspection (0.1.11):** `list_designators`, `component_data`, `component_pins`, `list_nets`, `net_connections`, `layers`, `copper_layers`, `read_rules`, `set_layer_visibility`, `footprint_primitives`, `schematic_data` — read the live design as data (pads→nets, rules, layers, sheet components with pins), all in tens of milliseconds, no dialogs. -All 94 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.+All 120 verbs (prefix `altium_`). Call `altium_describe` for the live catalog. **Readiness & lifecycle** — `readiness`, `status`, `launch`, `close`, `diagnostics`, `progress`,-`agent_start/_stop/_status/_recover`, `license`, `signin_status`, `connect_workspace`, `uninstall`+`agent_start/_stop/_status/_recover`, `license`, `signin_status`, `connect_workspace`, `uninstall`.+`agent_status` reports the resident loop (`alive`, `responsive`, `residentAnywhere`,+`liveWorkspaces[]`, heartbeat). `license_status` (licence state in plain words plus a frame of+the License Management page), `ensure_licensed` (launch if needed, claim a seat, or name the+human step; `launchWaitSec`, `allowSignInReset`, `allowTrial`) and `setup_trial` (returns the+one next onboarding action) are covered in the `altium-licensing` skill. **Surfaces** — `show_project`, `show_schematic`, `show_2d_board`, `show_3d_board`, `show_library`,-`show_symbol`, `show_footprint`, `show_3d_chip`, `open_document`, `state`, `demo`+`show_symbol`, `show_footprint`, `show_3d_chip`, `open_document`, `close_document` (`path`; closes+one open document through its handle, never saves), `state`, `demo`, `animate_view` (camera/zoom+choreography run in-bridge: `preset` punch_in | showcase3d | orbit | flip | finale and/or+`steps[]`, `lane` pcb | sch, `pauseMs`), `foreground` (raise Altium for a user who asked to+watch; `reason` of 10+ characters is required; the reply says whether Altium is `visiblyOnTop`).+`show_schematic {filePath}` opens a .SchDoc in the background, zooms to fit and returns the frame. **Engine** — `eval`, `run_command`, `list_commands`, `run_script` @@ -126,7 +153,25 @@ and centred on the pads), `set_params`, `build_footprint` (a whole footprint fro `tools/kicad2altium.py` spec: via pads by duplicate designator, rounded-rect pads, named mechanical layers), `build_symbol_modes` (one symbol, N display modes) + `symbol_mode` (switch + frame), `add_models` (N footprint variants on one part — the CHANGE PACKAGE equivalent),-`install_library`, `compile_intlib`, `extract_intlib`, `list_library`, `save`+`install_library`, `compile_intlib`, `extract_intlib`, `list_library`, `save`,+`install_library_bundle` + `install_receipt` (the last bundle install's full receipt, for a+client whose call outlived ab's budget), `set_models` (from 0.2.15: replace every footprint+model on a symbol with one PCBLIB model; `set_params` footprint and `add_models` append)++**Chip outline on the symbol (from 0.2.15)** — `add_symbol_art {libraryPath, symbolName, kicadSym,+clear?, mode?}` reads the chip-outline polylines adom-symbol baked into the KiCad symbol, maps+the KiCad body rectangle onto the SchLib component's rectangle and draws them, so the Altium+symbol shows the same chip as the KiCad and Fusion ones. `kicadSym` is a Windows path on the+Altium box (stage it with `send_files`). `clear` (default true) removes the component's+existing lines and polylines first, so a re-run replaces instead of stacking; `mode` is `lines`+(default, eLine segments) or `polyline`. `symbol_prims {libraryPath, symbolName, models?}` is+the read-only check: primitive counts by kind, `bodyRectMil`, `polyBboxMil`, and the footprint+models with `models:true` (off by default; the model walk halts the engine on some builds).++**Heatsink vias (from 0.2.20)** — `plate_vias {libraryPath, footprintName?}` turns every drilled+pad in a PcbLib that loaded on the Top Layer into a plated Multi-Layer pad. Run it once after+installing a library whose thermal-pad vias were written as pads with a hole (adom-lbr before+2.46.1), then `altium_save` the PcbLib. Replies `holes`, `plated`, `fixed`. **Manipulate libraries** — `clone_symbol` (all display modes carried), `rename_symbol`, `remove_symbol`, `read_symbol` (pins/params/modes read-back), `copy_footprint` (pads + bodies +@@ -136,7 +181,21 @@ drawing), `rename_footprint`, `remove_footprint`, `xcopy_footprint` (cross-libra sized to the part, nets + stub tracks), `board_shape`, `swap_footprint` (change the package on a placed part; wiring survives), `pcb_report` (ground truth: pads, bodies, nets, tracks with bboxes) -**Dialogs & desktop** — `check_dialogs`, `dismiss_dialogs`+**Whole-design reads & review** — `read_board` (one agent round trip: components, nets, layers,+copper, rules, occupancy, outline; `sections`, `limit`, `raw`), with `board_summary`,+`rules_by_kind` and `layers_in_use` shaped from the same read; `read_schematic` (components of+the current sheet; `sections`, `params`, `limit`); `cross_check {board?, sheet?}` (designators+on the sheet but not the board and the reverse); `review_board {board?, sheet?,+includeSchematic?, checks?}` (14 design-review checks, partial results by design: `skipped`+and `notImplemented` say what did not run)++**Dialogs & desktop** — `check_dialogs`, `dismiss_dialogs`, `dialog_log` (what the standing+dialog reaper dismissed; `limit`, default 25)++**Altium 365** — `a365_configure`, `a365_status`, `a365_query`, `a365_oauth_begin` / `_status` /+`_cancel`, `a365_refresh`, `a365_logout`, `a365_files_upload`, `a365_library_overview`,+`a365_library_schema`. Works with Altium Designer closed; tokens stay in the bridge cache and+never appear in a reply. See the `altium-a365` skill. ## The dock bar card @@ -183,7 +242,7 @@ proof frame — the library as Altium actually opened it:  `docs/webcontrol-altium-test.html` is the acceptance harness (it loads the wiki's live-`webcontrol.js`); `docs/HANDOFF-COLBY.md` is the contract for the page side.+`webcontrol.js`). ## The skillpack — ten skills, propagated like KiCad's and Fusion's
SKILL.md+19−4@@ -36,7 +36,7 @@ Altium is a licensed host app. The bridge never installs it silently; it detects 3. **`ok:true` is not enough — verify.** `show_*` and `state` return frames; look at them. `run_outjob` returns `files[]` actually written. 4. **One script at a time.** While the agent is resident, never run `altium_run_script`; it raises Altium's "Another script executing now" modal. Send work through `altium_eval` / `altium_run_command`. 5. **Never leave a modal.** Prefer commands that carry their parameters (`altium_run_command` with `params`) so no picker opens. After any mutating step, `altium_check_dialogs`.-6. **Reuse the running Altium.** Altium is single-instance; the agent survives across your calls (auto-stops after 30 min idle, auto-restarts on the next verb).+6. **Reuse the running Altium.** Altium is single-instance; the agent survives across your calls (auto-stops after 5 min idle from 0.2.9, auto-restarts on the next verb). 7. **Absolute Windows paths.** `C:/…` or `C:\\…`; no `%VAR%` expansion. Files from the container must be sent first (`send_files`). 8. **Surface `_hint` verbatim + branch on `errorCode`.** @@ -64,17 +64,32 @@ Altium is a licensed host app. The bridge never installs it silently; it detects | `altium_install_library` / `altium_compile_intlib` / `altium_extract_intlib` | Installed Libraries, .LibPkg → .IntLib, .IntLib → sources | `libraryPath` / `libpkgPath` / `intlibPath` | | `altium_build_footprint` / `altium_build_symbol_modes` | author a footprint from a `tools/kicad2altium.py` spec (via pads, roundrect, mech layers) / ONE symbol with N display modes | `libraryPath`, `spec` | | `altium_add_models` / `altium_symbol_mode` | N footprint models + params on a symbol / switch display mode + frame | `symbolName`, `models[]`, `params` / `mode` |-| `altium_test_board` / `altium_board_shape` / `altium_swap_footprint` | wired test board, rectangular board shape, footprint swap keeping nets | `footprintName`, `wMm`,`hMm`, `assigns` |+| `altium_add_symbol_art` / `altium_symbol_prims` (from 0.2.15) | draw the chip outline adom-symbol baked into the KiCad symbol onto a SchLib component (replaces earlier art unless `clear:false`) / read-only primitive counts, body rectangle, polyline bbox, models | `libraryPath`, `symbolName`, `kicadSym`, `clear`, `mode:"lines"\|"polyline"` / `models:true` |+| `altium_set_models` (from 0.2.15) | replace every footprint model on a symbol with ONE PCBLIB model (`set_params` footprint and `add_models` append) | `libraryPath`, `symbolName`, `footprint` |+| `altium_plate_vias` (from 0.2.20) | every drilled pad in a PcbLib that loaded on the Top Layer becomes a plated Multi-Layer pad (heatsink vias); `altium_save` afterwards | `libraryPath`, `footprintName` (optional filter) |+| `altium_place_on_sch` | one symbol or a `;` list on ONE new sheet as U1..Un; the sheet is sized from the measured parts (500 mil gaps, 600 mil margins, A4 minimum), designator above and part number below each body, title block filled | `libraryPath`, `symbolName`, `schPath`, `title`, `number`, `revision`, `drawnBy`, `description` |+| `altium_test_board` / `altium_board_shape` / `altium_swap_footprint` | wired test board (one footprint or a `;` list, parts U1..Un with each part's name on the Top Overlay under its footprint), rectangular board shape, footprint swap keeping nets | `footprintName`, `wMm`,`hMm`, `pcbPath`, `assigns`, `stubs` | | `altium_clone_symbol` / `altium_rename_symbol` / `altium_remove_symbol` / `altium_read_symbol` | library manipulation on a SchLib (clone carries every display mode) | `from`,`to` / `symbolName` | | `altium_copy_footprint` / `altium_rename_footprint` / `altium_remove_footprint` | the same for a PcbLib (copy carries pads, bodies, drawing) | `from`,`to` / `footprintName` | | `altium_pcb_report` / `altium_normalize_bodies` | ground truth (pads/bodies/nets/tracks with bboxes) / centre bodies on pads | `want:"board"`, `footprintName` | | `altium_save` | save the focused document (pre-clears a dangling transaction) | `filePath` |+| `altium_close_document` | close one open document through its handle; never saves | `path` |+| `altium_read_board` / `altium_board_summary` / `altium_rules_by_kind` / `altium_layers_in_use` | ONE agent round trip for the whole board (components, nets, layers, copper, rules, occupancy, outline) and three views shaped from it | `sections`, `limit`, `raw:true` for the unshaped payload |+| `altium_read_schematic` | components on the CURRENT sheet with their parameters | `sections`, `params:false`, `limit` |+| `altium_cross_check` | designators on the sheet but not the board and the reverse (`matched`, `onSheetNotOnBoard`, `onBoardNotOnSheet`, `sameProject`) | `board`, `sheet` (paths, optional; opened in turn) |+| `altium_review_board` | 14 design-review checks over one board read, schematic-side checks with `includeSchematic`; partial by design (`skipped`, `notImplemented`) | `board`, `sheet`, `includeSchematic`, `checks[]` |+| `altium_license_status` / `altium_ensure_licensed` / `altium_setup_trial` | licence state in words + page frame / launch, claim a seat or name the human step / the one next onboarding action (see `altium-licensing`) | — / `launchWaitSec`, `allowSignInReset`, `allowTrial` / — | | `altium_signin_status` / `altium_license` / `altium_connect_workspace` | account + licence + workspace state; drive the License Management page (`action: show\|refresh\|use_license\|start_trial`) | — |-| `altium_a365_configure` / `_status` / `_query` | Altium 365 GraphQL (needs a token from a workspace that has Admin → Developer; the Develop trial does not) | `workspace`, `token`, `query` |+| `altium_a365_configure` / `_status` / `_query` | Altium 365 GraphQL (needs a token from a workspace that has Admin → Developer; the Develop trial does not) | `workspace`, `token`, `query`, `variables` |+| `altium_a365_oauth_begin` / `_oauth_status` / `_oauth_cancel` / `_refresh` / `_logout` | OAuth (PKCE) sign-in with an Altium-issued public client ID; tokens stay in the bridge cache and never appear in a reply | `clientId`, `workspace`, `selectWorkspace`, `timeout` |+| `altium_a365_library_overview` / `_library_schema` / `_files_upload` | workspace library summary / library-related GraphQL queries and mutations / upload a file to the Files Service and get a temporary `fileId` (see `altium-a365`) | — / `timeout` / `filePath`, `maxBytes` |+| `altium_animate_view` / `altium_foreground` | camera and zoom choreography run in-bridge / raise Altium for a user who asked to watch (replies `visiblyOnTop`) | `preset`, `steps[]`, `lane`, `pauseMs` / `reason` (10+ chars, required) |+| `altium_dialog_log` | what the standing dialog reaper dismissed | `limit` (default 25) |+| `altium_install_receipt` | the last `install_library_bundle` receipt (proof frame included) after a call outlived ab's budget | — | | `altium_demo` | six-beat tour: symbol → footprint → 3D part → schematic → 2D → 3D board | `stage` | | `altium_describe` | the full machine-readable catalog | — | -Lifecycle: `altium_launch`, `altium_close`, `altium_agent_start` / `_stop` / `_status` / `_recover`, `altium_diagnostics`, `altium_progress`, `altium_uninstall`.+Lifecycle: `altium_launch`, `altium_close`, `altium_agent_start` / `_stop` / `_status` / `_recover`, `altium_diagnostics`, `altium_progress`, `altium_uninstall`. `altium_agent_status` is read-only: `alive`, `responsive`, `residentAnywhere`, `liveWorkspaces[]` (a loop resident in another workspace is a freeze-on-close hazard; `altium_close` stops every workspace's loop). ## Quick examples
dashboard/altium_dashboard.py+1−1@@ -273,7 +273,7 @@ def say_send(phrase): d = res.get("data") if isinstance(res.get("data"), dict) else res # VERIFY BY DELIVERY, never by the thread's name (issue #59: a correctly # named tab that delivered nothing, and an unnamed tab that delivered -- # measured live on AdomLapper 2026-08-25, where a successful send came back+ # measured live 2026-08-25, where a successful send came back # with "title": null). # # BUT delivered_to ALONE PRODUCES FALSE FAILURES. The API's own hint calls
install.sh+5@@ -15,6 +15,11 @@ for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do install_skill "$HOME_DIR/altium-libraries" "$HERE/skills/altium-libraries/SKILL.md" install_skill "$HOME_DIR/altium-boards" "$HERE/skills/altium-boards/SKILL.md" install_skill "$HOME_DIR/altium-outputs" "$HERE/skills/altium-outputs/SKILL.md"+ install_skill "$HOME_DIR/altium-instances" "$HERE/skills/altium-instances/SKILL.md"+ install_skill "$HOME_DIR/altium-licensing" "$HERE/skills/altium-licensing/SKILL.md"+ install_skill "$HOME_DIR/altium-uia" "$HERE/skills/altium-uia/SKILL.md"+ install_skill "$HOME_DIR/altium-wiki-import" "$HERE/skills/altium-wiki-import/SKILL.md"+ install_skill "$HOME_DIR/altium-a365" "$HERE/skills/altium-a365/SKILL.md" done # Dashboard: the pkg ships it, the dock runs `altium-dashboard serve --print-url`.
page.json+2−1@@ -4,7 +4,7 @@ "type": "app", "title": "Altium - the Altium Bridge", "brief": "Adom Bridge for Altium Designer: a resident DelphiScript agent gives the AI eval, the 1,100-command process launcher, all 8 EDA surfaces with screenshots, compile/ERC, DRC, OutJob outputs, native SchLib/PcbLib authoring with STEP 3D bodies, and dialog handling by PID.",- "version": "0.2.20",+ "version": "0.2.21", "hero": { "type": "image", "path": "screenshots/hero.png"@@ -126,6 +126,7 @@ "dockbar.json", "install.sh", "package.json",+ "skills/altium-a365/SKILL.md", "skills/altium-boards/SKILL.md", "skills/altium-bridge-plan/SKILL.md", "skills/altium-install/SKILL.md",
skills/altium-a365/SKILL.mdadded+87@@ -0,0 +1,87 @@+---+name: altium-a365+description: >-+ The Altium 365 cloud lane of the altium-bridge: the eleven `altium_a365_*` verbs. Store a+ workspace and a Bearer token, or sign in with OAuth (PKCE) using an Altium-issued public+ client ID; run GraphQL against the workspace; refresh and revoke tokens; read the workspace+ library overview and the library-related schema; upload a file to the Files Service. None of+ it needs Altium Designer running. Use when the user asks about their Altium 365 workspace,+ projects or cloud library, or when a verb returns a365_not_configured. Trigger words — altium+ 365, a365, altium workspace graphql, altium oauth, altium cloud library, a365 token,+ altium_a365_query, altium_a365_oauth_begin.+---++# altium-a365++Parent skill: [altium-bridge](../altium-bridge/SKILL.md).++The cloud lane talks HTTPS to the workspace directly. It has no Altium Designer dependency: it+works while X2.EXE is closed and none of these verbs is gated on a licence. Tokens live in the+bridge cache (`a365.json`) and NEVER appear in a verb reply; `refresh` and the OAuth result+surface only an allow-listed summary of the token's claims.++## Two ways to get a token++1. **Manual token.** `altium_a365_configure {workspace, token}` stores the workspace domain+ (e.g. `acme.365.altium.com`) and a Bearer token created in the workspace web UI under+ Admin → Developer. Only plans that expose that page have one; the Develop trial does not.+ `workspace` is required the first time; either field can be updated alone later.+2. **OAuth (PKCE, public client).** `altium_a365_oauth_begin {clientId, workspace,+ selectWorkspace?, timeout?}`. `clientId` must be an Altium-issued OAuth client ID (issued+ through Altium's Developer Center enrollment; do not reuse a first-party client ID). Both+ fall back to the stored values. `selectWorkspace` is `strict` (default), `optional` or+ `none`. `timeout` is how long the bridge waits for the sign-in, default 300 s.++## The OAuth flow++```bash+altium_a365_oauth_begin '{"clientId":"<id>","workspace":"acme.365.altium.com"}'+# -> { status:"waiting", authorizeUrl } the bridge is ALREADY listening when this returns+# open authorizeUrl in the user's existing browser; they finish Altium's consent page+altium_a365_oauth_status '{}' # poll: idle | waiting | authorized | cancelled | error+altium_a365_library_overview '{}' # proves the workspace-scoped token+```++- The bridge starts its wait on Altium's ActionWait endpoint BEFORE it returns+ `authorizeUrl`, so open the URL only after the verb has returned.+- One flow at a time: a second `oauth_begin` while one is waiting returns+ `a365_oauth_in_progress`. `altium_a365_oauth_cancel` stops a waiting flow (replies+ `alreadyStopped:true` when none is waiting); poll `oauth_status` afterwards.+- On `error`, `oauth_status` carries an `errorCode`: `a365_oauth_timeout`,+ `a365_state_mismatch`, `a365_actionwait_gone`, `a365_actionwait_http`,+ `a365_actionwait_shape`, `a365_token_exchange_failed`, `a365_network_error`.+- On success the bridge stores the access token, the refresh token (the scope includes+ `offline_access`), the client ID and the expiry.++## Verbs++| Verb | What it does | Args |+|---|---|---|+| `altium_a365_configure` | store workspace + manual Bearer token | `workspace`, `token` |+| `altium_a365_status` | `configured`, `workspace`, `hasToken`, `hasRefreshToken`, `clientConfigured`, `endpoint`; with a token it also runs a one-row probe query and reports `tokenLive` + `probe` | — |+| `altium_a365_query` | run GraphQL against `https://<workspace>/svc/napi/gateway/graphql/`; replies `data` and `errors` (`success` is false when `errors` is present) | `query` (required), `variables`, `timeout` (s, default 60) |+| `altium_a365_oauth_begin` | start the PKCE sign-in, return `authorizeUrl` | `clientId`, `workspace`, `selectWorkspace`, `timeout` |+| `altium_a365_oauth_status` | state of the current or last flow | — |+| `altium_a365_oauth_cancel` | cancel a waiting flow | — |+| `altium_a365_refresh` | exchange the stored refresh token for a new access token; a rotated refresh token is stored too | — |+| `altium_a365_logout` | revoke the refresh token at Altium, then remove every cached token; if revocation fails the local grant is KEPT and the verb fails | — |+| `altium_a365_library_overview` | fixed query: the workspace library's name, description, updatedAt, component ids/names, footprint and symbol counts | — |+| `altium_a365_library_schema` | introspects the schema and returns only the `queries` and `mutations` whose names contain lib, component, symbol, footprint, part or datasheet | `timeout` |+| `altium_a365_files_upload` | multipart upload to the workspace Files Service; replies a temporary opaque `fileId` (`expiresAfterHoursInactive: 24`) to pass to the matching library mutation | `filePath` (required, a path on the bridge's machine), `maxBytes` (default 100 MB), `timeout` (s, default 120) |++## Error codes to branch on++`a365_not_configured` (no workspace or token: run `oauth_begin` or `configure`),+`a365_workspace_required`, `a365_client_id_required`, `a365_bad_workspace_selection`,+`a365_oauth_in_progress`, `a365_refresh_not_configured` (no client ID or refresh token+stored), `a365_refresh_failed`, `a365_revoke_failed`, `a365_graphql_http` (non-200 from+GraphQL; the token has usually expired, so `refresh` or sign in again),+`a365_network_error`, `a365_file_required`, `a365_file_not_found`, `a365_file_too_large`,+`a365_file_upload_failed`.++## Rules++- `library_schema` is a read-only map. Choose a mutation only after its input types and its+ rollback or delete mutation are understood.+- Treat `fileId` as opaque and short-lived: do not parse it, do not persist it.+- Explore the full schema in a browser at `https://<workspace>/api/voyager`.
skills/altium-boards/SKILL.md+59@@ -21,6 +21,35 @@ at the centre (per-primitive copy), nets created and assigned by pad designator named "49" joins the EP net), one stub track per assign. Size the FR4 to enclose silk + courtyard + 3 mm margin so the pin-1 dot is visible on the 3D board. +`footprintName` takes a `;` list (from 0.2.13): the parts are spread evenly along X inside+the wMm x hMm outline on one new board, and `assigns` / `stubs` apply to the LAST part+placed. From 0.2.18 / 0.2.19 the parts are designated U1..Un (the designator text stays+hidden) with the footprint name as Comment, and each part's name is written on the Top+Overlay under its footprint: 35 mil stroke text, centred on the part using the text's+measured bounding rectangle, its top 2.5 mm below the part centre, clear of a 3 mm part's+courtyard. Size `hMm` so that label fits on the board. Every call creates a NEW PcbDoc;+without `pcbPath` it stays unsaved.++## Schematic sheets: `altium_place_on_sch`++`altium_place_on_sch {libraryPath, symbolName, schPath, title, number, revision, drawnBy,+description}` places one symbol, or a `;` list, on ONE new sheet as U1..Un with the part+number as Comment. From 0.2.16 / 0.2.17 the sheet is sized FROM the parts:+- pass 1 measures every symbol's bounding box, pins included, plus 250 mil a side for the+ pin-number text;+- the sheet is a custom size: the row of parts with 500 mil between them and 600 mil+ margins, never smaller than A4 landscape (11500 x 7600 mil); slack is spread evenly over+ the gaps and margins;+- the designator sits centred above each body and the Comment centred below it, whatever+ the library part did with them;+- `title` / `number` / `revision` / `drawnBy` fill the title block, `description` becomes a+ text frame inside the drawing frame along the top, and `_hints` names any of them left+ empty.+The sheet size is not read back from `SheetSizeX/Y`: on ADDevelop those report the 9500 x+7500 default after a custom size is set. Every call creates a NEW SchDoc (no append yet);+after a re-run `altium_close_document {path}` the stale sheet before capturing with+`altium_show_schematic {filePath}`.+ ## Board shape: only the editor process works `IPCB_BoardOutline.PointCount/Segments/Rebuild` is a silent no-op and `.Clear` HALTS the@@ -60,6 +89,36 @@ their nets. Trust the report, not a return code. - **Frames under a locked Windows session are BLACK** (DirectX): check `desktop_get_foreground` ≠ 0 before trusting a board frame; GDI editors still capture. +## Whole-board read, cross-check and review++- `altium_read_board {sections?, limit?, raw?}`: ONE agent round trip for components, nets,+ layers, copper, rules, occupancy and outline (`sections` is a csv of those, default all;+ `limit` default 2000). Returns `summary`, `designators`, `nets`, `layers`, `copperLayers`,+ `rules`. `raw:true` returns the unshaped agent payload (what the test fixtures record).+- `altium_board_summary`, `altium_rules_by_kind`, `altium_layers_in_use` are views shaped in+ the bridge from the same read. `layers_in_use` gives `copperLayersInUse` (copper layers+ carrying copper, densest first; a floor, not the stack, since an empty plane is invisible)+ and `layersWithPrimitives` (silkscreen and mechanical counted too).+- `altium_read_schematic {sections?, params?, limit?}`: components of the CURRENT sheet+ (`designators`, `components`, `count`); `params:false` skips parameters; `limit` default+ 500. A hierarchical parent holds sheet symbols, not parts.+- `altium_cross_check {board?, sheet?}`: opens each path in turn (Altium holds one current+ document), then reports `matched`, `onSheetNotOnBoard`, `onBoardNotOnSheet`,+ `schematicComponents`, `boardComponents`, `sameProject`. The schematic side is ONE sheet;+ on a hierarchical design run it per leaf sheet. A `_warning` appears when sheet and board+ sit in different folders.+- `altium_review_board {board?, sheet?, includeSchematic?, checks?}`: 14 checks over one+ board read: `placeholder_values`, `single_pin_nets`, `unused_mechanical_layers`,+ `off_grid_components`, `duplicate_designators`, `components_outside_board_outline`,+ `components_near_board_edge`, `disabled_design_rules`, `missing_critical_rules`,+ `duplicate_rule_priority`, `components_without_footprint`, and the schematic-side+ `vendor_part_numbers`, `missing_datasheets`, `schematic_placeholder_values` (these need+ `includeSchematic:true` or `sheet`). `checks` filters by name. Reply: `findings[]`+ (`check`, `severity`, `message`), `counts`, `checksRun`, `availableChecks`, `skipped`,+ `notImplemented`, `unknownChecks`. A clean result means the checks that RAN found nothing.++All of these need a licensed Altium and read the current document unless a path is given.+ ## Reading the board as data (0.1.11) `list_designators`, `component_data {designator?}`, `component_pins {designator}`, `list_nets`,
skills/altium-bridge-plan/SKILL.md+6−6@@ -16,8 +16,8 @@ description: >- --- Parent skill: **adom-bridge-sdk-new-app** (the method) and **adom-bridge-sdk** (the contract).-Written 2026-08-29 from live probes of Altium Designer 26.9.1 on John's box (`galliaApril`-container → `AdomLapper` desktop) plus the vendor docs and the community MCP bridges.+Written 2026-08-29 from live probes of Altium Designer 26.9.1 on a Windows test machine+(driven from an Adom container) plus the vendor docs and the community MCP bridges. # altium-bridge — plan of record @@ -32,7 +32,7 @@ container → `AdomLapper` desktop) plus the vendor docs and the community MCP b | Listening ports | **none** while idle. No application COM automation server registered — only document ProgIDs (`AltiumLibrary`, `AltiumDraftsmanDocument`, …). | | UIA | The VCL/DevExpress shell **IS exposed**: 45 nodes on the licence screen — `MainMenu` MenuItems (File/View/Project/Window/Help/Preferences…), `TGUIDocBarButton`s, `TdxTabSheet` panes, all with `automationId`s and `invokable:true`. The **Home / License Management page content is a WebView2** (`ProgramData\...\WebView2Cache` exists) — its *Start Free Trial / Get License / Sign Out* buttons are **not** in the UIA tree. | | State dirs | `%APPDATA%\Altium\Altium Designer {ED62C102-…}\` (prefs), `%APPDATA%\Altium\AD26\`, `C:\ProgramData\Altium\Altium Designer {ED62C102-…}\Extensions\` (Altimade, Draftsman, IPC Footprint Generator, EXPORT3D, IPC2581, SchLibGenerationTool, …), `HKCU\Software\Altium\Altium Designer {ED62C102-…}`. |-| Auth state | Signed in as *John John* (Altium account), **no license**; a free *Altium Designer Viewer* seat is offered and a **15-day trial** button exists. Vendor docs: **Viewer mode prohibits scripting** → licence is a hard prerequisite for every in-app lane. |+| Auth state | Signed in to an Altium account, **no license**; a free *Altium Designer Viewer* seat is offered and a **15-day trial** button exists. Vendor docs: **Viewer mode prohibits scripting** → licence is a hard prerequisite for every in-app lane. | ## 1. The surfaces, ranked into lanes @@ -138,7 +138,7 @@ Costs a licensed AD + a build toolchain; do it only after L2 proves insufficient ## 2. Authentication & licensing — the gates, in order 1. **Altium account sign-in** — system browser OAuth (email / Google / SSO). Drive with- `nbrowser`/pup from the bridge; the user's Google session on this box is warm. Human step: password/2FA only.+ `nbrowser`/pup from the bridge; the user's browser is usually already signed in to their identity provider. Human step: password/2FA only. 2. **Licence** — On-Demand / Standalone / **Trial (15 days)** / **Viewer (no scripting)**. The trial button lives in the WebView2 licence page → CDP or image click, one toast to the user because it is a contractual click. `altium_readiness` must report `licensed:false` with the@@ -214,11 +214,11 @@ heartbeat and recover. Keep `handlers/dialog_classify.py` with title → categor `altium-libraries`, `altium-a365`, `altium-signin` sub-skills), 30–40 discovery triggers, hero, `make release`, audit checklist. Then (optional) Phase 5: C# SDK extension. -## 6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial, AdomLapper) — the facts the code is built on+## 6b. Phase 0 RESULTS (measured 2026-08-29, AD 26.9.1 Trial) — the facts the code is built on | Experiment | Result | |---|---|-| Trial licence | `Start Free Trial` (WebView2, image click) → Edge opens `altium.com/365-intro/activate-add-eval` → workspace form (name, region, terms, **Activate**) → workspace `Adom` provisioned at `john-john-3.365.altium.com` in ~60 s → back in AD: **Refresh**, then **Use License** on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to `… - Trial`. |+| Trial licence | `Start Free Trial` (WebView2, image click) → Edge opens `altium.com/365-intro/activate-add-eval` → workspace form (name, region, terms, **Activate**) → a workspace is provisioned at `<name>.365.altium.com` in ~60 s → back in AD: **Refresh**, then **Use License** on "Altium Designer Develop · On-Demand · Trial · 30 days". Title flips to `… - Trial`. | | `-R` forwarding | A second `X2.EXE -R<Server>:<Cmd>(…)` forwards to the running instance and exits in ~2–8 s. **If the instance is busy (any modal, or a script running) the helper hangs and shows its own "There is another instance … busy" Information dialog** — kill it. | | `RunScriptText` | Works from the shell: `-RScriptingSystem:RunScriptText(Text="Begin RunApplication('…'); End;")`. **Statements only** — a `Var` block is "Unknown identifier: Var". | | `RunScript` | `-RScriptingSystem:RunScript(ProjectName="…\X.PrjScr"\|ProcName="Main")` works with a plain `\|` when the arg is passed VERBATIM (PowerShell `Start-Process`, or Python `Popen("<one string>")`). `Popen([exe, arg])` re-quotes (the path has a space) and the helper never forwards. `^\|` is only for cmd.exe. |
skills/altium-instances/SKILL.md+8@@ -47,6 +47,14 @@ If you upgrade a bridge while a loop is resident, the OLD loop keeps heartbeatin path. A script lane refuses when ANY workspace has a live heartbeat — that is deliberate; stop the old loop (its workspace path is named in the refusal) rather than forcing past it. +`altium_agent_status` shows this from the outside: `alive` and `responsive` for the selected+instance's loop, plus `residentAnywhere` and `liveWorkspaces[]` (`workspace`,+`heartbeatAgeSec`) across every workspace. `alive:false` with `residentAnywhere:true` means a+loop is resident in a workspace the selection does not point at, which is a freeze-on-close+hazard until it is torn down; `altium_close` stops every workspace's loop. `alive:true` with+`responsive:false` means the loop is inside a long Altium process: wait, do not start a+second agent.+ ## Where single-instance assumptions hid All three were live bugs, all found within an hour of running two Altiums:
skills/altium-libraries/SKILL.md+45@@ -31,6 +31,38 @@ data only — switch views with the process `Sch:ToggleComponentDisplayMode` **Ralph rule: frame-check EVERY mode, not a sample.** The first LAN7800 shipped with three defect classes visible only in the modes nobody looked at. +## Chip outline on the symbol (from 0.2.15)++adom-symbol bakes the chip-thumbnailer outline into every KiCad IC symbol as polylines.+`altium_add_symbol_art {libraryPath, symbolName, kicadSym}` carries that artwork onto the+SchLib component: the bridge reads `kicadSym` (a Windows path on the Altium box, stage it with+`send_files`), takes the symbol's body rectangle and every polyline, and the agent maps the+KiCad body rectangle onto the component's own rectangle, so the outline lands between the+pin-name columns as it does in KiCad and Fusion. Drawn thin (`eSmall`) in grey.+- `clear` (default true) removes the component's existing lines and polylines first, so a+ re-run replaces the art instead of stacking it. Only the pins and the body rectangle+ survive, hand-drawn lines included. `clear:false` stacks.+- `mode`: `lines` (default) draws each polyline as eLine segments, the primitive+ `build_symbol_modes` draws with. `polyline` draws ISch_Polyline objects; measured+ 2026-09-19, those rendered on VL53L5CX and stayed invisible on VL53L8CX with identical+ geometry, which is why `lines` is the default.+- Fails with a clear error when the symbol has no body rectangle or no polylines (bake the+ outline with adom-symbol first). Reply: `polylines`, `points`, `cleared`, `mode`,+ `bodyRectMil`. `altium_save` the SchLib afterwards, then `altium_show_symbol` to look.+- It draws the outline only. Group captions are written by adom-lbr into the SchLib it+ generates, not by this verb.++`altium_symbol_prims {libraryPath, symbolName, models?}` is the read-only check: `pins`,+`rectangles`, `lines`, `polylines`, `bodyRectMil`, `polyBboxMil` and, with `models:true`,+the footprint models. The model walk is off by default because it halted the engine on+ADDevelop. After `add_symbol_art` in `lines` mode, expect `lines` to have grown and the art+to sit inside `bodyRectMil`.++`altium_set_models {libraryPath, symbolName, footprint}` (from 0.2.15) replaces every model+on the component with ONE PCBLIB model. `set_params` footprint and `add_models` both append,+which is how a symbol ends up with two; this removes all existing models, 3D-only ones+included. Save afterwards.+ ## Footprints: the spec lane `altium_build_footprint {libraryPath, spec}` consumes a `tools/kicad2altium.py` spec:@@ -43,6 +75,19 @@ defect classes visible only in the modes nobody looked at. - Pads must carry a Layer or they are SILENTLY DROPPED (measured); `Bottom*/Mid*` pad size properties do not exist in this engine (halt). +## Heatsink vias from an imported library: `altium_plate_vias` (from 0.2.20)++adom-lbr writes heatsink vias as pads with a hole inside the thermal pad. Altium loaded those+pads on the Top Layer: the holes came, the layer did not, so they were not through-holes.+`altium_plate_vias {libraryPath, footprintName?}` walks every footprint in the PcbLib (or+only `footprintName`) and sets each drilled pad that is not already on Multi-Layer to+`eMultiLayer` with `Plated := True`. Reply: `holes` (drilled pads seen), `plated` (pads+changed), `fixed` (`Footprint:Pad;...`). It edits the open library in place, so+`altium_save` the PcbLib afterwards; `altium_footprint_primitives` on a placed part should+then report layerId 74 for the vias. Pads already on Multi-Layer are skipped, so running it+twice is harmless. adom-lbr 2.46.1 and later write the pads as plated Multi-Layer in the+file itself; the verb is for libraries generated before that.+ ## 3D bodies: the canvas rule The stored lib component is NOT the editing surface. A body added straight to the component
skills/altium-licensing/SKILL.md+17−1@@ -19,7 +19,7 @@ Parent skill: [altium-bridge](../altium-bridge/SKILL.md). ## The rule that explains almost everything **One On-Demand seat serves EVERY Altium app on the machine.** Altium Designer and Altium Designer-Develop draw from the *same* seat — on John's box the trial licence is literally named+Develop draw from the *same* seat — on the test machine the trial licence is literally named `Altium Designer Develop` (On-Demand, Single Site, 1 seat) and both apps use it. So: * Two Altiums can RUN at once. Only ONE can be LICENSED at once.@@ -47,6 +47,22 @@ whole fix, and nothing else worked. `altium_close {force:true}` now refuses unless you also pass `acceptSeatLoss:true`, precisely so this cannot be reached for casually. +## The three status and recovery verbs++- `altium_license_status`: read-only. `running`, `licensed`, `licenceState` (from the window+ title), `seatStuck`, a plain-words `summary`, and `shot` of the License Management page.+ Trial details (days left, seat n/n, licence code) exist only on the page, so read the frame.+- `altium_ensure_licensed {launchWaitSec?, allowSignInReset?, allowTrial?}`: one call to get+ the user usable. Launches Altium if it is not running (waits up to `launchWaitSec`, default+ 240), returns at once when already licensed, otherwise tries `use_license`. With no seat+ free it returns `no_seat_available`; with `allowSignInReset:true` it signs out instead and+ returns `signed_out_for_reset` (`needsHuman: "browser sign-in"`); with `allowTrial:true` it+ opens the trial flow and returns `trial_needs_browser`. Every reply carries `steps[]`.+- `altium_setup_trial`: onboarding from nothing to a working trial. Returns ONE next action+ per call with a `stage` (`install`, `launch`, `signin`, `done`) and names human steps in+ `needsHuman` (the installer's EULA click, the browser sign-in) rather than faking them.+ Signed in but unlicensed, it hands over to `ensure_licensed` with `allowTrial`.+ ## Swapping the seat between Designer and Develop ```bash
skills/altium-part-import/SKILL.md+12@@ -80,6 +80,18 @@ python3 tools/kicad2altium.py part.kicad_mod fp.spec "<Footprint>" "<desc>" # altium_import_part '{"schLib":"...","pcbLib":"...","footprintSpec":"C:/.../fp.spec","step":"C:/.../part.step","params":{...},"description":"..."}' ``` +After the library exists (from 0.2.15 / 0.2.20), two finishing steps for parts that came+through adom-symbol / adom-lbr:+```bash+# chip outline from the KiCad symbol onto the SchLib component, then check it+altium_add_symbol_art '{"libraryPath":"<lib>.SchLib","symbolName":"VL53L5CX","kicadSym":"C:/lib/VL53L5CX.kicad_sym"}'+altium_symbol_prims '{"libraryPath":"<lib>.SchLib","symbolName":"VL53L5CX"}'+# heatsink vias that loaded on the Top Layer -> plated Multi-Layer pads (libraries from adom-lbr before 2.46.1)+altium_plate_vias '{"libraryPath":"<lib>.PcbLib"}'+altium_save '{"filePath":"<lib>.PcbLib"}'+```+`add_symbol_art` replaces earlier art by default (`clear:true`); details in `altium-libraries`.+ ## Path C — APPEND into an existing library ```bash
skills/altium-uia/SKILL.md+7@@ -35,6 +35,13 @@ do those clicks"*. He was right — the licence lane had been clicking hardcoded | **Panels button** (bottom-right) | `GUIDocForm.TPanelsHolderXPButton`, name `Panels` | Invokable. | | Canvas / PCB + schematic editing area | NOT exposed | Custom-drawn. Geometry work goes through the resident agent's scripting API, never through input. | +## What the reaper did: `altium_dialog_log`++`altium_dialog_log {limit?}` (default 25) returns `reaped[]`, the dialogs the standing reaper+dismissed, including close-watchdog firings. Anything that reached the user and is NOT in+the list is either a save prompt (never auto-answered, by design) or a dialog missing from+the catalog: report it so it can be added with a safe action.+ ## Two costs, both paid for in this session 1. **UIA against Altium's MAIN window is slow enough to be unusable.** A `desktop_find_control` took
uninstall.sh+1−1@@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail for HOME_DIR in "$HOME/.claude/skills" "$HOME/.codex/skills"; do- rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" "$HOME_DIR/altium-install" "$HOME_DIR/altium-libraries" "$HOME_DIR/altium-boards" "$HOME_DIR/altium-outputs" "$HOME_DIR/altium-bridge-dev" "$HOME_DIR/altium-bridge-publish"+ rm -rf "$HOME_DIR/altium-bridge" "$HOME_DIR/altium-bridge-plan" "$HOME_DIR/altium-part-import" "$HOME_DIR/altium-kicad-import" "$HOME_DIR/altium-install" "$HOME_DIR/altium-libraries" "$HOME_DIR/altium-boards" "$HOME_DIR/altium-outputs" "$HOME_DIR/altium-instances" "$HOME_DIR/altium-licensing" "$HOME_DIR/altium-uia" "$HOME_DIR/altium-wiki-import" "$HOME_DIR/altium-a365" "$HOME_DIR/altium-bridge-dev" "$HOME_DIR/altium-bridge-publish" done rm -rf "$HOME/.local/share/adom/altium-bridge" rm -f "$HOME/.local/bin/altium-dashboard"