← Commit history
PLAN.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@@ -211,11 +211,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. |
bridge-rs/Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "altium-bridge"-version = "0.2.8"+version = "0.2.21" dependencies = [  "getrandom",  "serde",
bridge-rs/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.20"+version = "0.2.21" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/a365.rs+2−2
@@ -184,7 +184,7 @@ pub fn configure(args: &Value) -> Value {         }     }     if cfg.get("workspace").and_then(|w| w.as_str()).unwrap_or("").is_empty() {-        return json!({"success": false, "error": "workspace is required, e.g. 'john-john-3.365.altium.com'"});+        return json!({"success": false, "error": "workspace is required, e.g. 'acme.365.altium.com'"});     }     save(&cfg);     json!({"success": true, "workspace": cfg.get("workspace"), "hasToken": cfg.get("token").map(|t| !t.is_null()).unwrap_or(false),@@ -247,7 +247,7 @@ pub fn oauth_begin(args: &Value) -> Value {     let workspace = args.get("workspace").or_else(|| cfg.get("workspace")).and_then(|x| x.as_str()).unwrap_or("").to_string();     if workspace.is_empty() {         return json!({"success": false, "errorCode": "a365_workspace_required",-            "error": "workspace is required, e.g. john-john-3.365.altium.com"});+            "error": "workspace is required, e.g. acme.365.altium.com"});     }     if client_id.is_empty() {         return json!({"success": false, "errorCode": "a365_client_id_required",
bridge-rs/src/agent.rs+18
@@ -167,6 +167,8 @@ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], p         return json!({"success": false, "error": format!("cannot post request: {e}")});     }     let t_post = t0.elapsed().as_millis() as u64;+    let posted_unix_ms = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map(|x| x.as_millis() as u64).unwrap_or(0);+    let mut next_halt_check = Duration::from_millis(400);     let resp = ws.join(format!("response_{id}.json"));     let mut t_seen: Option<u64> = None;     let mut parse_retries = 0u32;@@ -189,6 +191,22 @@ pub fn call_in(ws: &std::path::Path, command: &str, params: &[(&str, String)], p             std::thread::sleep(Duration::from_millis(5));             continue;         }+        // The engine halted on a script runtime error: the reaper pressed the Error box, the+        // script now sits on a breakpoint and will NEVER answer. Do not burn the timeout and do+        // not leave Altium parked in its debugger: stop the script now and say what happened.+        if t0.elapsed() >= next_halt_check {+            next_halt_check = t0.elapsed() + Duration::from_millis(400);+            if let Some(ev) = crate::dialogs::script_error_since(posted_unix_ms) {+                let recovered = crate::lifecycle::recover(&format!("auto: script Error box during agent command '{command}'"));+                return json!({"success": false, "errorCode": "agent_script_error", "command": command,+                    "error": format!("the agent script raised a runtime error while running '{command}' (Altium showed an 'Error' box; its text is in errorShot)"),+                    "errorShot": ev.get("shot").cloned().unwrap_or(Value::Null),+                    "autoRecovered": recovered.get("success").and_then(|b| b.as_bool()).unwrap_or(false),+                    "recoverSteps": recovered.get("steps").cloned().unwrap_or(Value::Null),+                    "agentAlive": is_alive(),+                    "_hint": "READ errorShot (a PNG of Altium's error box, e.g. 'Undeclared identifier: X' = that API does not exist on this Altium build). The bridge already sent EditScript:Stop, so Altium is NOT parked on a breakpoint and the next verb restarts the agent. Do not retry the same command: it will halt again. Report it on adom/altium-bridge with the errorShot."});+            }+        }         std::thread::sleep(Duration::from_millis(2));     }     json!({"success": false, "errorCode": "agent_timeout", "error": format!("agent did not answer within {}s", timeout.as_secs()),
bridge-rs/src/dialogs.rs+32
@@ -244,6 +244,22 @@ pub fn dismiss(button: &str, only_titles: Option<&[String]>) -> Value { static REAPED: Mutex<Vec<Value>> = Mutex::new(Vec::new()); static REAPED_HWNDS: Mutex<Vec<i64>> = Mutex::new(Vec::new()); static REAPER_RUNNING: AtomicBool = AtomicBool::new(false);+/// The last lone-OK box titled "Error" the reaper pressed: {atMs, title, shot}. A DelphiScript+/// runtime error ("Undeclared identifier ...") is exactly this box, and pressing OK leaves the+/// script STOPPED ON A BREAKPOINT in Altium's script debugger: the agent loop is dead, Altium+/// refuses to close, and the caller used to see nothing but a timeout (found on AD 26.9.1,+/// 2026-09-20). agent::call watches this so the halt is reported and cleared, never silent.+static LAST_SCRIPT_ERROR: Mutex<Option<Value>> = Mutex::new(None);++fn now_ms() -> u64 {+    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|x| x.as_millis() as u64).unwrap_or(0)+}++/// The script Error box reaped at or after `since_ms` (unix ms), if any.+pub fn script_error_since(since_ms: u64) -> Option<Value> {+    LAST_SCRIPT_ERROR.lock().ok().and_then(|g| g.clone())+        .filter(|e| e.get("atMs").and_then(|a| a.as_u64()).unwrap_or(0) >= since_ms)+}  /// Record a service event (close watchdog etc.) in the same ring `altium_dialog_log` shows. pub fn note_event(mut ev: Value) {@@ -300,7 +316,23 @@ pub fn reap_once() -> usize {         if REAPED_HWNDS.lock().map(|g| g.contains(&hwnd)).unwrap_or(true) {             continue;         }+        // A lone-OK "Error" is how a script runtime error looks. Altium PAINTS the message, so+        // photograph it BEFORE pressing OK: the picture is the only copy of the error text.+        let title_str = d.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string();+        let is_script_error = cat == "single_ok_acknowledgement" && title_str.trim().eq_ignore_ascii_case("error");+        let mut err_shot = Value::Null;+        if is_script_error {+            let r = ab::call("desktop_screenshot_base64", json!({"hwnd": hwnd}), &[], 10);+            if let Some(img) = r.get("image").or_else(|| r.get("base64")).and_then(|i| i.as_str()) {+                err_shot = json!(img);+            }+        }         let ok = press(hwnd, &act).get("success").and_then(|b| b.as_bool()).unwrap_or(false);+        if is_script_error {+            if let Ok(mut g) = LAST_SCRIPT_ERROR.lock() {+                *g = Some(json!({"atMs": now_ms(), "title": title_str, "shot": err_shot}));+            }+        }         let mut followup = Value::Null;         if ok && cat == "licence_chooser" {             // The chooser only opens the License Management page; the seat is claimed by the
bridge-rs/src/main.rs+1−1
@@ -25,7 +25,7 @@ mod win32; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.20";+pub const BRIDGE_VERSION: &str = "0.2.21";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/verbs.rs+1−1
@@ -122,7 +122,7 @@ fn describe() -> Value {             "Run once per part right after assemble-altium / build_symbol_modes, then altium_save the SchLib and altium_show_symbol to check.",             &["show_symbol", "symbol_prims", "save"], &["clear:true (default) also removes hand-drawn lines on the symbol; only the pins and the body rectangle survive.", "The KiCad symbol must already carry the outline (adom-symbol create bakes it; --outline-name none for side-etched chips)."],             json!({"libraryPath": "C:/lib/Adom_ToF.SchLib", "symbolName": "VL53L5CX", "kicadSym": "C:/lib/VL53L5CX.kicad_sym"})),-        verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sized A4 or B for the part count, parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given.",+        verb_doc("place_on_sch", "Place one symbol, or a ';' list of symbols, from the open SchLib on ONE new sheet (sheet sized FROM the measured parts: 500 mil gaps, 600 mil margins, A4 minimum; parts inside the frame as U1..Un with the part number as Comment); fills the title block from title/number/revision/drawnBy and places description as a text frame; saves to schPath when given.",             json!({"libraryPath": "path (.SchLib)", "symbolName": "string | 'A;B;C'", "schPath": "path (.SchDoc, optional save)", "title": "string (Title)", "number": "string (DocumentNumber)", "revision": "string", "drawnBy": "string", "description": "string (what the sheet shows, 2-3 sentences)"}),             json!({"placed": "string", "count": "int", "designator": "string", "primitives": "int", "sch": "path"}), 90,             "Three parts on one sheet: symbolName 'VL53L5CX;VL53L7CX;VL53L8CX' + schPath, then altium_show_schematic {filePath: schPath}.",
bridge.json+20−5
@@ -2,20 +2,20 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.41",+  "version": "0.2.21",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",   "author": "Adom Inc.",   "license": "MIT",   "spawn": {-    "kind": "python",-    "entrypoint": "server.py",+    "kind": "exe",+    "entrypoint": "altium-bridge.exe",     "port": 0,     "persistent": true,     "healthEndpoint": "/health",     "stopMethod": "kill",-    "killImageName": "python.exe"+    "killImageName": "altium-bridge.exe"   },   "verbPrefixes": [     "altium_"@@ -35,17 +35,21 @@     "altium_activate_document",     "altium_add_body",     "altium_add_models",+    "altium_add_symbol_art",     "altium_agent_recover",     "altium_agent_start",     "altium_agent_status",     "altium_agent_stop",+    "altium_animate_view",     "altium_board_shape",+    "altium_board_summary",     "altium_build_footprint",     "altium_build_symbol_modes",     "altium_check_dialogs",     "altium_cleanup_libs",     "altium_clone_symbol",     "altium_close",+    "altium_close_document",     "altium_compile",     "altium_compile_intlib",     "altium_component_data",@@ -69,11 +73,13 @@     "altium_extension_ping",     "altium_extract_intlib",     "altium_footprint_primitives",+    "altium_foreground",     "altium_import_kicad",     "altium_import_part",     "altium_install_extension",     "altium_install_library",     "altium_install_library_bundle",+    "altium_install_receipt",     "altium_instances",     "altium_launch",     "altium_layers",@@ -86,6 +92,7 @@     "altium_list_nets",     "altium_net_connections",     "altium_normalize_bodies",+    "altium_plate_vias",     "altium_open_document",     "altium_open_project",     "altium_pcb_report",@@ -102,6 +109,7 @@     "altium_rename_footprint",     "altium_rename_symbol",     "altium_review_board",+    "altium_rules_by_kind",     "altium_run_command",     "altium_run_drc",     "altium_run_erc",@@ -110,6 +118,7 @@     "altium_save",     "altium_schematic_data",     "altium_set_layer_visibility",+    "altium_set_models",     "altium_set_params",     "altium_set_visibility",     "altium_setup_trial",@@ -127,6 +136,7 @@     "altium_status",     "altium_swap_footprint",     "altium_symbol_mode",+    "altium_symbol_prims",     "altium_test_board",     "altium_uninstall",     "altium_window_info",@@ -135,6 +145,7 @@   "statusVerb": "altium_status",   "uninstall": "altium_uninstall",   "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/altium-bridge/files/adom-bridge-altium-manifest.json",+  "insidersManifestUrl": "https://wiki.adom.inc/api/v1/pages/insiders/files/altium-bridge/version.json",   "timeouts": {     "default": 60,     "verbs": {@@ -151,7 +162,10 @@       "import_kicad": 300,       "compile_intlib": 300,       "extract_intlib": 300,-      "install_extension": 200+      "install_extension": 200,+      "install_library_bundle": 330,+      "install_library": 150,+      "install_receipt": 10     },     "prefixes": [       {@@ -166,6 +180,7 @@   },   "verbDurations": {     "altium_launch": 60,+    "altium_install_library_bundle": 30,     "altium_agent_start": 8,     "altium_run_outjob": 60,     "altium_run_drc": 30
dev-skills/altium-bridge-dev/SKILL.md+14−1
@@ -75,6 +75,19 @@ Warning. When you add a mutating command, wrap file reads and never leave a tran inside a brace comment (breaks compilation). An unknown identifier is a RUNTIME error that halts the debugger; `altium_agent_recover` clears it. +**From 0.2.21 a halt is never silent.** The standing reaper photographs a lone-OK `Error` box before+pressing it (`dialogs::script_error_since`); `agent::call` polls that every 400 ms while it waits, and+on a hit runs `lifecycle::recover` itself (`EditScript:Stop`) and returns `errorCode:+agent_script_error` with `errorShot` (the PNG: Altium paints the text, so the picture is the only+copy), `autoRecovered` and `recoverSteps`. Before that, the reaper pressed OK, the script sat on a+breakpoint, every verb timed out and Altium refused to close ("cannot close the current document+when a script is stopped on a breakpoint"). Also: `ISch_Component.ImplementationCount` /+`.Implementation[i]` do not exist on AD 26.9.1 (walk models with a `SchIterator_Create` ++`AddFilter_ObjectSet(MkSet(eImplementation))`). Every identifier in this section that a regex can+recognise is in `scripts/release.py` `ENGINE_HALTING`, and a build that contains one is refused:+add to that list whenever you find another. Test new agent commands on a SECOND Altium build+before promoting; one machine proved nothing about 26.9.1.+ ## The brace rule for JSON-building code  Result strings build JSON with `'{' + ... + '}'`; that's fine in a STRING LITERAL. It is only@@ -120,7 +133,7 @@ locked Windows session (DirectX) — check `desktop_get_foreground` first. Decision (John): the bridge PROCESS moves to a Rust exe that (1) calls ab's verbs for everything desktop-shaped (no Win32 in the bridge), (2) absorbs the DelphiScript agent's shaping logic so the agent shrinks to generic read/apply primitives, (3) ships no MCP server (external agents use-`adom-bridge-cli`). Vertical slice VERIFIED live on AdomLapper: `/health` (Altium detected through+`adom-bridge-cli`). Vertical slice VERIFIED live on a Windows test machine: `/health` (Altium detected through `desktop_find_window`, title parsed incl. the ADDevelop shape), `/verbs`, `/command` for status, eval, run_command, open_document and the 11 Phase-1 introspection verbs — speaking the existing agent's file-IPC protocol unchanged. Build: cross-compile from the container (`rustup` + target
install.sh

  
page.json+1−1
@@ -140,4 +140,4 @@     "skills/altium-wiki-import/SKILL.md",     "uninstall.sh"   ]-}\ No newline at end of file+}
scripts/release.py+28
@@ -108,6 +108,32 @@ def lint_versions(ver):         raise SystemExit(f"bridge.json insidersManifestUrl must be {INSIDERS_URL} (#9)")  +# Identifiers MEASURED to halt Altium's DelphiScript engine (an unknown identifier is a RUNTIME+# error: an 'Error' box, then the script parks on a breakpoint and Altium will not even close).+# dev-skills/altium-bridge-dev lists how each was found. Add to this list whenever one is found;+# a build that contains one is refused, because the only other way to find it is a user's Altium.+ENGINE_HALTING = [+    r"\.ImplementationCount\b", r"\.Implementation\[", r"\bBottomXSize\b", r"\bMidXSize\b",+    r"\beProcessFreePrimitives\b",   # (IPCB_Board.GroupIterator_Create also halts, but the same call is+    # correct on a component/footprint and a regex cannot see the receiver: review those by hand)+    r"\bRunProcessWithParameters\b", r"\.SuperClass\s*:=", r"\bCreateOleObject\b",+]+++def lint_agent_script():+    """Refuse an agent template that uses an identifier known to halt the engine."""+    src = (SRC / "agent" / "AdomAgent.pas.tmpl").read_text(encoding="utf-8", errors="replace")+    code = re.sub(r"\{[^}]*\}", "", src)            # brace comments may NAME the identifier+    code = re.sub(r"//[^\n]*", "", code)+    code = re.sub(r"'[^'\n]*'", "''", code)          # and so may string literals+    hits = []+    for pat in ENGINE_HALTING:+        for m in re.finditer(pat, code):+            hits.append(f"{pat}  ->  ...{code[max(0, m.start() - 40):m.end() + 20].strip()}...")+    if hits:+        raise SystemExit("agent template uses engine-halting identifiers (see ENGINE_HALTING):\n  " + "\n  ".join(hits))++ def build_exe():     subprocess.run(["cargo", "test", "--quiet"], cwd=RS, check=True)     subprocess.run(["cargo", "build", "--release", "--target", TARGET], cwd=RS, check=True)@@ -167,6 +193,7 @@ def two_curl_split_check(): def ship_insiders(log_line):     ver = json.loads((SRC / "bridge.json").read_text())["version"]     lint_versions(ver)+    lint_agent_script()     n = lint_verbs()     print(f"verb lint ok: {n} verbs declared and dispatchable")     build_exe()@@ -298,6 +325,7 @@ def main():     else:         ver = json.loads((SRC / "bridge.json").read_text())["version"]         lint_versions(ver)+        lint_agent_script()         n = lint_verbs()         print(f"verb lint ok: {n} verbs")         build_exe()
src/agent/AdomAgent.pas.tmpl+16−4
@@ -1638,12 +1638,18 @@ Begin     Models := '';     If Param(Lines, 'models') = '1' Then     Begin-        For I := 0 To Comp.ImplementationCount - 1 Do+        { ISch_Component has no ImplementationCount/Implementation[] in DelphiScript on every AD+          build (26.9.1 halts the script there); the eImplementation child iterator works on all. }+        Iter := Comp.SchIterator_Create;+        Iter.AddFilter_ObjectSet(MkSet(eImplementation));+        Impl := Iter.FirstSchObject;+        While Impl <> Nil Do         Begin-            Impl := Comp.Implementation[I];             If Models <> '' Then Models := Models + ',';             Models := Models + '"' + Impl.ModelType + ':' + Impl.ModelName + '"';+            Impl := Iter.NextSchObject;         End;+        Comp.SchIterator_Destroy(Iter);     End;     Result := '{' + JRaw('ok', 'true') + ',' + JStr('libref', Comp.LibReference) + ',' +         JRaw('pins', IntToStr(NPin)) + ',' + JRaw('rectangles', IntToStr(NRect)) + ',' + JRaw('lines', IntToStr(NLine)) + ',' +@@ -1691,9 +1697,15 @@ Begin         Exit;     End;     Removed := 0;-    While Comp.ImplementationCount > 0 Do+    { Same rule as CmdSymbolPrims: walk implementations with the eImplementation iterator. One+      iterator per removal, because removing under a live iterator invalidates it. }+    While Removed < 64 Do     Begin-        Impl := Comp.Implementation[0];+        Iter := Comp.SchIterator_Create;+        Iter.AddFilter_ObjectSet(MkSet(eImplementation));+        Impl := Iter.FirstSchObject;+        Comp.SchIterator_Destroy(Iter);+        If Impl = Nil Then Break;         Comp.RemoveSchImplementation(Impl);         Removed := Removed + 1;     End;
src/caller_identity.py+1−1
@@ -32,7 +32,7 @@ def set_from_request(headers, args) -> dict:       * INBOUND relay (Bridge -> bridge): the CLI's --ai-thread flag and any explicit         `caller` in args arrive as `args["caller"] = {"aiThread", "containerName",         "source"}`. This is what actually shows up in practice (verified on-        AdomLapper 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers.+        ab 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers.       * Header form (X-Adom-Caller-*): the shape Bridge uses on its OWN direct-API         callbacks and what the SDK examples forward. Read it as a fallback so we         also work if a future Bridge stamps the relay path with headers.
src/handlers/png_stdlib.py+1−1
@@ -3,7 +3,7 @@ Why: ab's managed Python runtime (adom-runtimes/python-3.12.13, ab 2.0.44+) is stdlib-only — no PIL. On a fresh user box every screenshot fell back to .bmp, which kicad_state refuses ("capture failed (non-png fallback?)"), so a brand-new-install had NO working captures (found on ConfRoomROG, 2026-08-15). The bridge's+install had NO working captures (found on a second test machine, 2026-08-15). The bridge's own dependency policy is stdlib-only; the screenshot path now honors it.  Input is what GetDIBits hands us: 32bpp BGRA, bottom-up. Output is an 8-bit RGB
src/handlers/progress.py+1−1
@@ -40,7 +40,7 @@ _lock = threading.Lock() # Shipped defaults, used ONLY to seed a machine with no history of its own. # Every one of these is a measured figure from this fleet, not a guess. _DEFAULT_SECONDS = {-    "cold_start": 60.0,       # X2.exe from nothing (AD26, measured once on AdomLapper — refine)+    "cold_start": 60.0,       # X2.exe from nothing (AD26, measured once — refine)     "warm_start": 1.0,        # Altium already running     "agent_start": 8.0,       # bootstrapping the resident DelphiScript agent     "show_symbol": 12.0,
src/handlers/uia.py+1−1
@@ -1,6 +1,6 @@ """UI Automation helpers — the QUIET way to drive Altium. -Measured 2026-09-01 on AD26.9.1 (John's box), after John asked "you keep turning on the cursor,+Measured 2026-09-01 on AD26.9.1 (test machine), after John asked "you keep turning on the cursor, but you could maybe use UIA to do those clicks":  Altium exposes a real UIA tree, and far more of it than this bridge was using:
src/server.py+2−2
@@ -1110,7 +1110,7 @@ def h_a365_configure(args):         if args.get(k):             cfg[k] = args[k]     if not cfg.get("workspace"):-        return {"success": False, "error": "workspace is required, e.g. 'john-john-3.365.altium.com'"}+        return {"success": False, "error": "workspace is required, e.g. 'acme.365.altium.com'"}     _a365_save(cfg)     return {"success": True, "workspace": cfg["workspace"], "hasToken": bool(cfg.get("token")),             "_hint": "altium_a365_status to verify; altium_a365_query {query} to run GraphQL."}@@ -1194,7 +1194,7 @@ def h_a365_oauth_begin(args):     workspace = args.get("workspace") or cfg.get("workspace")     if not workspace:         return {"success": False, "errorCode": "a365_workspace_required",-                "error": "workspace is required, e.g. john-john-3.365.altium.com"}+                "error": "workspace is required, e.g. acme.365.altium.com"}     with _a365_oauth_lock:         if _a365_oauth.get("status") == "waiting":             return {"success": False, "errorCode": "a365_oauth_in_progress",
uninstall.sh