← Commit history
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.9.255\ No newline at end of file+1.9.256\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.9.255",-  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.255/adom-bridge-fusion-v1.9.255.zip",-  "sha256": "34ec5b871505618fc58d7b72aa344de6c7ce60126bf8dde2ee6598c632c31162",-  "size": 550007,+  "version": "1.9.256",+  "url": "https://wiki.adom.inc/download/adom/fusion-bridge/1.9.256/adom-bridge-fusion-v1.9.256.zip",+  "sha256": "b6cf3b2da20fd7baaa0149d4093c29ef70dbeac00d174274c156ffaf0ca65a2a",+  "size": 552857,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.9.255",+  "version": "1.9.256",   "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.255"+version = "1.9.256" dependencies = [  "serde_json", ]
dashboard/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.9.255"+version = "1.9.256" edition = "2021"  [dependencies]
describe.py+1−1
@@ -15,7 +15,7 @@ _STATUS = "fusion_get_app_state" _T = [     # ── Lifecycle ──────────────────────────────────────────────────────────     ("fusion_preflight_lbr", "PRE-ANALYZE a .lbr: will Fusion open it? Offline and instant - no Fusion needed, nothing launched. Runs the named checks the bridge learned from real Fusion rejections: file-readable, size-sane (truncated-transfer corpse detection), xml-parses, root-is-eagle, packages-nonempty (Fusion's silent 'No Components'), symbols-present, devicesets-present (missing ones make EDIT raise Create-new prompts), connects-resolve (dead wiring renders fine and connects nothing). fusion_open_lbr and fusion_create_library run the same gate automatically; call this directly to gate a batch BEFORE staging.", {"libraryPath": "str (Windows .lbr path)"}, {"success": "bool", "preflight": "{ok, checks[], errors[]}"}, 10, None, True, {"libraryPath": "C:/Temp/PART.lbr"}),-    ("fusion_install_library_bundle", "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.", {"zipPath": "str (staged bundle zip on THIS machine) - or", "lbrPath": "str (staged .lbr, STEP models beside it)", "libraryName": "optional str (default: file stem)", "capture": "optional bool (default true: bind + prove; false: place only)"}, {"installed": "bool", "libraryPath": "str", "partsTotal": "int", "partsToBind": "int", "unbound": "[{package, why}]", "partMetadata": "{ok, summary, warnings[]}", "fusion": "{installed, running, addinReady, status, message}", "runId": "str", "statusVerb": "fusion_prove_result"}, 60, "fusion_prove_result", True, {"zipPath": "C:/Temp/MyLib.zip", "libraryName": "MyLib"}),+    ("fusion_install_library_bundle", "COMPOSITE, the Fusion twin of kicad_install_library_bundle (#68): ONE call takes a staged Cloud Library bundle (zip of <Lib>.lbr + <Part>.step models, or a bare lbrPath with STEPs beside it), unpacks it safely, places it under ~/Documents/Adom Libraries/<name>/, pairs EVERY deviceset with its STEP by name (deviceset, package, normalised, prefix; a one-part library pairs its single model regardless), lints part numbers (partMetadata: MPN, manufacturer, Mouser/DigiKey/LCSC - reported, never blocking), pre-flights Fusion state (fusion{status}), and starts the same bind -> prove chain fusion_create_library runs. ASYNC BY CONTRACT: returns IMMEDIATELY with runId + statusVerb because a STEP bind is 3-15 minutes and cannot fit a relay window; poll fusion_prove_result (measured percent, four streamed proof frames per part, provedParts[], and an addin_restart_required payload the CALLER turns into the ask). PARTIAL SHIPS: a deviceset with no model is a NAMED skip in unbound[], the rest still bind; partsBound < partsTotal is the signal. Each part is minted on its own single-deviceset library (the Package3D generator loads the FIRST package of the open library, so a merged library would put every chip on part one's footprint), with the STEP placed exactly as published (orient:false, origin trusted), then all urns are bound into the merged library, and after prove the bound library is SAVED TO THE CLOUD under <project>/Adom Libraries so it appears in Fusion's Library Manager (cloudLibrary{saved, fileId, duplicatesBefore} in fusion_prove_result). Per-part scaffolding is deleted; the user is left with one library plus its per-package f3d folder. capture:false only places the files and returns. This is the one contract the wiki CLI (`library install --format fusion`) and the Export button both read; the recipe has one owner.", {"zipPath": "str (staged bundle zip on THIS machine) - or", "lbrPath": "str (staged .lbr, STEP models beside it)", "libraryName": "optional str (default: file stem)", "capture": "optional bool (default true: bind + prove; false: place only)"}, {"installed": "bool", "libraryPath": "str", "partsTotal": "int", "partsToBind": "int", "unbound": "[{package, why}]", "partMetadata": "{ok, summary, warnings[]}", "fusion": "{installed, running, addinReady, status, message}", "runId": "str", "statusVerb": "fusion_prove_result"}, 60, "fusion_prove_result", True, {"zipPath": "C:/Temp/MyLib.zip", "libraryName": "MyLib"}),     ("fusion_create_library", "ONE-CALL library creation for web-control (one call = one consent). EVERY LIBRARY IS MULTI-PART - a single-part library is simply parts:[one entry], and there is no separate single-part verb to choose. Pass parts:[{package, lbrPath, modelPath}] for any number of parts; the older {libraryPath, stepPath} shape still works and is normalised into a one-element parts list, so both run the identical code path and the ETA scales with the part count either way. TWO entry shapes: A) {libraryPath, stepPath} - a ready .lbr plus the 3D; B) {kicadSymbolPath, kicadFootprintPath, stepPath, pinMap?} - KiCad sources; needs adom-lbr, which is Linux-container-only by platform rule, so on Windows/Mac boxes shape B refuses IMMEDIATELY (adom_lbr_unavailable, before any staging or Fusion launch - fusion_readiness reports kicadIngest up front). Convert at AUTHORING time in a container and use shape A. pinMap = explicit {pinName: padName} for when pin numbers do not match pad names (BGA-style). Chains convert -> bind 3D -> prove (four evidence frames + verdict) in the BACKGROUND and returns immediately; poll fusion_prove_result (stage narrates; top-level `verdict` proven/not_proven is the score - `stage:done` only means the chain finished). EVERY PART IS PROVED, not just the first: a 10-component library streams 40 evidence frames, FOUR per component (symbol, footprint, 3d, ganged) in assembly order, each tagged with `part` so a front end can group them as they arrive, and `provedParts[]` reports per-part pass/fail. The library is proven only if EVERY part is - one good part does not vouch for nine unchecked ones. On the FINAL fetch pass {inlineFrames:true} to get each evidence frame as a base64 thumbnailDataUrl a browser can render (plain polls return box paths only). The chain closes its library document on success AND failure - batches need no between-part sweeping. stepPath is required for a PROVEN library - no real 3D body without it.", {"parts": "optional [{package, lbrPath, modelPath}] - THE canonical shape, any count", "libraryPath": "optional str (shape A, or output path for B)", "kicadSymbolPath": "optional str", "kicadFootprintPath": "optional str", "stepPath": "optional str (required for proven)", "pinMap": "optional {pinName: padName}", "name": "optional str"}, {"started": "bool", "statusVerb": "str"}, 30, "fusion_prove_result", True, {"libraryPath": "C:/Temp/PART.lbr", "stepPath": "C:/Temp/PART.step"}),     ("fusion_start", "Launch Fusion 360 (if needed) and wait for the AdomBridge add-in to be ready.",      {}, {"success": "bool", "addinReady": "bool", "status": "str"}, 150, None, True, {}),
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.255"+DASH_VERSION="1.9.256" 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.255",+  "version": "1.9.256",   "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.255",+  "version": "1.9.256",   "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+93−5
@@ -12017,6 +12017,47 @@ COMMAND_HANDLERS["prove_result"] = _handle_prove_result   ++def _publish_library_to_cloud(lbr_path: str, name: str, folder: str = "Adom Libraries") -> dict:+    """Save the bound library into the user's cloud project so Fusion's Library Manager lists it.++    Folder is created if missing (project root/<folder>). A same-named library already there is+    reported (Fusion allows duplicate names; this saves a NEW document and says so, update-in-+    place is a follow-up). Returns {saved, name, project, folder, fileId, versionId,+    duplicatesBefore, error}.+    """+    out = {"saved": False, "name": name, "folder": folder}+    stem = lbr_path.replace("\\", "/").rsplit("/", 1)[-1].rsplit(".", 1)[0]+    name = (name or stem).strip()+    out["name"] = name+    try:+        r = _proxy_to_addin("create_cloud_folder", {"folderName": folder}, timeout=60)+        out["folderCreated"] = bool(r.get("success")) and not bool((r.get("data") or {}).get("existed"))+    except Exception as e:+        out["folderNote"] = str(e)[:120]+    try:+        lst = _proxy_to_addin("list_cloud_files", {"folderPath": folder}, timeout=60)+        names = [str(f.get("name") or "") for f in ((lst.get("data") or {}).get("files") or [])]+        out["duplicatesBefore"] = sum(1 for n in names if n.lower() == name.lower())+    except Exception:+        out["duplicatesBefore"] = None+    act = _ensure_library_active(lbr_path)+    if not act.get("ok"):+        out["error"] = "library document is not active, so it could not be saved: " + str(act.get("why") or "")[:120]+        return out+    r = _proxy_to_addin("save_to_cloud", {"name": name, "folderPath": folder,+                                          "description": "Imported by Adom (bound 3D packages)"},+                        timeout=180)+    d = r.get("data") or {}+    if not r.get("success"):+        out["error"] = str(r.get("error") or "save_to_cloud failed")[:200]+        return out+    out.update(saved=True, project=d.get("project"), folder=d.get("folder") or folder,+               fileId=d.get("fileId"), versionId=d.get("versionId"), versionNumber=d.get("versionNumber"))+    _bridge_action_ledger("library_saved_to_cloud", {"name": name, "folder": folder,+                                                     "fileId": d.get("fileId")})+    return out+ def _handle_create_library(fusion_info: dict, args: dict) -> dict:     """ONE verb for the whole "create a Fusion library" task - one call, one consent. @@ -12326,6 +12367,15 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                             "bind failed and returned no reason and no urn")                     state.update(stage="failed", success=False); _save(); return                 _leave("bind")+                # Per-part mint libraries were scaffolding (one deviceset each so the generator+                # loads the right footprint); nothing of them may linger on the user's disk.+                for _cd in (args.get("_cleanupAfterBind") or []):+                    try:+                        import tempfile as _tfc+                        if str(_cd).startswith(_tfc.gettempdir()):+                            shutil.rmtree(_cd, ignore_errors=True)+                    except Exception:+                        pass             _enter("proving (four evidence frames)")             # #60: publish each artifact THE MOMENT it lands, in assembly order             # (symbol -> footprint -> 3d -> ganged), so a poller watches the bundle build@@ -12436,6 +12486,19 @@ def _handle_create_library(fusion_info: dict, args: dict) -> dict:                 state.update(stage="done", success=bool(prove.get("proven")),                              verdict=("proven" if prove.get("proven") else "not_proven"),                              evidenceCount=_frames, prove=prove)+            # THE LIBRARY MUST END UP IN THE USER'S LIBRARY MANAGER (John, 2026-08-29: "the user+            # is just after a multipart library"). Until now the chain bound, proved, and closed+            # the local .lbr with save:false, leaving a proven FILE and no library in Fusion.+            _pub = args.get("_publishToCloud")+            if _pub and (state.get("bind") or {}).get("success"):+                try:+                    state["stageLabel"] = "saving the library to your cloud Library Manager"+                    _save()+                    state["cloudLibrary"] = _publish_library_to_cloud(+                        target_lbr, _pub.get("name") or "", _pub.get("folder") or "Adom Libraries")+                except Exception as _pe:+                    state["cloudLibrary"] = {"saved": False, "error": str(_pe)[:200]}+                _save()             # Resolve the in-Fusion banner to match the verdict, then let it linger briefly             # so the human sees the outcome, and hide it - a stale "working" banner over an             # idle Fusion would be the same lie as a stuck progress bar.@@ -12609,16 +12672,35 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:         dest = os.path.join(home, "Documents", "AdomLibraries", re.sub(r"\s+", "_", lib_name))         placed = _lb.place_library(lbr_found, models, dest)         lbr_path = placed["lbrPath"]-        parts = [{"package": p["package"], "lbrPath": lbr_path,-                  "modelPath": placed["models"].get(p["modelPath"], p["modelPath"]).replace("\\", "/")}-                 for p in pairing["parts"]]+        # ONE LIBRARY PER PART FOR THE MINT (John, 2026-08-29). Fusion's Package3D generator+        # loads the FIRST package of the open library, so minting every part against the+        # merged file put chips two and three on part one's footprint. Each part is minted+        # on its own single-deviceset library; the urns are bound back into the merged file.+        # orient:false - the wiki publishes STEPs with the correct origin and orientation;+        # no bounding-box guess (rotation or translation) is ever right for a lopsided chip.+        _split_dir = _tf.mkdtemp(prefix="adom-lib-parts-")+        _split = _lb.split_per_part(lbr_path, _split_dir)+        parts = []+        for p in pairing["parts"]:+            _ds = p.get("deviceset") or p["package"]+            _sp = _split.get(_ds) or {}+            if not _sp.get("lbrPath") or not _sp.get("package"):+                pairing["unbound"].append({"package": _ds, "why": "could not split this deviceset "+                                           "into its own library (no package on its devices?)"})+                continue+            parts.append({"deviceset": _ds, "package": _sp["package"], "lbrPath": _sp["lbrPath"],+                          "modelPath": placed["models"].get(p["modelPath"], p["modelPath"]).replace("\\", "/"),+                          "orient": False})         report = _part_metadata_report(lbr_path)         plan = _handle_plan_library_import(fusion_info, {"parts": parts})         out = {"success": True, "installed": True, "libraryName": lib_name, "libraryPath": lbr_path,                "installDir": dest.replace("\\", "/"),                "partsTotal": len(meta.get("devicesets") or []),                "partsToBind": len(parts),-               "parts": [{"package": p["package"], "modelPath": p["modelPath"]} for p in parts],+               "parts": [{"deviceset": p["deviceset"], "package": p["package"], "modelPath": p["modelPath"],+                          "mintedOn": p["lbrPath"]} for p in parts],+               "mint": "each part minted on its own single-deviceset library so the generator loads "+                       "that part's footprint; STEP placed as published (orient:false, origin trusted)",                "unbound": pairing["unbound"], "modelsUnused": [os.path.basename(m) for m in pairing["modelsUnused"]],                "partMetadata": report, "fusion": plan.get("fusion"), "plan": plan.get("plan"),                "estimatedSec": plan.get("estimatedSec"),@@ -12638,7 +12720,13 @@ def _handle_install_library_bundle(fusion_info: dict, args: dict) -> dict:         # "prove ONLY this deviceset", so passing the library name made prove hunt for a         # symbol called ray-dac80504 and score DAC80504's real frames not_proven. The chain         # proves EVERY deviceset when name is absent, which is what a bundle wants.-        chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts})+        chain = _handle_create_library(fusion_info, {"libraryPath": lbr_path, "parts": parts,+                                                     # scaffolding only: deleted once the bind is done+                                                     "_cleanupAfterBind": [_split_dir],+                                                     # the end state the user is after: ONE library in+                                                     # Fusion's Library Manager, not a file on disk+                                                     "_publishToCloud": {"name": lib_name,+                                                                         "folder": "Adom Libraries"}})         out.update({k: chain.get(k) for k in ("started", "attached", "runId", "statusVerb", "stage")})         if not chain.get("success"):             out.update(success=False, error=chain.get("error"), errorCode=chain.get("errorCode"))
skills/fusion-multipart-libraries/SKILL.md+5
@@ -17,6 +17,11 @@ real chip, flat on its footprint, built entirely this way. > **From a Cloud Library bundle (zip of .lbr + STEPs):** `fusion_install_library_bundle > {zipPath}` derives `parts[]` itself by pairing every deviceset with a STEP by name, then > runs this exact chain. Unpaired devicesets are named in `unbound[]`; the rest still ship.+> It mints EACH part on its own temporary single-deviceset library (never the merged file:+> the generator loads the merged file's first package, so three chips landed on one footprint,+> John 2026-08-29), trusts the STEP's published origin and orientation (`orient:false`, no+> bounding-box guesses), binds every urn into the merged library, proves it, and then saves+> it into the user's cloud project under `Adom Libraries` so Fusion's Library Manager lists it.  For a whole many-part library, you no longer loop `make_3d_package` + hand-write bindings. ONE verb does the entire pipeline - per-part footprint+chip f3d packages, **all** bindings injected in one
skills/fusion-web-control/SKILL.md+17
@@ -111,6 +111,23 @@ Any verb needing the add-in now returns: - **Branch on `documentsKnown`.** The counts come from the add-in, which by definition is not answering; when it cannot be reached they are `null` rather than guessed. - **Ask before restarting when a human is present.** Then `fusion_restart_for_addin {save:true}`, which saves first and **refuses** with `unsaved_named_document` rather than discarding a never-saved document. +### The chain asks too (proven 2026-08-29, bridge 1.9.255)++With the add-in's server stopped on purpose inside a running Fusion, `fusion_create_library`+now stops at "asking you whether to save and restart it" with `errorCode:+addin_restart_required`, `userMessage`, `documentsKnown`, `remedyVerb` and `notificationId`+in the run state, fires the two-button toast, and waits (30 min). Fusion's process is+untouched while it waits (same pid before and during). Answers:+- `fusion_restart_for_addin {save:true}` (the toast's "Save and restart" or the page button):+  Fusion restarts, the add-in answers, the SAME run continues to `proven` with+  `restartChoice: "restart"`.+- `fusion_restart_for_addin {decline:true}` (the page's "I'll handle it"; the toast button+  records the same hold): the run fails `user_owns_restart`, Fusion is untouched, and+  `fusion_start` refuses with `user_owns_restart` until the user restarts Fusion themselves.+Before 1.9.255 the chain fired nothing and `_handle_launch` killed Fusion after a 30 s gate;+only direct add-in verbs ever asked. `fusion_addin_status` also said `ok` to a dead add-in;+it now returns `addin_not_answering`.+ ## 3. Reading progress honestly  `fusion_prove_result` gives `stage`, `stageLabel`, `plan[]`, `stagesDone`, `percent`,