← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.261\ No newline at end of file+1.9.262\ No newline at end of file
CLAUDE.md+16−11
@@ -5,11 +5,16 @@ Fusion, **you own and maintain this code.** You write it, version it, publish it Stop treating bridge problems as someone else's. (This file exists because an AI repeatedly forgot all of this and filed its own bridge bugs as "adom-desktop" requests. Don't.) -## You vs adom-bridge (AD) core - the boundary+## You vs adom-bridge (ab) core - the boundary -- **AD core owns:** the `adom-bridge` CLI, the relay / command passthrough, bridge lifecycle+**Naming (John, 2026-08-29):** the host app is **Adom Bridge, "ab"**. "Adom Desktop" and "AD" are the+retired names; do not use them in prose, hints, skills or issues. Identifiers such as `_ad_call`,+`ad_client.py` and the repo folder keep their old spelling on purpose (renaming buys nothing and risks+breakage).++- **ab core owns:** the `adom-bridge` CLI, the relay / command passthrough, bridge lifecycle   (`bridge_install` / streaming the bridge from the wiki into `…\bridges-cache\fusion360`), and the-  core `desktop_*` verbs. That's it. AD just **hosts and loads** this bridge.+  core `desktop_*` verbs. That's it. ab just **hosts and loads** this bridge. - **YOU (this bridge) own:** every `fusion_*` verb and its response `_hint`s, the **APS** integration   (auth/config/quota - see below), the **Fusion add-in** code AND **deploying it into Fusion**, the   `SKILL.md` + bundled `skills/`, and the edit → publish → reinstall loop.@@ -60,17 +65,17 @@ page's own issues (ab watches those).  ## Map of what you manage -- `server.py` - bridge server / verb router (talks to AD's relay, proxies to the add-in). **On start+- `server.py` - bridge server / verb router (talks to ab's relay, proxies to the add-in). **On start   it calls `install_addin()`** to deploy the add-in (see deploy below). - `aps.py` - the **entire APS** implementation: PKCE OAuth, token store, server-indexed cloud search,   never-charge cap. **All APS is yours** (the `fusion_aps_*` verbs + `fusion_search_cloud_files` /-  `fusion_walk_cloud_tree`). NOTE: port **8910 is AD's vestigial "native" APS** - NOT yours; this-  bridge uses **8917/8918/8920** and steers around it. The bare `aps_status` (8910) is that AD stub,+  `fusion_walk_cloud_tree`). NOTE: port **8910 is ab's vestigial "native" APS** - NOT yours; this+  bridge uses **8917/8918/8920** and steers around it. The bare `aps_status` (8910) is that ab stub,   not this bridge's `fusion_aps_status`. - `addin/AdomBridge/` - the add-in that runs **inside Fusion**: `AdomBridge.py`, `http_server.py`,   `commands/*.py` (cloud_documents, open_electronics_file, electronics, export, modeling,   manufacturing, screenshot, document_info, parameters, …).-- `describe.py` - the `fusion_describe` self-describe catalog (AD's Verbs tab).+- `describe.py` - the `fusion_describe` self-describe catalog (ab's Verbs tab). - `handlers/` - close / recovery / dialog-classify / UI / open_design. - `skills/` - bundled skills you ship: `fusion-aps-search`, `fusion-aps-signin`, `fusion-bridge-dev`,   `fusion-onboarding`. `resources/` - JLCPCB CAM jobs, Adom DRU rules, the layer-detect ULP.@@ -92,7 +97,7 @@ fusion_start); runOnStartup does the rest. The exact, watched-it-happen release 1. **`bridge_install {"manifestUrl": ".../adom-bridge-fusion-manifest.json"}`** - streams the new zip    into the CACHE via `method: in_place_merge`. Updates the cache ONLY; it does NOT touch Fusion's    AddIns dir. (Pass `"force": true` to re-merge when the version already bumped.)-   Do NOT manually `bridge_kill` - `bridge_install`'s own output says "AD reaps + respawns it from the+   Do NOT manually `bridge_kill` - `bridge_install`'s own output says "ab reaps + respawns it from the    new cache on the next call (no manual kill needed)," so the bridge SERVER (new server.py/describe.py)    reloads automatically on the next verb. (Running `bridge_kill` here is pointless.) 2. **`fusion_stop`** (graceful; `fusion_kill` if it is wedged) - REQUIRED. Fusion holds the add-in files@@ -101,13 +106,13 @@ fusion_start); runOnStartup does the rest. The exact, watched-it-happen release 3. Deploy the add-in into Roaming. Two ways:    - **(intended)** Run the cache's `install_addin.py`: `shell_execute` -> `cd <cache>\fusion360 &&      python install_addin.py`. It prints `Updated: commands\cloud_documents.py ...`.-   - **⚠️ TRAP (hit 2026-06-29): `python`/`py` are NOT on the box's shell PATH.** AD spawns the bridge+   - **⚠️ TRAP (hit 2026-06-29): `python`/`py` are NOT on the box's shell PATH.** ab spawns the bridge      with its OWN python, so `python install_addin.py` via `shell_execute` returns "Python was not      found" / errors with empty output (same Windows-Store-alias stub that makes a bridge fail to-     respawn). (AD >=1.9.63 PROVISIONS Python itself - a system install if one is on PATH, else a+     respawn). (ab >=1.9.63 PROVISIONS Python itself - a system install if one is on PATH, else a      portable no-UAC python-build-standalone copy under `~/.adom/adom-runtimes/python-<ver>/`, pinned      to 3.12.13 - and spawns `server.py` by ABSOLUTE PATH from it. So the bridge-SPAWN case is fully-     handled by AD; NEVER bootstrap/download your own Python. Check runtime state anytime with AD's+     handled by ab; NEVER bootstrap/download your own Python. Check runtime state anytime with ab's      `runtimes` verb (`state ∈ absent|installing|ready|failed`). But a bare `python` you invoke      yourself via `shell_execute` still won't resolve, so the file-copy deploy below remains the way.)      `cmd /c copy` and `robocopy` also fight you on the spaces in `Adom Bridge` / `Autodesk
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.261",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.261/adom-bridge-fusion-v1.9.261.zip",-  "sha256": "266bcc4dc5a62f4dd5a72da0a08315b74365c31eb73b3f9e079aa9ef6901c079",-  "size": 554558,+  "version": "1.9.262",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.262/adom-bridge-fusion-v1.9.262.zip",+  "sha256": "322d4aee01e4522fed0ce6adeda757b7b353142b4435f10536118d428ba164b0",+  "size": 554511,   "verbPrefixes": [     "fusion_"   ],
aps.py+5−5
@@ -45,12 +45,12 @@ APS_BASE = "https://developer.api.autodesk.com" AUTH_URL = f"{APS_BASE}/authentication/v2/authorize" TOKEN_URL = f"{APS_BASE}/authentication/v2/token" -# Fixed callback — MUST match the registered APS app exactly. 8910 is AD-native+# Fixed callback — MUST match the registered APS app exactly. 8910 is ab-native # APS's port; the bridge uses its own so the two never collide. # Fixed callback ports — tried in order at sign-in; the FIRST free one is bound. # All of these (× localhost AND 127.0.0.1) are registered on the APS app, so a # port collision on any user's machine just falls through to the next — no failed-# sign-ins. 8917 avoids AD's own APS on 8910; all verified free + outside Windows'+# sign-ins. 8917 avoids ab's own APS on 8910; all verified free + outside Windows' # reserved/excluded ranges (5357, 27339, 50000-50059) on a real box. REDIRECT_PORTS = [8917, 8918, 8920] REDIRECT_PATH = "/fusion-aps/callback"@@ -84,7 +84,7 @@ APS_REQUIRED_HINT = (     "⛔ NEVER enumerate cloud files via app.data (dataProjects/dataFiles) in fusion_run_modeling_script: "     "the DEPRECATED method holds Fusion's main thread for minutes, times out, and locks the app. "     "AUTO-RECOVER APS instead: (1) NOT configured? The org Client ID is a PUBLIC PKCE id - pull it from a "-    "peer that has it: read ~/.adom/fusion-aps/config.json (key 'client_id') off another AD desktop (adom-"+    "peer that has it: read ~/.adom/fusion-aps/config.json (key 'client_id') off another ab desktop (adom-"     "desktop status lists them) via shell_execute/pull_file, then fusion_aps_set_client_id {clientId}. "     "(2) fusion_aps_signin -> open the returned authUrl in the user's REAL browser with a warm Autodesk "     "session; INSTALL + USE the adom-browser-extension (nbrowser/ABE, https://wiki.adom.inc/adom/adom-"@@ -252,7 +252,7 @@ _ABE_UPSELL = ("OPTIONAL, offer it: installing the Adom browser extension lets m   def _ad(verb: str, args: dict, timeout: int = 60) -> dict:-    """Call an AD verb from aps.py. Lazy-imports server (which imports us) so there is no+    """Call an ab verb from aps.py. Lazy-imports server (which imports us) so there is no     circular import at module load. Never raises."""     try:         import server as _s@@ -793,7 +793,7 @@ def _aps_user_info() -> dict:   def handle_status(args: dict) -> dict:-    # NOTE: AD's relay only forwards output/data/success/error/_hint to the+    # NOTE: ab's relay only forwards output/data/success/error/_hint to the     # cloud caller — every structured field MUST live under `data`.     client_id = get_client_id()     tok = _load_tokens()
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.261",+  "version": "1.9.262",   "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.",   "homepage": "https://wiki.adom.inc/adom/fusion-bridge",   "author": "Adom Inc.",
dashboard/Cargo.lock+1−1
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "fusion-dashboard"-version = "1.9.261"+version = "1.9.262" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.261"+version = "1.9.262" edition = "2021"  [dependencies]
describe.py+11−11
@@ -1,9 +1,9 @@-"""fusion_describe — self-describe the bridge's verb surface for Adom Desktop.+"""fusion_describe — self-describe the bridge's verb surface for Adom Bridge. -AD v1.9.23+ added a Verbs tab to each bridge's detail pop-out (lists verbs,-expands each for its I/O + timeout + status schema, with an inline runner). AD+ab v1.9.23+ added a Verbs tab to each bridge's detail pop-out (lists verbs,+expands each for its I/O + timeout + status schema, with an inline runner). ab does NOT hardcode a cloud-owned bridge's verbs, so the bridge must self-describe.-AD POSTs {command:"describe", args:{}} and caches the result at ~/.adom/bridge-verbs/.+ab POSTs {command:"describe", args:{}} and caches the result at ~/.adom/bridge-verbs/.  Each entry: name, summary, input, output, timeoutSeconds, statusVerb, longRunning, example. Keep this in step with the actual handlers as verbs are added.@@ -27,7 +27,7 @@ _T = [      {"hwnd": "required int (the dialog window)"}, {"success": "bool"}, 30, None, False, {"hwnd": 123456}),     ("fusion_get_app_state", "Current app state: running, active document, workspace, isElectronics. The poll/status verb for long-running calls.",      {}, {"activeDocument": "str", "activeWorkspace": "str", "isElectronics": "bool"}, 20, None, False, {}),-    ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? AD DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (AD >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with AD's bridge_readiness.",+    ("fusion_readiness", "FAST readiness check (does NOT launch Fusion): is the Fusion host app installed + running + the bridge ready to drive? ab DETECTS Fusion (never installs it) via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47). Call this before driving Fusion; if installed-but-not-running, fusion_start. SELF-HEALS the seat/licensing dialog (background UIA + screenshot-verify) whenever it detects one. Pairs with ab's bridge_readiness.",      {}, {"installed": "bool", "running": "bool", "ready": "bool", "hostApp": "str", "bridgeVersion": "str"}, 15, None, False, {}),      ("fusion_new_electronics_from_eagle", "Import a legacy EAGLE .sch (+ paired .brd) into a NEW Fusion electronics design so the parts INSTANTIATE (schematic + populated board, lands in the PCB editor). The ONLY way to author a board from EAGLE source: Document.newDesignFromLocal opens the schematic editor but does NOT instantiate parts, and the Fusion-native .fsch/.fbrd container is opaque binary you can't build offline. This fires Fusion's own ImportSCHAndBRDCmd and drives BOTH native Open dialogs (the .sch then the .brd) in the BACKGROUND via desktop_ui_click by accessible name (no foreground). Stage both files to Windows first (send_files). After: fusion_show_2d_board, RATSNEST + 'AUTO ;' to autoroute, fusion_show_3d_board for 3D.",@@ -35,7 +35,7 @@ _T = [      {"success": "bool", "imported": "list"}, 200, None, False,      {"schPath": "C:/Users/me/adom-lib/MyBoard.sch", "brdPath": "C:/Users/me/adom-lib/MyBoard.brd"}), -    ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-AD to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.",+    ("fusion_notify_owner", "LAST-RESORT human escalation: toast the user's MAIN computer (fans out cross-ab to every peer on the relay, so it reaches them even when this bridge runs on an unattended VM). ONLY for true human walls - password/2FA entry, UAC elevation, a physical step. Exhaust programmatic options first (UIA background clicks, seat auto-resolve, warm-SSO sign-in). After sending, WAIT and poll fusion_readiness; do not re-toast within a few minutes.",      {"title": "optional str", "body": "required str", "level": "optional str info|warning|error"},      {"success": "bool", "targets": "list"}, 20, None, False,      {"title": "Fusion needs you", "body": "Please complete the Autodesk 2FA prompt on winvm - everything else is done."}),@@ -138,7 +138,7 @@ _T = [     ("fusion_attach_3d_package", "LOW-LEVEL primitive: attaches ONE model to ONE package. Prefer fusion_create_library with parts[]. Attach a real 3D chip model to a library package, end to end: opens the .lbr (library active), runs Electron.Create3DPackage to enter the Package3DEnvironment showing the footprint, imports the STEP + auto-orients it flat on the footprint, then executes Package3DStop (FINISH). This BINDS the 3D onto the deviceset (Content Manager shows the 3D nested under the package; the Packages 'Package' column flips Placeholder->part-name). ONE desktop-side step remains: Fusion pops a modal Save dialog (an OWNED popup) - the response _hint gives the exact desktop_ui_click to confirm it. AFTER: re-grab desktop_screenshot_window and check ownedPopupCount for errors; the 3D preview LAGS a beat before showing the real chip. Both paths (filePath/modelPath) are WINDOWS paths - stage from the container first.",      {"filePath": "required str (.lbr WINDOWS path C:/...)", "modelPath": "required str (.step WINDOWS path C:/...)", "packageName": "optional str"},      {"success": "bool", "savePending": "bool", "data": "{steps}", "_hint": "the Save-dialog click + verify steps"}, 240, _STATUS, True, {"filePath": "C:/Users/<user>/adom-lib/ADS8588SIPM.fusion.lbr", "modelPath": "C:/Users/<user>/adom-lib/ADS8588SIPM.step", "packageName": "ADS8588SIPM"}),-    ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO AD approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so AD's bridge_readiness recommends it.",+    ("fusion_install_fusion", "Install Fusion 360 FOR the user - downloads Autodesk's official Client Downloader and streams the free trial silently, with NO shell_execute and NO ab approval gate (runs inside the trusted bridge). Elevation-aware: --globalinstall when admin, PER-USER otherwise (a non-elevated globalinstall dies silently on UAC - learned live). Returns promptly; poll fusion_readiness (reports installing:true) until installed:true, then fusion_start. Declared as detect.installVerb so ab's bridge_readiness recommends it.",      {}, {"success": "bool", "installing": "bool", "alreadyInstalled": "bool?", "mode": "globalinstall|per-user", "statusVerb": "str"}, 90, _STATUS, True, {}),     ("fusion_generate_package", "Generate an IPC-7351-compliant parametric 3D package via Fusion's BUILT-IN ElectronicsPackageGenerator (EPG) - ~50 package families (chip/soic/qfn/qfp/bga/sot23/dfn/melf/ecap/crystal/headers/...), each a few seconds, ZERO GUI. Optionally LASER-ETCH a marking (MPN / '103') into the body top as REAL cut geometry (survives STEP), and export STEP in the same call - the royalty-free, license-clean path to a whole component library (our own generated output, not vendor models). Proven live: 0603 + '103' etch + STEP in ~2s.",      {"type": "required str (EPG family, e.g. chip|soic|qfn|qfp|bga|sot23|dfn2|melf|ecap|crystal|header_straight - see supportedTypes in the error for the full list)", "params": "optional dict (generator dims in MM by default, e.g. chip: {D,E,A,L,L1}; soic: {A,A1,b,D,E,E1,e,L,DPins}; omitted keys use EPG defaults)", "unitsCm": "optional bool (params already EPG-native cm)", "etch": "optional str (marking text on the chip top: MPN / '103'; multi-line with newline, e.g. MPN+variant - laid along the LONGEST face axis)", "etchStyle": "optional str raised|engraved (default raised = thin WHITE positive extrude, silkscreen-style contrast; engraved = sunken cut)", "etchDepthMm": "optional float (marking height/depth, default 0.03)", "etchHeightMm": "optional float (text height; default auto-fit: top-5%-band face, 10% margin, width-aware)", "outputStep": "optional str (WINDOWS path; exports STEP incl. the marking)"},@@ -182,7 +182,7 @@ _T = [     ("fusion_import_step", "Import a STEP/.stp (or other native-importable) file into a NEW Fusion design via the native import API. The open-electronics hints redirect .step here. (Was implemented but missing from this catalog until the verb suite's reverse-drift check caught it, 2026-08-14.)", {"filePath": "required str (WINDOWS path C:/...; stage from container first)"}, {"success": "bool", "documentName": "str"}, 300, None, True, {"filePath": "C:/Temp/part.step"}),      # ── Catalog completion (2026-08-14): 14 verbs that were LIVE but undescribed, caught by-    # the verb suite's reverse drift check. An undescribed verb is invisible to AD's Verbs+    # the verb suite's reverse drift check. An undescribed verb is invisible to ab's Verbs     # tab and to every caller reading fusion_describe - never let these drift again.     ("fusion_open_cloud_file", "Open a CLOUD file by name (the front door to every design flow). PCB/ELECTRONICS NAV RULE: open the electronics DESIGN document (EcadDesignProductType), never a child .brd/.sch/3D directly - then switch views with fusion_show_schematic / fusion_show_2d_board / fusion_show_3d_board. LONG-RUNNING: fire it, then poll fusion_get_app_state until activeDocument matches (cloud opens on electronics designs take 1-3 min). A crash-recovery modal can block the open - fusion_dismiss_blocking_dialogs.", {"fileName": "required str", "projectName": "optional str (defaults to active project)", "folderPath": "optional str"}, {"success": "bool", "documentName": "str"}, 240, "fusion_get_app_state", True, {"fileName": "BQ25792", "projectName": "Main"}),     ("fusion_open_design", "Open a LOCAL design file (routes through the add-in import). Prefer cloud flows (fusion_open_cloud_file) for team designs; this is for local .f3d/.step style files.", {"filePath": "required str (WINDOWS path)"}, {"success": "bool"}, 240, None, True, {"filePath": "C:/Temp/part.f3d"}),@@ -296,7 +296,7 @@ _T = [      60, None, False, {"dryRun": True}),      # ── Self-describe ──────────────────────────────────────────────────────-    ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for AD's Verbs tab + runner.",+    ("fusion_describe", "Self-describe every verb the bridge exposes (this list) for ab's Verbs tab + runner.",      {}, {"success": "bool", "verbs": "[{name,summary,input,output,timeoutSeconds,statusVerb,longRunning,example}]"}, 20, None, False, {}), ] @@ -321,8 +321,8 @@ def _verbs():  def handle_describe(args: dict) -> dict:     verbs = _verbs()-    # `verbs` at the TOP LEVEL is what AD's Verbs tab reads from the raw /command-    # response. AD's relay strips non-standard top-level fields on the CLI proxy+    # `verbs` at the TOP LEVEL is what ab's Verbs tab reads from the raw /command+    # response. ab's relay strips non-standard top-level fields on the CLI proxy     # path, so we ALSO nest under `data` (which the relay forwards) for CLI/debug.     return {         "success": True,
fusion_detect.py+4−4
@@ -72,7 +72,7 @@ _callbacks = [] # Guard windll so this module imports on non-Windows hosts (galliaApril smoke # tests, macOS). The Win32-only functions below are never reached off-Windows — # detect_fusion() short-circuits to installed:False — but the bridge must still-# boot + serve /status on any platform AD spawns it on.+# boot + serve /status on any platform ab spawns it on. user32 = ctypes.windll.user32 if hasattr(ctypes, "windll") else None  @@ -332,7 +332,7 @@ def _has_fusion_process() -> bool:         creationflags = 0x08000000 if hasattr(subprocess, "CREATE_NO_WINDOW") else 0         if hasattr(subprocess, "CREATE_NO_WINDOW"):             creationflags = subprocess.CREATE_NO_WINDOW-        # stdin=DEVNULL is load-bearing: AD spawns the bridge console-less ++        # stdin=DEVNULL is load-bearing: ab spawns the bridge console-less +         # detached, so the inherited stdin handle is invalid. Without an explicit         # DEVNULL, this child can block on a dead handle and deadlock the probe —         # the exact failure mode that bit the KiCad bridge.@@ -786,7 +786,7 @@ def _popen_fusion_background(exe_path: str):     ⚠ THE #50 ROOT CAUSE (proven by process parentage on AdomLapper, 2026-08-23):     Fusion360.exe <- FusionLauncher.exe <- python (this bridge) <- adom-bridge.exe.     Fusion was a CHILD of the bridge, so every bridge reap - each `bridge_install` of a-    release, AD's supervisor respawn (adom-bridge #61), the idle reaper - killed Fusion+    release, ab's supervisor respawn (adom-bridge #61), the idle reaper - killed Fusion     with it. Seven releases shipped during a 20-part build matrix = seven dead Fusions,     every one mid-chain, every one a 'clean exit' with no crash artifact. @@ -1139,7 +1139,7 @@ def ensure_fusion_running(fusion_info: dict, wait_addin: bool = False) -> dict |         _set_min_animate(0)         # Launch MINIMIZED + NOT ACTIVATED (STARTUPINFO). DEVNULL on all three std         # streams + CREATE_NO_WINDOW: the bridge is a detached, console-less child of-        # AD, so inherited handles are invalid.+        # ab, so inherited handles are invalid.         _popen_fusion_background(exe_path)         # Reactive backstop via the refcounted envelope (minimize mode: splash/seat         # windows vanish rather than being demoted). The envelope restores the user's
handlers/taskbar_overlay.py+2−2
@@ -6,8 +6,8 @@ glance; the same signal belongs on FUSION's button whenever this bridge drives i THE LESSON (stolen from pup, who paid for it first - src/server.js): an in-bridge ITaskbarList3::SetOverlayIcon is IN-PROCESS-ONLY. The COM call returns S_OK cross-process and the shell silently ignores it (verified live here too: badge never-rendered). The working path is AD-core's `desktop_taskbar` verb (AD >=1.9.106): hand-it the badge PNG as base64 and AD paints the overlay natively, cross-process.+rendered). The working path is AD-core's `desktop_taskbar` verb (ab >=1.9.106): hand+it the badge PNG as base64 and ab paints the overlay natively, cross-process.      set:   desktop_taskbar {hwnd, overlay: {icon: <b64 png>, tooltip}}     clear: desktop_taskbar {hwnd, overlay: {badge: "none"}}
install.sh+1−1
@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.9.261"+DASH_VERSION="1.9.262" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.261",+  "version": "1.9.262",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {
page.json+11−8
@@ -1,6 +1,6 @@ {   "slug": "fusion-bridge",-  "version": "1.9.261",+  "version": "1.9.262",   "type": "app",   "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",   "dependencies": {@@ -11,6 +11,10 @@     "uninstall": "./uninstall.sh"   },   "tags": [+    "ab",+    "bridge",+    "fusion",+    "desktop-app",     "fusion360",     "autodesk",     "cad",@@ -75,7 +79,10 @@     "build a basic parts library",     "add a 3d model to a component",     "make a fusion 360 library with 3d",-    "set up the fusion bridge"+    "set up the fusion bridge",+    "fusion dashboard",+    "fusion status page",+    "open the fusion launch page"   ],   "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",   "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.",@@ -104,9 +111,5 @@       "label": "Part volume",       "prompt": "How much material does each printed part in this assembly use?"     }-  ],-  "author": {-    "name": "John Lauer",-    "email": "[email protected]"-  }-}\ No newline at end of file+  ]+}
server.py+71−71
@@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Adom Fusion 360 Bridge Server — localhost HTTP server for Fusion 360 integration. -Receives commands from the Adom Desktop (Tauri app) and controls+Receives commands from the Adom Bridge (Tauri app) and controls Fusion 360 via Win32 API, os.startfile, and the AdomBridge add-in.  Usage:@@ -98,8 +98,8 @@ def _addin_staleness(reported_version) -> dict: # Populated at startup fusion_info = None -# Caller identity: WHO we are currently acting for (issue #348). AD stamps X-Adom-Caller-* on-# every request it dispatches to us; we echo those back on our own AD callbacks so the user sees+# Caller identity: WHO we are currently acting for (issue #348). ab stamps X-Adom-Caller-* on+# every request it dispatches to us; we echo those back on our own ab callbacks so the user sees # "chip-fetcher tab 3 (via fusion)" instead of a nameless bridge. # # THREAD-LOCAL, deliberately. This is a ThreadingHTTPServer, so a module global would let@@ -121,7 +121,7 @@ def _get_caller_identity() -> dict:   def _stamp_caller(args: dict) -> dict:-    """Attribute an OUTGOING AD call (issue #348).+    """Attribute an OUTGOING ab call (issue #348).      Forwards the identity of the AI thread we are carrying out work for. When we act on our OWN     behalf (health poll, timer, cleanup sweep) nothing is bound to this thread, so we name@@ -148,7 +148,7 @@ def _reclaim_seat_from_peers() -> int:     John (2026-07-06): "if the user asked you to do something with fusion you should     just grab the license back. the user can always grab it back on the other machine,     so there's no harm." So on a seat conflict we do NOT ask - we free the seat at the-    SOURCE via the cross-AD direct API (fully background + reliable, no CEF-dialog+    SOURCE via the cross-ab direct API (fully background + reliable, no CEF-dialog     clicking), then relaunch locally with no conflict. Best-effort; returns the number     of peers signalled (0 if the direct API/peers are unavailable)."""     n = 0@@ -175,8 +175,8 @@ def _resolve_seat_via_uia(dlg: dict) -> bool:     user - unlike SendInput coordinate clicks (need foreground) or kill+relaunch (the     server keeps the seat). Proven live: `desktop_ui_click {hwnd, name:"Continue"}`     returned "Clicked in the BACKGROUND - the window did NOT come to the foreground"-    and the dialog cleared. The bridge calls the AD `desktop_ui_click` verb on ITSELF-    via the AD 1.9.84 direct API. Never raises."""+    and the dialog cleared. The bridge calls the ab `desktop_ui_click` verb on ITSELF+    via the ab 1.9.84 direct API. Never raises."""     try:         hwnd = dlg.get("hwnd")         if not hwnd or not ad_client.available():@@ -202,7 +202,7 @@ def _resolve_seat_via_uia(dlg: dict) -> bool:   def _owned_popup_count(main_hwnd) -> int:-    """ownedPopupCount on a window via AD's desktop_screenshot_window - the SAME+    """ownedPopupCount on a window via ab's desktop_screenshot_window - the SAME     parent/child screenshot signal John kept pointing at (2026-07-06): a modal seat/     error dialog shows up as an OWNED POPUP of the main Fusion window. This is the     ground-truth 'is a dialog still up' check used to VERIFY a seat dialog actually@@ -268,9 +268,9 @@ def _resolve_seat_dialog(max_clicks: int = 3) -> dict: def _find_adom_desktop_cli() -> str | None:     """Locate the host app's CLI so the bridge can drive it when the in-process ad_client is down. -    Accepts BOTH names. The host app was renamed Adom Desktop -> Adom Bridge in v2.0.0 (issue #537),+    Accepts BOTH names. The host app was renamed Adom Bridge -> Adom Bridge in v2.0.0 (issue #537),     so a box may carry either layout: a renamed install has "Adom Bridge"/adom-bridge-cli.exe, an-    older one still has "Adom Desktop"/adom-desktop-cli.exe. Looking for only one silently kills the+    older one still has "Adom Bridge"/adom-desktop-cli.exe. Looking for only one silently kills the     CLI fallback on the other, so we try new-first then old and let whichever exists win.      The hard rule survives the rename: shell the **-cli** exe only. Never the GUI exe (adom-bridge.exe@@ -279,7 +279,7 @@ def _find_adom_desktop_cli() -> str | None:     """     import shutil     la = os.environ.get("LOCALAPPDATA", "")-    apps = (("Adom Bridge", "adom-bridge-cli.exe"), ("Adom Desktop", "adom-desktop-cli.exe"))+    apps = (("Adom Bridge", "adom-bridge-cli.exe"), ("Adom Bridge", "adom-desktop-cli.exe"))     if la:         for folder, exe in apps:             for parent in (la, os.path.join(la, "Programs")):@@ -306,7 +306,7 @@ def _find_adom_desktop_cli() -> str | None:  def _cli_notify_all(title: str, body: str, level: str, buttons=None) -> dict:     """Deliver a toast to EVERY connected desktop via `adom-bridge --target all notify_user`. This is-    the reliable cross-AD path when the bridge's in-process ad_client is down (headless VM): the CLI+    the reliable cross-ab path when the bridge's in-process ad_client is down (headless VM): the CLI     joins the relay itself and `--target all` reaches the user's real machine (not just this VM).     Best-effort + never raises. Returns {delivered, targets, error}. (John 2026-07-14: the notify MUST     actually leave the box - a returned-but-unsent payload is the bug, not a feature.)"""@@ -332,7 +332,7 @@ def _cli_notify_all(title: str, body: str, level: str, buttons=None) -> dict:                     capture_output=True, text=True, timeout=30,                     creationflags=getattr(_sp, "CREATE_NO_WINDOW", 0))         out = (r.stdout or "") + (r.stderr or "")-        # AD's CLI returns {status:'ok', action:'displayed'} per target; treat a zero exit or a+        # ab's CLI returns {status:'ok', action:'displayed'} per target; treat a zero exit or a         # 'displayed'/'ok' in the output as delivered. The Windows exe may print nothing yet still         # deliver, so a clean exit code is accepted too.         delivered = (r.returncode == 0) or ("displayed" in out) or ('"status": "ok"' in out) or ("status':'ok" in out)@@ -541,7 +541,7 @@ def _addin_restart_info(fire_toast: bool = True) -> dict:   def _handle_notify_owner(args: dict) -> dict:-    """LAST-RESORT: toast the user's MAIN desktop (cross-AD) to ask for a human step.+    """LAST-RESORT: toast the user's MAIN desktop (cross-ab) to ask for a human step.      John's standing rule (2026-07-07): the bridge/AI does EVERYTHING itself; the only     legitimate uses are true human walls - password/2FA entry, UAC elevation, a physical@@ -552,7 +552,7 @@ def _handle_notify_owner(args: dict) -> dict:     body = args.get("body") or args.get("message") or "A human step is required to continue."     level = args.get("level") or "warning"     if not ad_client.available():-        # ad_client is the bridge's IN-PROCESS AD API - it is routinely UNAVAILABLE when this bridge+        # ad_client is the bridge's IN-PROCESS ab API - it is routinely UNAVAILABLE when this bridge         # runs on an unattended Hyper-V VM (found live 2026-07-14: the toast silently never reached the         # user, and the AI "forgot" to relay it - exactly the failure John demanded we engineer out).         # SELF-DELIVER via the adom-bridge CLI, which connects to the relay independently: `--target@@ -562,14 +562,14 @@ def _handle_notify_owner(args: dict) -> dict:         if cli.get("delivered"):             return {"success": True, "via": "cli:--target all", "targets": cli.get("targets"),                     "_hint": ("Toast fanned out to ALL connected desktops (the user's main machine "-                              "included) via the adom-bridge CLI, because the bridge's in-process AD "+                              "included) via the adom-bridge CLI, because the bridge's in-process ab "                               "API was unavailable (this VM). WAIT and poll fusion_readiness; do not "                               "re-toast within a few minutes. Only notify when the box is ACTUALLY "                               "ready for the user to act - do not toast for a step you can do yourself.")}         # CLI fallback also failed - return the payload so the AI can relay as the true last resort.         return {             "success": False,-            "error": "AD in-process API unavailable AND the adom-bridge CLI fallback failed: "+            "error": "ab in-process API unavailable AND the adom-bridge CLI fallback failed: "                      + str(cli.get("error"))[:200],             "notifyUser": {"title": title, "body": body, "level": level},             "_hint": ("Relay the notifyUser payload yourself: `adom-bridge --target all notify_user "@@ -588,7 +588,7 @@ def _handle_notify_owner(args: dict) -> dict:         "via": "ad_client:direct(all)",         "targets": reached,         "_hint": ("Toast fanned out to ALL connected desktops (the user's machine included) via the "-                  "in-process AD direct API. This is the LAST RESORT - only use after exhausting "+                  "in-process ab direct API. This is the LAST RESORT - only use after exhausting "                   "programmatic options (UIA background clicks, seat auto-resolve, warm-SSO sign-in), "                   "and only when the box is ACTUALLY ready for the user to act. Now WAIT and poll "                   "fusion_readiness for the state to clear; do not re-toast within a few minutes."),@@ -604,7 +604,7 @@ def _notify_before_foreground(reason: str) -> None:     Some Fusion interactions (SendInput key/click, CEF modal dialogs) can ONLY be driven     with Fusion in the FOREGROUND, which steals the user's focus mid-work. The standing     rule: ALWAYS drive in the background; foreground ONLY as a last resort. When we truly-    must, TELL the user why (via an AD notify, so every Adom user learns the principle) and+    must, TELL the user why (via an ab notify, so every Adom user learns the principle) and     rib Fusion for not being AI-native enough to allow it. Debounced so a burst of keystrokes     fires ONE notice, not dozens. Best-effort; never raises, never blocks the operation."""     try:@@ -649,7 +649,7 @@ def _handle_new_electronics_from_eagle(args: dict) -> dict:     THIS VERB is only for a legacy `.sch` whose parts must be instantiated by Fusion's own     importer: `Document.newDesignFromLocal <file.sch>` opens the schematic EDITOR but does NOT     instantiate parts (board_info == 0); Fusion's REAL EAGLE importer is `ImportSCHAndBRDCmd`,-    which pops the two Open dialogs. This verb drives them via the AD direct API when present -+    which pops the two Open dialogs. This verb drives them via the ab direct API when present -     but that FOREGROUNDS Fusion, so use the `.brd`+`fusion_open_board` route whenever you can     place the parts yourself. The native `.fsch/.fbrd` container is opaque BINARY, un-authorable. @@ -664,11 +664,11 @@ def _handle_new_electronics_from_eagle(args: dict) -> dict:                          "to Windows (send_files), then call with {schPath, brdPath}."}     brd = (args.get("brdPath") or (sch[:-4] + ".brd")).replace("\\", "/")     if not ad_client.available():-        # Do NOT dead-end here (old bug: hard-failed even on latest AD when this bridge process-        # didn't get AD's injected direct-API env). The RIGHT move is the background route.+        # Do NOT dead-end here (old bug: hard-failed even on latest ab when this bridge process+        # didn't get ab's injected direct-API env). The RIGHT move is the background route.         brd_guess = (args.get("brdPath") or (sch[:-4] + ".brd")).replace("\\", "/")         return {"success": False, "errorCode": "use_open_board_instead",-                "error": "AD direct API unavailable - and this dialog-driven import FOREGROUNDS Fusion anyway.",+                "error": "ab direct API unavailable - and this dialog-driven import FOREGROUNDS Fusion anyway.",                 "_hint": ("Don't drive the Open dialogs. If the parts are placed in the .brd (elements + "                           "embedded library), call `fusion_open_board {filePath:\"" + brd_guess + "\"}` - it "                           "instantiates them via newDesignFromLocal with NO dialogs and NO foreground (the "@@ -891,7 +891,7 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:             "success": False,             "error": "Fusion 360 is not installed on this machine.",             "errorCode": "fusion_not_installed",-            "_hint": "Fusion 360 isn't installed. Do NOT tell the user to install it themselves - OFFER to install it FOR them and do it on a yes: the fusion-onboarding skill silent-installs Fusion + drives the Autodesk sign-in. (For a plain 'what EDA tools are installed' check, AD's bridge_readiness reports this cleanly without erroring.)",+            "_hint": "Fusion 360 isn't installed. Do NOT tell the user to install it themselves - OFFER to install it FOR them and do it on a yes: the fusion-onboarding skill silent-installs Fusion + drives the Autodesk sign-in. (For a plain 'what EDA tools are installed' check, ab's bridge_readiness reports this cleanly without erroring.)",         }      import time as _t0@@ -1020,7 +1020,7 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:     except Exception:         pass -    # Launch Fusion. AD's relay caps a single request at ~60s, but a FIRST launch+    # Launch Fusion. ab's relay caps a single request at ~60s, but a FIRST launch     # (fresh install: component downloads, updates, cloud sync) can take 2-4 min -     # far past the cap. So we DON'T block on the add-in for the whole launch (that     # dead-ended #63/Arav, and {"timeout":300} can't beat the relay cap). Instead:@@ -1065,7 +1065,7 @@ def _handle_launch(fusion_info: dict, args: dict) -> dict:     #    Detection is deterministic (find_licensing_dialog by owning process + size,     #    NOT the "Fusion360" title). No kill/relaunch (the server keeps the seat), no     #    notify (we handle it ourselves). ──-    _RELAY_BUDGET = 50  # keep total handler time under AD's ~60s relay cap+    _RELAY_BUDGET = 50  # keep total handler time under ab's ~60s relay cap     _deadline = _t.time() + max(6, int(_RELAY_BUDGET - (_t.time() - _start)))     _seat_clicks = 0     addin_ok = False@@ -1558,7 +1558,7 @@ def _handle_screenshot_fusion(fusion_info: dict, args: dict) -> dict:     else:         result = screenshot_fusion_window()     if result.get("success"):-        # This bridge natively names the shot `savedTo`/`sizeKB`/`dimensions`, but AD's CLI+        # This bridge natively names the shot `savedTo`/`sizeKB`/`dimensions`, but ab's CLI         # auto-pulls a screenshot into the CALLER'S container by keying on the ab-core field         # names (fullPath/safePath/fullPathHost/safePathHost + fullWidth/fullHeight). Without         # those, `savedTo` is a Windows-only path the container can't read and the shot looks@@ -1748,7 +1748,7 @@ def _handle_relocate_recovery(fusion_info: dict, args: dict) -> dict:  def _installer_running() -> bool:     """Is the Fusion Client Downloader / streamer currently installing? Bridge-side-    check (subprocess from OUR process = no AD shell-approval gate)."""+    check (subprocess from OUR process = no ab shell-approval gate)."""     try:         import subprocess as _sp         out = _sp.run(["tasklist", "/FO", "CSV", "/NH"], capture_output=True, text=True,@@ -1759,9 +1759,9 @@ def _installer_running() -> bool:   def _handle_install_fusion(fusion_info: dict, args: dict) -> dict:-    """Install Fusion 360 FOR the user - no shell_execute, no AD approval gate.+    """Install Fusion 360 FOR the user - no shell_execute, no ab approval gate. -    Declared as detect.installVerb (AD >=1.9.79) so bridge_readiness recommends+    Declared as detect.installVerb (ab >=1.9.79) so bridge_readiness recommends     THIS over the generic winget fallback. Downloads the official Fusion Client     Downloader and runs it silently: --globalinstall when elevated, PER-USER when     not (learned live: a non-admin shell makes --globalinstall die silently on@@ -1827,9 +1827,9 @@ def _aps_quick_state() -> dict:  def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     """FAST readiness check for the AI: is the Fusion host app present + running + the bridge ready?-    Does NOT launch Fusion (fast-fails when it's not running). AD DETECTS Fusion (never installs it)-    via bridge.json 'detect', and auto-installs Python if missing (AD >=1.9.47) - this verb just-    reports state. Pairs with AD's bridge_readiness."""+    Does NOT launch Fusion (fast-fails when it's not running). ab DETECTS Fusion (never installs it)+    via bridge.json 'detect', and auto-installs Python if missing (ab >=1.9.47) - this verb just+    reports state. Pairs with ab's bridge_readiness."""     # ALWAYS live-detect. fusion_info is a bridge-START snapshot; Fusion may have been     # installed OR UNINSTALLED since. Trusting a cached installed:True made readiness     # report installed/running/ready:true on a machine where Fusion had been removed@@ -2650,7 +2650,7 @@ COMMAND_HANDLERS = {     ),     "addin_status": lambda fi, args: _handle_addin_status(),     # LAST-RESORT human escalation (John 2026-07-07): when the bridge is truly blocked-    # on the user (password/2FA, UAC), send an AD toast that reaches their MAIN machine+    # on the user (password/2FA, UAC), send an ab toast that reaches their MAIN machine     # (reach_user=True fans out to peer ADs, so a bridge running on an unattended VM     # still lands the toast where the user actually is). ALWAYS exhaust programmatic     # options first - this exists so the AI has ONE deterministic call when it must ask.@@ -2715,7 +2715,7 @@ def _handle_aps_open(fusion_info: dict, args: dict) -> dict:                 "data": {"match": match, "openArgs": open_args},                 "_hint": "Fusion isn't running — call fusion_start, then fusion_aps_open again to open it."}     # Open by the EXACT file URN (works for any nesting). The FIRST cloud-open can-    # take 60-90s (download) — longer than AD's relay timeout — so FIRE it in the+    # take 60-90s (download) — longer than ab's relay timeout — so FIRE it in the     # background and return immediately. Caller polls fusion_get_app_state. Unless     # {wait:true} is passed (then block and return the open result).     urn = match.get("id")@@ -2739,7 +2739,7 @@ def _handle_aps_open(fusion_info: dict, args: dict) -> dict:         "success": True,         "output": f"Found '{name}' in project {match.get('projectName')} — opening in Fusion.",         "data": {"match": match, "opening": True},-        "statusVerb": "fusion_get_app_state",  # AD 1.9.9 convention — poll this for completion+        "statusVerb": "fusion_get_app_state",  # ab 1.9.9 convention — poll this for completion         "_hint": "The cloud file is opening in Fusion in the background (first open can take "                  "~60-90s; large ASSEMBLIES download all referenced parts and take longer). "                  "Poll fusion_get_app_state until activeDocument is the file. Pass "@@ -3613,7 +3613,7 @@ def _orchestrate_open_lbr(args: dict) -> dict:             "ALWAYS VERIFY VIA SCREENSHOT: a .lbr can FAIL to open ('<file>.lbr has errors and cannot be "             "opened') while this call still returns success - that error is an OWNED POPUP. Grab "             "desktop_screenshot_window on the Fusion main hwnd and CHECK ownedPopupCount + read the "-            "_screenshots[] array (AD v1.8.177+ captures owned dialogs invisible to a plain capture); "+            "_screenshots[] array (ab v1.8.177+ captures owned dialogs invisible to a plain capture); "             "ownedPopupCount>0 means an error/confirm dialog is up. "             "NOTE: an adom-lbr .lbr is 2D ONLY - symbol + footprint + a PLACEHOLDER 3D package. "             "To attach the real 3D chip, use fusion_attach_3d_package (it runs the Package3D generator + "@@ -4211,7 +4211,7 @@ _BUILD_FLIGHT_FILE = os.path.join(os.path.expanduser("~"), ".adom", "fusion-brid   def _flight_load() -> dict:-    """In-flight builds recorded on DISK, because AD reaps and respawns this bridge.+    """In-flight builds recorded on DISK, because ab reaps and respawns this bridge.      Peer lesson (pup thread, 2026-08-28): a guard that reads its own in-memory bookkeeping     "passes vacuously" the moment that record is reset while the real condition persists.@@ -4329,10 +4329,10 @@ def _publish_run_state(run_id: str, state: dict):  def _orchestrate_build_library_3d(args: dict) -> dict:     """Bind real 3D onto a multi-part library. Runs DETACHED by default (async=True) so it-    SURVIVES AD's 60s relay cap.+    SURVIVES ab's 60s relay cap.      ⚠️ LEARNED THE HARD WAY (2026-07-08): the cloud Package3D generate + Hub upload takes MINUTES-    on a real machine, but AD's relay hard-caps every request at ~60s and `timeoutSeconds` is NOT+    on a real machine, but ab's relay hard-caps every request at ~60s and `timeoutSeconds` is NOT     honored for this verb - so the old synchronous build got its thread KILLED at 60s, wrote no     bound .lbr, and lost every wip_urn (3/4 pins on a demo board came back as flat pads because the     4th never got a fresh urn). Fix: the build now runs in a BACKGROUND daemon thread and returns@@ -4395,7 +4395,7 @@ def _orchestrate_build_library_3d(args: dict) -> dict:             "runId": _run_id, "statusVerb": "fusion_prove_result",             "boundLbr": out_path, "partsTotal": len(args.get("parts") or []),             "_hint": (-                "⏳ 3D bind runs DETACHED (survives AD's 60s relay cap, which used to kill it + lose "+                "⏳ 3D bind runs DETACHED (survives ab's 60s relay cap, which used to kill it + lose "                 "every wip_urn). POLL the boundLbr via read_file every ~30s until it has one "                 "<package3d name=... wip_urn=urn:...> PER PART (count == partsTotal). Do NOT re-fire "                 "while running (check fusion_addin_status.busy). When done, embed those <packages3d> + "@@ -4573,7 +4573,7 @@ def _taskbar_progress(pct=None):     cannot stop Fusion REQUESTING it - but we can (a) clear the flash the moment we     have handled whatever raised it, and (b) replace the signal with an honest     progress bar during chain stages, paced by this machine's measured estimates.-    pct=None clears the bar. Best-effort: never raises, silently no-ops without AD."""+    pct=None clears the bar. Best-effort: never raises, silently no-ops without ab."""     try:         if not ad_client.available():             return@@ -5464,7 +5464,7 @@ def _orchestrate_export_optimized_glb(args: dict) -> dict:         if not (silk_top or silk_bottom):             silk_note = "silkscreen capture unavailable on this design; GLB built without a baked silk texture."     # 4) Submit to the molecule optimizer (fire-and-return: a big board's tessellation-    #    can run minutes, longer than the AD relay's request timeout, so we do NOT block+    #    can run minutes, longer than the ab relay's request timeout, so we do NOT block     #    the whole time here). Then bounded-wait up to `wait` seconds (default 90) so     #    small boards still come back complete in one call.     job_name = os.path.splitext(os.path.basename(output_path))[0]@@ -5561,8 +5561,8 @@ def _describe_profile(p: dict) -> str:   def _ad_direct_api_call(verb: str, args: dict, timeout: float = 60) -> dict | None:-    """Call an AD verb over the loopback DIRECT API - the correct, non-foregrounding way for a-    bridge to reach AD when the in-process ad_client is down (issue #295). AD spawns the bridge+    """Call an ab verb over the loopback DIRECT API - the correct, non-foregrounding way for a+    bridge to reach ab when the in-process ad_client is down (issue #295). ab spawns the bridge     with ADOM_DIRECT_API_URL in its env (fallback: ~/.adom/direct-api-port); POST     {"command", "args"} to <base>/command. Returns the parsed result dict, or None if the direct     API is unavailable or errors (so _ad_call can fall through). Never shells the GUI exe."""@@ -5580,13 +5580,13 @@ def _ad_direct_api_call(verb: str, args: dict, timeout: float = 60) -> dict | No     try:         body = json.dumps({"command": verb, "args": args}).encode("utf-8")         headers = {"Content-Type": "application/json"}-        # The bridge token is what keeps a bridge's own callbacks exempt from AD's identity gate+        # The bridge token is what keeps a bridge's own callbacks exempt from ab's identity gate         # (ad_client already sends it; this path used to omit it).         tok = os.environ.get("ADOM_BRIDGE_TOKEN") or ""         if tok:             headers["X-Adom-Bridge-Token"] = tok-        # Echo the identity AD handed us, then name OURSELVES as the delegate (issue #348). The-        # thread alone would hide who ran it; the bridge alone would hide who asked. AD needs both+        # Echo the identity ab handed us, then name OURSELVES as the delegate (issue #348). The+        # thread alone would hide who ran it; the bridge alone would hide who asked. ab needs both         # to render "chip-fetcher tab 3 (via fusion)". Delegate is ADDED, never forwarded.         ident = _get_caller_identity()         for key, hdr in (("thread", "X-Adom-Caller-Thread"),@@ -5613,17 +5613,17 @@ def _ad_direct_api_call(verb: str, args: dict, timeout: float = 60) -> dict | No   def _ad_call(verb: str, args: dict, timeout: int = 60) -> dict:-    """Call another AD verb (nbrowser_*, desktop_*) from inside this bridge.+    """Call another ab verb (nbrowser_*, desktop_*) from inside this bridge.      Prefers the in-process ad_client; then the loopback DIRECT API (issue #295); only as a last     resort shells the host app's **-cli** exe (adom-bridge-cli.exe, NEVER the GUI adom-bridge.exe,-    which would foreground AD). Works on an unattended VM where ad_client is down. Never raises -+    which would foreground ab). Works on an unattended VM where ad_client is down. Never raises -     returns {} on failure so callers can degrade to instructing the AI instead.      Attribution (issue #348) is stamped into args HERE, once, so it survives on ALL THREE paths -     the in-process client and the CLI subprocess cannot carry HTTP headers, and an explicit caller-    block in args is the highest-precedence form AD accepts. The direct-API path additionally sends-    the X-Adom-Caller-* headers so AD can record the delegation chain."""+    block in args is the highest-precedence form ab accepts. The direct-API path additionally sends+    the X-Adom-Caller-* headers so ab can record the delegation chain."""     args = _stamp_caller(args)     why = []     try:@@ -5643,7 +5643,7 @@ def _ad_call(verb: str, args: dict, timeout: int = 60) -> dict:             why.append("ad_client unavailable")     except Exception as e:         why.append("ad_client raised %s" % e)-    # #295: prefer the loopback DIRECT API - it reaches AD without foregrounding anything.+    # #295: prefer the loopback DIRECT API - it reaches ab without foregrounding anything.     direct = _ad_direct_api_call(verb, args, timeout=timeout)     if isinstance(direct, dict):         return direct@@ -7525,7 +7525,7 @@ def _wall_hint(wall: str, notified: bool) -> str:   def _unwrap(r):-    """AD verb responses come back either flat or nested under `data` (the CLI puts the real+    """ab verb responses come back either flat or nested under `data` (the CLI puts the real     payload in output.data). Always look through both - forgetting this silently broke the     server-side Sign In click, because coordMap/shotId live one level down."""     if not isinstance(r, dict):@@ -8207,7 +8207,7 @@ def _remember_open_library(lbr_path: str):     needs the .lbr on disk, and `fusion_capture_library_views` never took a libraryPath - so     the ONE verb that EDITs the most names was the one verb structurally unable to run the     guard, and it "handled" the prompt by raising it and dismissing it afterwards. Persisted-    (not just module state) because AD reaps and respawns this bridge between calls, which+    (not just module state) because ab reaps and respawns this bridge between calls, which     would drop in-memory state exactly when a capture follows an open.     """     # LOCAL imports + a LOUD except. Both matter: this module imports os/time inside@@ -8695,7 +8695,7 @@ def _demo_tour_load(demo_id: str):   def _bmp_downscale_png(path: str, max_w: int = 640):-    """Pure-STDLIB BMP -> small PNG (nearest-neighbor). Exists because AD's provisioned+    """Pure-STDLIB BMP -> small PNG (nearest-neighbor). Exists because ab's provisioned     python has NO PIL, so screenshot captures fall back to raw 24MB BMPs - and a 24MB BMP     is 31MB as base64, which no relay frame should ever carry (caught live 2026-08-16:     every tour stepShot silently skipped, and the done gallery would have been ~280MB).@@ -8743,7 +8743,7 @@ def _bmp_downscale_png(path: str, max_w: int = 640):  def _shot_b64_small(path: str, max_w: int = 640):     """Base64 evidence frame for ab's stepShot contract (~640px wide). PIL downscale when-    available; the stdlib BMP downscaler when not (AD's python ships without PIL); else+    available; the stdlib BMP downscaler when not (ab's python ships without PIL); else     the raw file if it is small enough for the relay envelope."""     import base64     try:@@ -12994,8 +12994,8 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No     # open_lbr regression (issue #47). The gate now runs at every release.     """Dispatch a command to the appropriate handler. -    caller_identity: {'thread','container','reason'} from the X-Adom-Caller-* headers AD stamped on-    this request. Bound to THIS thread (issue #348) so any AD callback we make while carrying out+    caller_identity: {'thread','container','reason'} from the X-Adom-Caller-* headers ab stamped on+    this request. Bound to THIS thread (issue #348) so any ab callback we make while carrying out     the verb is attributed to the AI thread that asked for it. Pass None to leave the binding alone.     """     if caller_identity is not None:@@ -13461,13 +13461,13 @@ def _dispatch_command_inner(command: str, args: dict, caller_identity: dict = No   def _build_status() -> dict:-    """Build the /status payload — the endpoint AD declares as healthEndpoint.+    """Build the /status payload — the endpoint ab declares as healthEndpoint. -    MUST return HTTP 2xx whenever the server is up: AD's health check polls this-    path and a 404 (the classic manifest-vs-server mismatch) makes AD wait the+    MUST return HTTP 2xx whenever the server is up: ab's health check polls this+    path and a 404 (the classic manifest-vs-server mismatch) makes ab wait the     full startup grace then report a generic "not reachable". We also self-report-    the GUI chip fields {led, summary, tooltip} — AD renders them verbatim (the-    bridge owns its color; AD owns only the unreachable→gray state).+    the GUI chip fields {led, summary, tooltip} — ab renders them verbatim (the+    bridge owns its color; ab owns only the unreachable→gray state).     """     import platform @@ -13536,7 +13536,7 @@ class FusionBridgeHandler(BaseHTTPRequestHandler):         command = request.get("command", "")         args = request.get("args", {}) -        # Who is asking (issue #348). AD stamps these on every request it dispatches to a bridge.+        # Who is asking (issue #348). ab stamps these on every request it dispatches to a bridge.         caller_identity = {             "thread": self.headers.get("X-Adom-Caller-Thread", ""),             "container": self.headers.get("X-Adom-Caller-Container", ""),@@ -13573,7 +13573,7 @@ class FusionBridgeHandler(BaseHTTPRequestHandler): def _prune_bridge_logs(max_mb: int = 8, keep_tail_mb: int = 2) -> dict:     """LOG JANITOR (John, 2026-07-22: "do you have a janitor to clean up your log?"). -    AD captures this bridge's stdout/stderr into ~/.adom/bridge-logs/fusion360.log. Nothing+    ab captures this bridge's stdout/stderr into ~/.adom/bridge-logs/fusion360.log. Nothing     rotated it, so it grew unbounded - fine today (it is small), a slow leak on a long-lived box.     On every bridge start we truncate our OWN logs to the most recent keep_tail_mb once they pass     max_mb, keeping the tail because that is where a crash traceback lives.@@ -13610,7 +13610,7 @@ def _prune_bridge_logs(max_mb: int = 8, keep_tail_mb: int = 2) -> dict: def main():     global fusion_info -    # Line-buffer stdout/stderr. AD spawns the bridge console-less and captures+    # Line-buffer stdout/stderr. ab spawns the bridge console-less and captures     # our stdout+stderr into ~/.adom/bridge-logs/fusion360.log. Python BLOCK-buffers     # stdout when it's not a TTY, so without this the buffer never flushes while     # serve_forever() runs → the log stays 0 bytes (and a spawn-crash traceback@@ -13675,9 +13675,9 @@ def main():     all_commands = list(COMMAND_HANDLERS.keys()) + sorted(ADDIN_COMMANDS)     print(f"[Fusion Bridge] Available commands: {', '.join(all_commands)}") -    # AD (>=1.9.63) passes ADOM_BIND_HOST (always 127.0.0.1) to every bridge it+    # ab (>=1.9.63) passes ADOM_BIND_HOST (always 127.0.0.1) to every bridge it     # spawns. Honor it and NEVER bind 0.0.0.0/'' by default - a public bind pops a-    # Windows Firewall "allow access?" dialog, and AD's guarantee to users is no+    # Windows Firewall "allow access?" dialog, and ab's guarantee to users is no     # firewall prompts. Default to loopback if the var is absent (e.g. local dev).     bind_host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")     server = ThreadingHTTPServer((bind_host, port), FusionBridgeHandler)@@ -14038,7 +14038,7 @@ def _handle_live_feed(fusion_info: dict, args: dict) -> dict:                         _LIVE_FEED["lastError"] = ("ad_client: %s" % _ce)[:200]                     if not isinstance(shot, dict):                         shot = _ad_call("desktop_screenshot_window", {"hwnd": int(hwnd)}, timeout=20) or {}-                    # The AD reply nests the capture under `data` ({data:{fullPath,...}, message}).+                    # The ab reply nests the capture under `data` ({data:{fullPath,...}, message}).                     # Reading the top level gave "screenshot returned no path: ['data','message']"                     # on every tick all session (found by the honesty fields, 2026-08-29).                     # Owned popups (banner, panels, dialogs) may sit at the OUTER level while the@@ -14327,7 +14327,7 @@ COMMAND_HANDLERS["demo_progress"] = lambda _info, args: _handle_demo_progress(ar   if __name__ == "__main__":-    # Surface any fatal startup error to stderr (which AD captures into+    # Surface any fatal startup error to stderr (which ab captures into     # ~/.adom/bridge-logs/fusion360.log) so a spawn-crash is debuggable, then     # re-raise for a non-zero exit.     try:
skills/fusion-aps-signin/SKILL.md+3−3
@@ -11,7 +11,7 @@ part is Autodesk's **email OTP**, which **expires fast** — the user may walk a cup of coffee"), so you must **monitor** the flow and turn the code around in seconds.  All browser steps use the native-browser extension verbs (`nbrowser_*`) against the-AD-connected laptop: `adom-bridge --target <laptop> nbrowser_* '{...}'`.+ab-connected laptop: `adom-bridge --target <laptop> nbrowser_* '{...}'`.  ## Working principles (non-negotiable) @@ -39,7 +39,7 @@ AD-connected laptop: `adom-bridge --target <laptop> nbrowser_* '{...}'`.    `sessionId`** in every `nbrowser_eval/type/click`. The extension **refuses to drive PWA /    installed-app windows** (e.g. "Hydrogen - Chat") and `nbrowser_eval` defaults to the    foreground tab — which is often the wrong (PWA) window.-4. **Foreground it with `desktop_bring_to_front`** (the AD verb) so the user can see/act on+4. **Foreground it with `desktop_bring_to_front`** (the ab verb) so the user can see/act on    the login. `nbrowser_focus_window` is NOT implemented. 5. **Proactively use adom-google.** Autodesk emails the OTP — you can read it via the    `adom-google` Gmail API passthrough. If adom-google isn't set up, TELL the user it exists@@ -285,7 +285,7 @@ This whole flow was driven live and works. Replicate it per org: 3. The bridge's **monthly hard cap** (fails closed). Card on file = identity verification only.  ## Port choice for the callback-Check availability LIVE on the box (no AD verb exists): `python` bind-test on 127.0.0.1 ++Check availability LIVE on the box (no ab verb exists): `python` bind-test on 127.0.0.1 + `netsh int ipv4 show excludedportrange protocol=tcp`. 8917/8918/8920 verified free + outside Windows reserved ranges (5357, 27339, 50000-50059). The box runs ~67 listeners — don't assume. 
skills/fusion-autodesk-signin/SKILL.md+1−1
@@ -63,7 +63,7 @@ On the Autodesk sign-in page (`signin.autodesk.com/?flowId=...`): click **"Conti ## Step 5 — Escalate the secret step (never type it) `fusion_notify_owner {title:"Fusion sign-in needs you", body:"On <box> Fusion is at the Google password step for <email>. Please enter your password/2FA in the open browser; I'll finish once-you're in."}`. It toasts the user's MAIN computer, fanning out cross-AD so it reaches them even though+you're in."}`. It toasts the user's MAIN computer, fanning out cross-ab so it reaches them even though this bridge runs on an unattended VM. Then WAIT and poll `fusion_readiness`; do not re-toast for a few minutes. (The user may instead remote into the box and type it — either way you take over after.) 
skills/fusion-driving/SKILL.md+2−2
@@ -95,13 +95,13 @@ ways to get the array, in order of preference:    enforces this" below.) 2. **`fusion_screenshot_all`** - captures the main window PLUS every Qt dialog window as separate    shots, and returns the list of saved paths. Read each.-3. **`desktop_screenshot_window {"hwnd": <fusion-hwnd>}`** (AD core) - returns `screenshots[]` and+3. **`desktop_screenshot_window {"hwnd": <fusion-hwnd>}`** (ab core) - returns `screenshots[]` and    **`ownedPopupCount`**. `ownedPopupCount > 0` means dialogs are stacked on the window. Read the    shots. Get the hwnd from `fusion_window_info` (its `hwnd` field) or `desktop_find_window    {"titleContains":"Autodesk Fusion"}`.  **To actually SEE a shot - mind WHERE it lands:**-- AD core `desktop_screenshot_window {hwnd}` saves the PNG **into your container** (e.g.+- ab core `desktop_screenshot_window {hwnd}` saves the PNG **into your container** (e.g.   `/tmp/adom-desktop-screenshots/`) - Read it directly. - The bridge's own captures (the auto `dialogArray[].screenshot` paths and `fusion_screenshot_all`)   are saved on the **Windows box** (`C:/tmp/conduit-screenshots/`). To see one, either re-capture that
skills/fusion-molecule-export/SKILL.md+1−1
@@ -87,7 +87,7 @@ pipeline: anchor to MP pins -> optional silk bake -> gold pins -> dedup/flatten/ prune -> Draco), so the same board comes out **~465 KB / ~31 draw calls, auto-anchored**. ALWAYS use this for a component page's `component.parts.model_3d` - never a raw STEP->GLB. -Two verbs, because a big board tessellates for minutes (longer than the AD relay's ~60s+Two verbs, because a big board tessellates for minutes (longer than the ab relay's ~60s request timeout), so it is submit-then-poll, never one long blocking call:  ```
skills/fusion-multiprofile-signin/SKILL.md+2−2
@@ -103,7 +103,7 @@ Two things bite after the profile fix works: ## ⭐ THE PROVEN APPROACH (John's directive, 2026-08-22) - DO NOT over-automate this  After hours of trying to fully automate the desktop sign-in, John's ruling is: **trigger the sign-in,-then put up an AD CAPTION asking the USER to complete it in the browser window that just opened, and+then put up an ab CAPTION asking the USER to complete it in the browser window that just opened, and STOP. Do not try to automate the browser-side auth or the protocol handoff.** The exact flow:  1. **If switching accounts:** sign the current user out first (reset the shared Autodesk identity, see@@ -117,7 +117,7 @@ STOP. Do not try to automate the browser-side auth or the protocol handoff.** Th 3. **Detect the browser auth window** (you CAN): `Get-Process chrome|msedge | Where MainWindowTitle -match    "Sign in - Autodesk"` gives its hwnd, and `nbrowser_list_tabs {profile}` shows which profile+tab holds    the `signin.autodesk.com` flow.-4. **Put an AD caption on that browser window asking the user to finish**, then poll and STOP automating:+4. **Put an ab caption on that browser window asking the user to finish**, then poll and STOP automating:    `desktop_caption {hwnd:<auth window>, text:"Please finish signing in as <email> here", subtext:"Adom    will detect when it completes"}`. Do NOT drive the page, do NOT reopen the flow elsewhere, do NOT click    the protocol dialog. John completes it by hand.
skills/fusion-onboarding/SKILL.md+7−7
@@ -15,18 +15,18 @@ All laptop steps run via `adom-bridge --target <laptop> shell_execute` / `run_sc `send_files`. Drive the user's own machine; don't ask them to do steps you can do.  ## Step 0 — Check readiness (fast, never launches Fusion)-Call **`fusion_readiness`** (or AD's **`bridge_readiness`**) FIRST - it reports `installed` / `running`+Call **`fusion_readiness`** (or ab's **`bridge_readiness`**) FIRST - it reports `installed` / `running` / `ready` without launching anything: - `installed:false` → onboard (the steps below). - `installed:true, running:false` → **no onboarding needed**, just `fusion_start`, then drive. - `ready:true` → drive Fusion directly. -NOTE (AD >=1.9.63): AD **DETECTS** the Fusion host app (via the bridge.json `detect` block - the+NOTE (ab >=1.9.63): ab **DETECTS** the Fusion host app (via the bridge.json `detect` block - the `%LOCALAPPDATA%\Autodesk\webdeploy\production\*\FusionLauncher.exe` path) but **NEVER installs it** --Fusion is the user's host app. AD also **provisions Python itself** - a system install if one is on+Fusion is the user's host app. ab also **provisions Python itself** - a system install if one is on PATH, else a portable, no-UAC python-build-standalone copy (pinned 3.12.13) under `~/.adom/adom-runtimes/` - and spawns the bridge from it by absolute path. So **never** download or-bootstrap Python here; the user sees no UAC prompt for a runtime (check state with AD's `runtimes`+bootstrap Python here; the user sees no UAC prompt for a runtime (check state with ab's `runtimes` verb). Onboarding is only about Fusion itself + the Autodesk login + deploying the add-in.  ## Step 1 — Download the Fusion installer@@ -99,11 +99,11 @@ auth tab must be in nbrowser's window; if Fusion opened a different Edge window, path above.  > **NOTIFY only when you truly can't proceed.** The sign-in is something you should AUTOMATE, not-> punt. Reserve an AD **`notify_user`** toast for a genuine wall (a UAC/elevation prompt, a+> punt. Reserve an ab **`notify_user`** toast for a genuine wall (a UAC/elevation prompt, a > `--globalinstall` consent, license acceptance, or a 2FA/password step you must not auto-fill).-> As of **AD 1.9.84** `notify_user` works directly over the bridge's direct API — you don't relay a+> As of **ab 1.9.84** `notify_user` works directly over the bridge's direct API — you don't relay a > payload to the AI anymore. **If the bridge runs UNATTENDED (e.g. on a VM) and the user is on-> another machine, add a cross-AD `target`** (a peer clientName from the `targets` verb, or `"all"`)+> another machine, add a cross-ab `target`** (a peer clientName from the `targets` verb, or `"all"`) > so the toast lands where they actually are. The bridge already fires this itself for a seat/ > licensing DECISION dialog (a true wall) — see `notifyDelivered` in the launch response.