← Commit history
README.md+1−1
@@ -11,7 +11,7 @@ verbs.  ```bash adom-wiki pkg install barrett-land/adom-desktop-altium-bridge   # the skill, in the container-adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-manifest.json"}'+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-manifest.json"}' adom-desktop --ai-thread "<your thread>" altium_ping '{}' ``` 
SKILL.md+39−11
@@ -12,7 +12,7 @@ forwards work into the running GUI as DelphiScript via `X2.EXE`. ## Install  ```bash-adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-manifest.json"}'+adom-desktop bridge_install '{"manifestUrl":"https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-manifest.json"}' adom-desktop --ai-thread "<your thread>" altium_ping '{}'   # -> {"altiumInstalled": true, "version": "..."} ``` @@ -82,18 +82,46 @@ adom-desktop --ai-thread "<thread>" altium_run_outjob '{  - Always pass `project`. The script opens **and compiles** it first, which is what populates an   OutJob's `[Project Physical Documents]` target. Without it the OutJob has nothing to act on.-- Pass `expect` to get an authoritative `out_exists` in the reply instead of inferring success.+- Pass `expect`. With it, the verb checks the filesystem and reports `ok: false, triggered: false`+  when the run produced nothing, instead of a hollow success. - An OutJob targeting `[Project Physical Documents]` is **generic and reusable** across boards —   add it to the project, compile, run.-- ⚠️ **Known limitation:** `GenerateReport` is observed to no-op in configurations where the GUI's-  *Generate content* button on the same OutJob works. `ran: true` with `out_exists: false` means-  the trigger did not fire — it does **not** mean the OutJob is misconfigured. Ask the user to click-  *Generate content* once rather than rebuilding a working OutJob.--`altium_export_step` opens a `.PcbDoc` and reports that the board resolved. With `run: true` it also-fires the process named by `proc` (default `Pcb:ExportSTEP`, overridable so candidates can be probed-without redeploying the script). If that process opens a modal, the call times out with the export-possibly still pending — **check the output path on disk before concluding it failed.**++### What generates headlessly, and what doesn't++| Output type | Headless via `altium_run_outjob`? |+|---|---|+| **Gerber** (`.GTL` copper, `.GTO` silk, `.GTS` mask, `.GM1` outline) | ✅ yes |+| **ExportSTEP** | ❌ no — `GenerateReport` no-ops; a human must click *Generate content* once |++The STEP exception is not a misconfiguration. It was verified with+`ObjectKind=OutputBatch/OutputMedium/Document`, with and without `OutputMedium`, and with the+OutJob already a member of a compiled project. Ask the user to click the button rather than+rebuilding an OutJob that is already correct.++### ⚠️ There is no headless STEP-export process++`altium_export_step` **opens a board and reports it resolved — it does not export.** Every+`Command='...'` in AD26's resource files was enumerated:++- `Pcb:ExportSTEP` **does not exist.** It was this verb's old default, and because Altium silently+  ignores unknown process ids, the call returned `ran: true` and wrote no file.+- The real one, `PCB:Export`, is index-based (`Group=Model`) and opens a file dialog.++`run: true` now demands an explicit `proc` (a probe hatch only) and fails loudly when no file+appears. **To actually get a STEP, use an ExportSTEP OutJob plus one human click.**++### The STEP has no copper and no silkscreen++Altium's STEP is the board substrate plus component 3D bodies — nothing else. A render made from+it alone is a bare green board. Rebuild both from Gerbers:++- **silkscreen** = `.GTO`+- **exposed pads** = `.GTL` ∩ `.GTS`. `.GTS` is `TF.FilePolarity,Negative`, so its features are the+  mask *openings*; all of `.GTL` is ~76% of the board because most copper is a covered pour.++Composite them into one RGBA overlay and pass it to `step2glb --silk-top` (which only takes effect+with `--molecule`). The full recipe lives in the **altium-export-molecule** skill.  ## Developing the bridge 
adom-desktop-altium-bridge/bridge-src/__pycache__/addin_client.cpython-312.pycadded
⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/altium_detect.cpython-312.pycadded
⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/check_wiring.cpython-312.pycadded
⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/server.cpython-312.pycadded
⋯ 1 unchanged line ⋯
adom-desktop-altium-bridge/bridge-src/__pycache__/test_addin_client.cpython-312.pycadded
⋯ 1 unchanged line ⋯
altium-bridge-manifest.json+3−3
@@ -1,3 +1,3 @@-{ "manifest_version": 1, "name": "altium", "version": "0.0.40",-  "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.40/altium-bridge-v0.0.40.zip",-  "sha256": "49a6ac3d9ca77f5be2c43a1c1f257a427fd99a8296f93391b8faf105f6042ec1", "size": 26085, "released_at": "2026-08-05T20:55:37Z" }+{ "manifest_version": 1, "name": "altium", "version": "0.0.41",+  "url": "https://wiki.adom.inc/download/barrett-land/adom-desktop-altium-bridge/0.0.41/altium-bridge-v0.0.41.zip",+  "sha256": "ce614444afefcdffa56f4d03e57ff4fdd4bd13d2f8b59859477f2b192db9c92f", "size": 26776, "released_at": "2026-08-05T22:16:21Z" }
altium-bridge-v0.0.41.zipadded
⋯ 1 unchanged line ⋯
bridge-src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.0.40+0.0.41
bridge-src/__pycache__/server.cpython-312.pyc
⋯ 1 unchanged line ⋯
bridge-src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Bridge",-  "version": "0.0.40",+  "version": "0.0.41",   "description": "Drives Altium Designer from a container: build, clone and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob. Pairs with the container-side altium-jlc orchestrator (adom-jlcpcb + chip-fetcher).",   "homepage": "https://wiki.adom.inc/adom/adom-desktop-altium-bridge",   "author": "Adom Inc.",
bridge-src/server.py+54−17
@@ -277,29 +277,49 @@ def handle_script_state(args: dict) -> dict:   def handle_export_step(args: dict) -> dict:-    """Export a .PcbDoc's 3D board assembly to STEP.+    """Open a .PcbDoc and report that its board resolved. Does NOT export by default. -    Two modes. Without run=true it only OPENS the board and reports that it resolved-    (the safe probe). With run=true it also fires the export process named by 'proc'-    (default Pcb:ExportSTEP) writing to 'out'.+    There is NO headless board->STEP process in Altium. Every `Command='...'` in+    AD26's resource files was enumerated: the old default here, `Pcb:ExportSTEP`,+    does not exist, and Altium silently ignores unknown process ids — so the call+    reported ran=true and wrote nothing. The real process, `PCB:Export`, is+    index-based (`Group=Model`) and opens a file dialog, so it cannot be driven+    headlessly either. -    Altium exposes no documented headless board->STEP process, so 'proc' is-    overridable to probe candidates without redeploying the script. If the process-    opens a modal dialog the script blocks and never writes a response — the call-    times out with the export possibly still pending. Check 'out' on disk before-    assuming failure.+    Use `altium_run_outjob` with an OutJob carrying an ExportSTEP output instead.+    `run` is kept only as a probe hatch for trying a candidate process id, and now+    demands an explicit `proc` rather than guessing one that cannot work.     """     if not args.get("pcbdoc"):         return {"ok": False, "error": "missing 'pcbdoc' (path to the .PcbDoc on the desktop)"}-    if args.get("run") and not args.get("out"):-        return {"ok": False, "error": "run=true also needs 'out' (destination .step path)"}++    if args.get("run"):+        if not args.get("proc"):+            return {+                "ok": False,+                "error": "run=true needs an explicit 'proc'; there is no working default.",+                "_hint": "Altium has no headless STEP-export process. To actually export, use "+                         "altium_run_outjob with an OutJob containing an ExportSTEP output. Pass "+                         "'proc' here only to probe a candidate process id.",+            }+        if not args.get("out"):+            return {"ok": False, "error": "run=true also needs 'out' (destination .step path)"}+     params = {         "pcbdoc": args.get("pcbdoc"),         "run": "true" if args.get("run") else None,         "out": args.get("out"),         "proc": args.get("proc"),     }-    return addin_call("export_step", params, timeout=args.get("timeout", 240))+    res = addin_call("export_step", params, timeout=args.get("timeout", 240))++    # An unknown process id still comes back ran=true. Only the file on disk is proof.+    if isinstance(res, dict) and res.get("ran") and not res.get("out_exists"):+        res["ok"] = False+        res["error"] = (f"process {args.get('proc')!r} produced no file at {args.get('out')!r} — "+                        "it most likely does not exist (Altium ignores unknown process ids).")+        res["_hint"] = "Use altium_run_outjob with an ExportSTEP OutJob; that path is proven."+    return res   def handle_run_outjob(args: dict) -> dict:@@ -311,10 +331,15 @@ def handle_run_outjob(args: dict) -> dict:     Pass 'expect' (a path the run should produce) to get an authoritative out_exists     in the reply instead of guessing from the process return. -    KNOWN LIMITATION: GenerateReport is observed to no-op in some configurations-    where the GUI's 'Generate content' button on the same OutJob works. Treat-    out_exists=false with ran=true as "the trigger did not fire", not "the OutJob is-    misconfigured", and verify by hand before rebuilding the OutJob.+    WHAT WORKS, MEASURED: Gerber outputs generate fine headlessly through this verb.+    `ExportSTEP` outputs are the exception — GenerateReport no-ops on them while the+    GUI's 'Generate content' button on the SAME OutJob works. Verified with+    ObjectKind=OutputBatch/OutputMedium/Document, with and without OutputMedium, and+    with the OutJob already a member of a compiled project, so none of those are the+    cause. For STEP, ask the user to click Generate content once.++    Pass `expect` and this verb will tell you the trigger failed instead of reporting+    a hollow success.     """     if not args.get("outjob"):         return {"ok": False, "error": "missing 'outjob' (path to the .OutJob on the desktop)"}@@ -331,7 +356,19 @@ def handle_run_outjob(args: dict) -> dict:     for key in ("reload", "disable_dialog"):         if key in args:             params[key] = "false" if args[key] in (False, "false") else "true"-    return addin_call("run_outjob", params, timeout=args.get("timeout", 300))+    res = addin_call("run_outjob", params, timeout=args.get("timeout", 300))++    # GenerateReport reports success even when it silently did nothing, which is how a+    # no-op reads as a working export. If the caller told us what to expect, believe+    # the filesystem over the process.+    if isinstance(res, dict) and args.get("expect") and res.get("ran") and not res.get("out_exists"):+        res["ok"] = False+        res["triggered"] = False+        res["error"] = f"OutJob ran but produced nothing at {args['expect']!r}."+        res["_hint"] = ("GenerateReport no-ops on ExportSTEP outputs (Gerber outputs are fine). "+                        "Ask the user to open the OutJob and click 'Generate content' once; do not "+                        "rebuild the OutJob, it is most likely correct.")+    return res   COMMAND_HANDLERS = {
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-desktop-altium-bridge",-  "version": "0.0.40",+  "version": "0.0.41",   "type": "app",   "description": "Adom Desktop bridge for Altium Designer \u2014 build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.",   "tags": [
page.json+4−3
@@ -1,8 +1,8 @@ {   "slug": "adom-desktop-altium-bridge",-  "version": "0.0.40",+  "version": "0.0.41",   "type": "app",-  "description": "Adom Desktop bridge for Altium Designer \u2014 build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.",+  "description": "Adom Desktop bridge for Altium Designer — build and edit file-based symbol/footprint/Integrated libraries with LCSC/JLCPCB part numbers attached, and generate batch outputs (STEP, PDF, Gerber, BOM, CPL) from an OutJob, by driving Altium via DelphiScript.",   "tags": [     "app",     "adom-desktop",@@ -18,6 +18,7 @@   },   "needs_sudo": false,   "hero": {+    "type": "image",     "path": "docs/hero.png"   },   "author": {@@ -48,4 +49,4 @@       "prompt": "Is the Altium bridge connected and what version is it?"     }   ]-}+}\ No newline at end of file