← Commit history

Upload 1 file(s)

John Lauer ·6d9f815965 ·2mo ago ·parent 033cf59
1 file changed +386−107
SKILL.md+386−107
@@ -1,110 +1,389 @@ ----name: fusion-multiprofile-signin-description: >-  Fix Fusion 360 signing into the WRONG Autodesk account because it fires its OAuth at the OS-DEFAULT-  browser. Fusion opens sign-in in whatever browser is default (often a personal Chrome profile), but-  a user's Autodesk seat usually lives in a DIFFERENT profile (their work Google/SSO). The bridge's-  fusion_signin verb reads the full authorize URL Fusion wrote into that wrong browser's history and-  re-opens it in the RIGHT profile, where it completes back to the running Fusion. Trigger words --  fusion wrong browser, fusion wrong account, fusion signs into wrong profile, fusion opens personal-  chrome, autodesk wrong profile, multiple chrome profiles autodesk, fusion_signin, fusion sign in-  wrong google account, fusion default browser oauth, authenticate fusion multiple profiles.+name: fusion-bridge-dev+description: How to develop, version, and PUBLISH the Adom Desktop Fusion 360 bridge — where the source canonically lives, and the rule to ALWAYS publish to the wiki AND push the GitHub backup on every change. Read before editing/shipping the fusion bridge. Trigger words — fusion bridge dev, publish fusion bridge, fusion bridge repo, where does the fusion bridge live, ship fusion bridge, bump fusion bridge, adom-desktop-fusion-bridge. --- -# Signing Fusion into the right browser profile (fixing Fusion's dumb OAuth)--## The problem (Fusion's fault, we work around it)--Fusion's desktop sign-in fires an OAuth URL at the **OS-default browser**. If your Autodesk account-lives in a *different* Chrome/Edge profile - personal Chrome is default but your seat is-`[email protected]` in your work profile - Fusion signs you into the wrong account or stalls on the wrong-Google session. Autodesk should let you pick the profile; they don't. So we do.--Verified live (John, 2026-07-21): Fusion opened his personal Chrome (`[email protected]`) when his-Autodesk login is `[email protected]` in his work profile.--## The fix: `fusion_signin`--```bash-# BEST - one server-side call does the whole chain, beats the ~2-min clock:-adom-desktop fusion_signin '{"auto":true,"profile":"chrome:[email protected]"}'-adom-desktop fusion_signin '{"profile":"chrome:[email protected]"}'   # staged/manual: fix + REMEMBER profile-adom-desktop fusion_signin '{}'                                  # staged: probe/remember/ask-```--### `auto:true` - the reliable path (do this)--The manual staged loop below needs THREE separate remote round-trips (click Sign In, capture, reopen)-and each one races Fusion's ~2-minute request expiry - on a slow or flaky relay you lose that race and-the callback shows "Sign-in request expired". `auto:true` collapses the whole chain into ONE-server-side call that runs **on the box**: it clicks Fusion's Sign In, polls the browser History-on-box (up to `waitSec`, default 32) for the fresh authorize URL, then re-opens it in the target-profile - so only ONE remote hop happens and everything else is local and fast, fitting inside the-2-min window. Pass the profile too (`{"auto":true,"profile":"chrome:[email protected]"}`) so it does not-have to stop and ask. Prefer this; use the staged calls below only to debug or when auto reports it-could not click/capture.--What it does:--1. **Reads the full authorize URL out of the wrong browser's History.** Fusion writes a complete-   `developer.api.autodesk.com/.../authorize?client_id=...&code_challenge=...&state=...&request_id=idsdk-...`-   into whatever browser it opened. The address bar rewrites within a second (the Autodesk SPA-   cleans it), but Chrome's/Edge's **History DB keeps the full URL** - so the bridge copies the-   locked History file and reads the newest Fusion (`idsdk`/`idmgr`) authorize entry. Racing the-   address bar over the relay does NOT work; the History read is the reliable capture.-2. **Re-opens that exact URL in the RIGHT profile, backgrounded** (ABE / `nbrowser_open_window`), so-   it never interrupts what the user is doing.-3. It completes back to the **running Fusion** on its own. The redirect is-   `accounts.autodesk.com/idmgr/callback` + an `autodesk://` protocol handoff matched by-   `request_id`, **not** a browser-specific localhost port - so the profile no longer has to be the-   OS default. Any profile that finishes the login hands the token to Fusion.--## Picking the right profile--- **Best:** the user tells you once (`{profile:"chrome:[email protected]"}`); the bridge **remembers** it-  (`~/.adom/fusion-signin/profile.json`) for every future sign-in.-- **Auto:** the verb probes each live profile with `nbrowser_login_state` against-  accounts.autodesk.com and prefers the one holding a real Autodesk session; failing that, a-  work/corporate identity over a consumer mailbox - and says it is guessing.-- **Unsure:** it returns `stage:"ask_profile"` - ASK which account their Autodesk login is, do not-  assume. Name profiles by identity (`Chrome - you ([email protected])`), never "your browser".--## The staged loop--- `stage:"click_signin"` - Fusion has not opened the browser yet. Click Fusion's **Sign In** (it is a-  webview with no UIA node, so an image-space `desktop_click` on the button; screenshot the Fusion-  window first for a shotId), wait ~4s, call `fusion_signin` again.-- `stage:"opened"` - the URL is now in the right profile (`signinWhere`). Tell the user where, why-  (`signinProfileReason`), and offer: they finish it, you foreground it, or (with their OK) you drive-  "Continue with Google/Apple/Microsoft". **Never type their password/2FA** - escalate a real secret-  with `fusion_notify_owner`. Poll `fusion_readiness` until `ready:true`.-- `stage:"ask_profile"` - ask which profile, then re-call with it.--## The ~2-minute clock and the last-mile handoff (learned live 2026-07-21)--Two things bite after the profile fix works:--1. **Fusion's sign-in request EXPIRES in ~2 minutes.** If capture-to-completion takes longer (e.g.-   the user logs in by hand and dawdles), the callback page shows **"Sign-in request expired"** and-   Fusion never gets the handoff. `fusion_signin` now REFUSES a captured URL older than ~150s and-   returns `stage:"stale"` telling you to get a FRESH request: **fusion_stop -> fusion_start** (resets-   Fusion to a clean "Welcome to Fusion"), click Sign In, then call fusion_signin again promptly.-   Completion is near-instant when the target profile is ALREADY signed into Autodesk (it-   auto-consents), so pre-sign-into the profile once and future sign-ins fly.-2. **The `autodesk://` protocol handoff is the real last mile.** After login the callback page-   (`signin.autodesk.com/idmgr/callback#code=...`) says **"Click Open Identity Manager in the browser-   dialog"** with a **Retry** button. Fusion completes only when Chrome's native-   *"Open Autodesk Identity Manager?"* dialog is accepted (tick "Always allow", click "Open"). If it-   does not auto-fire: click **Retry** on the callback tab, then accept the Chrome Open dialog-   (a native UIA dialog - `desktop_find_control`/`desktop_ui_click` "Open", or a foreground click if-   UIA Invoke no-ops). This step is the same for ALL Fusion sign-ins; it is not caused by the profile-   move.--## Gotchas--- Read History AFTER Fusion has opened the browser, and take the NEWEST authorize entry - a stale one-  from a previous attempt carries a dead `request_id` and Fusion (waiting on a new one) never gets it.-- If the wrong account completes in the default browser before you move it, sign Fusion out and re-run.-- Chrome keeps History locked; always COPY the file before opening it (the verb does this).-- This is also the sign-in stage `fusion_demo` uses, so a first-time demo never lands in the wrong-  Autodesk account.+# Fusion bridge — dev + publish workflow++## ★ THE CORE GOAL: the AI runs EVERYTHING so the user never has to (why this bridge exists)++The entire reason adom-desktop and its bridges exist is so that **the AI drives the whole flow+end to end and the user does nothing manual** (John, 2026-07-06). This bridge is a FAILURE any time+it punts a step back to the user. That means the code + verbs must automate, on their own:+**install → launch (background) → Autodesk sign-in → seat/licensing conflicts → every startup dialog+→ recovery/update prompts → the actual work (open/model/export).**++- **NEVER ask the user to do something manually** - not "click Continue", not "sign in yourself",+  not "close that dialog". The ONLY acceptable exceptions are a true hard wall the OS forbids us to+  cross: a **UAC/elevation** prompt, or a **password/2FA** you must not enter for them. Everything+  else you AUTOMATE. If you catch yourself about to ask the user to click/close/choose something,+  that is a bug in this bridge - fix the bridge so it does it.+- **Detect and handle state in CODE, deterministically** - do NOT rely on the AI noticing a dialog+  and following a skill (the AI is unreliable at that). Hard signals (add-in round-trip, owning+  process, window size, `family_windows()`), not titles or vibes. See the background-launch state+  machine + `find_licensing_dialog()`.+- When something can't yet be fully automated (e.g. a browser OAuth handoff), the goal is still to+  push automation as far as it goes and shrink the manual part to zero over time - not to hand it back.++Hold every change to this bar: does it move the bridge toward "the user does nothing"? If a flow+still needs the user, that's the next thing to fix, not the user's job.++## ⛔ Background-launch invariant: NEVER steal the user's foreground (HARD RULE)++The bridge runs on a machine the **user is actively working on**. Anything this bridge launches or+opens must appear in the **BACKGROUND** and must **never yank focus** off what the user is doing+(John, 2026-07-06: "make sure new windows ALWAYS only open in the background so as not to disturb my+main work"). This is not best-effort etiquette — it is enforced **in code**, and any new window-+opening path you add MUST follow it:++- **Launch minimized + not-activated.** Start GUI processes with `STARTUPINFO.wShowWindow =+  SW_SHOWMINNOACTIVE (7)` + `CREATE_NO_WINDOW`. See `_popen_fusion_background()` in `fusion_detect.py`+  — copy that, never a bare `subprocess.Popen([exe])` (which opens Fusion in the foreground).+- **Guard the startup window — match the whole Autodesk FAMILY, not just `Fusion360.exe`.**+  `STARTUPINFO` alone is not enough: Fusion re-foregrounds itself several times as it initializes, and+  the noisiest offenders are NOT `Fusion360.exe` — the splash "Loading additional modules" is+  `FusionLauncher.exe`, and the "Signing in" + **"Active Sessions Exceeded"** dialogs are+  `AdskIdentityManager.exe`. A guard scoped to only `Fusion360.exe` lets those yank focus (the exact+  bug John hit 2026-07-06). `ensure_fusion_running()` fires `_start_focus_guard()`: a daemon that, for+  ~150s, minimizes (no-activate) **only** the foreground window when it belongs to the Autodesk launch+  family (`_FUSION_FAMILY_IMAGES` = Fusion360 / FusionLauncher / AdskIdentityManager / …, matched by+  process image via `_fusion_family_pids()`). It acts ONLY on focus-theft, so it never fights a user+  who deliberately foregrounds Fusion. Any window-family you add MUST be in that set.+- **⚠️ ctypes HANDLE TRUNCATION — the silent killer (this bit us 2026-07-06).** Any Win32 call that+  RETURNS or ACCEPTS a handle (`HWND`, `HANDLE`) MUST have its `restype`/`argtypes` set, or ctypes+  defaults to `c_int` (32-bit) and **TRUNCATES the handle on 64-bit Windows**. `GetForegroundWindow()`+  with no `restype` handed back a garbage HWND, so `ShowWindowAsync` silently no-op'd and the focus+  guard minimized NOTHING for two releases (1.6.33/1.6.34) while looking correct — the user still had+  to alt-tab back themselves. Always: `user32.GetForegroundWindow.restype = wintypes.HWND`,+  `ShowWindowAsync.argtypes = [wintypes.HWND, c_int]`, etc. VERIFY by objective window state, not by+  eye or by sampling the foreground (a 0.1s-poll guard minimizes too fast to catch in a snapshot):+  `GetWindowPlacement(...).showCmd == 2` (SW_SHOWMINIMIZED) on the family window == guard is working.+- **The guard only runs on a FRESH launch.** `ensure_fusion_running()` starts it after it spawns the+  process; if Fusion is ALREADY running, `fusion_start` returns early and the guard never fires, so an+  already-foreground Fusion stays put. When you need a clean background state, `fusion_kill` first,+  confirm the process is gone, THEN launch.+- **Seat conflict ("Active Sessions Exceeded") = AUTO-RESOLVE via UIA, in the background, never ask+  (SOLVED live 2026-07-06 after a very long day).** The definitive facts, hard-won:+  - It is a **Qt** dialog (`automationId` = `QTApplication.QTSessionBlockedStartup...`), NOT CEF.+  - Its window **TITLE is literally "Fusion360"** — the seat text is body content — so NEVER detect by+    title. There are **multiple variants of different SIZES**: "Active Sessions Exceeded" (~1235x527,+    with radios) and the **"Suspend Remote Session" confirm (only 823x262)**.+  - **DETECT by the OWNED-POPUP signal, not a size window (fixed 2026-07-06 v1.6.46/1.6.47 after a+    fragile 320px height floor silently MISSED the 262px confirm and readiness lied that it was gone).**+    A seat/sign-in modal is ALWAYS an **owned popup** of the main Fusion window (`GetWindow(hwnd,+    GW_OWNER)` != 0; confirmed by `desktop_screenshot_window`'s `ownedPopupCount` on the main window).+    The splash and the maximized main app are NEVER owned popups. `find_licensing_dialog()` returns the+    owned family window that is **dialog-SHAPED: width ≥ 480** (excludes Fusion's own ~300px-wide side+    panels — the Data Panel/browser rail are owned popups too, and matching them false-blocked `ready`+    the moment a doc was open) **and height ≥ 180** (excludes toolbar strips), smaller than the maxed+    main app. ctypes `GetWindow` MUST have `restype/argtypes=HWND` set or the owner handle truncates.+  - Its native **"Continue"** button IS exposed to **UIA** (`desktop_ui_click {hwnd, name:"Continue"}`),+    and UIA **Invoke runs in the BACKGROUND — no foreground, no focus steal, no cursor move**. Suspend+    is the pre-selected radio, so Continue grabs the license. (The radios themselves are web content+    NOT in the a11y tree — do NOT depend on selecting them.)+  - **DO NOT** kill+relaunch to "free the seat at the source": the Autodesk licensing SERVER keeps the+    seat even when the other machine is OFF, so that never clears it. `_reclaim_seat_from_peers()` is+    dead code, kept only as a comment-marker — do not resurrect it.+  - **DO NOT** coordinate-click it (`desktop_click`/SendInput): that needs foreground (disturbs the+    user) AND the focus-guard fights you by minimizing the dialog.+  - **RESOLVE + SCREENSHOT-VERIFY.** The FIRST UIA Invoke on a mid-render dialog silently NO-OPs, so+    `_resolve_seat_dialog()` re-invokes "Continue" and confirms it TRULY cleared by re-reading the+    parent window's `ownedPopupCount` via `desktop_screenshot_window` (parent/child screenshotting) —+    NOT by re-running the size heuristic that once false-'resolved' it. Do the screenshot check;+    "I clicked it" is not "it's gone."+  - **AUTO-RESOLVE ANYWHERE, in CODE, not just the launch loop.** The seat dialog also appears LATER —+    minutes after sign-in, when the license server notices too many sessions — long after the launch+    loop exits. So `fusion_readiness` SELF-HEALS: whenever it detects the dialog it auto-resolves it in+    the background and re-probes. The seat is handled deterministically in code no matter when it shows+    up; the AI never has to notice or act (John: "do all this tracking from your code ... the ai never+    follows skills"). A STALE add-in port (8774 from a just-killed instance) answers while the CURRENT+    Fusion is blocked, so the seat check takes PRIORITY over the add-in probe in both paths.+- **The add-in still loads while minimized** — background operation (open/model/export) needs no+  visible window. So minimized costs you nothing.+- **Foreground is opt-in, only to SHOW the user something.** The ONLY time you may foreground/restore+  a Fusion window is when a verb's explicit job is to show the user (and even then, per the user's+  standing rule, you surface the exact window + their viewer). Never `SetForegroundWindow` as a side+  effect of automation.+- **Applies to every window surface**, not just the Fusion launcher: any child process, helper, or+  dialog you spawn opens background/no-activate by the same means. When you add one, add a focus-guard+  or launch flag — do not assume it will behave.++If you catch yourself about to `Popen` a GUI or call a foreground API, stop and route it through the+background-launch helpers. A window that steals the user's focus is a **regression**, full stop.++## You OWN this bridge (do NOT file your own bugs as adom-desktop requests)+AD core only HOSTS/LOADS this bridge: the `adom-desktop` CLI, the relay/passthrough, bridge lifecycle++ streaming this bridge into `…\bridges-cache\fusion360`, and the core `desktop_*` verbs. **Everything+`fusion_*` is yours** - the verbs + their `_hint`s, the APS integration (auth/config/quota; port 8910+is AD's vestigial native APS, this bridge uses 8917), the add-in code AND its deployment into Fusion,+the SKILL.md, the publish loop. A `fusion_*` bug is a bug in THIS repo: fix it here and republish,+never as an adom-desktop discussion ask. (See the repo `CLAUDE.md`.)++## Your wiki structure: ONE page, THREE artifacts (know this cold)++You are a single wiki page (`wiki.adom.inc/adom/adom-desktop-fusion-bridge`) that ships **three distinct+things**, each with a different consumer:++1. **The page git repo — the ONE source of truth.** Everything lives here: `server.py`, `aps.py`,+   `addin/`, `handlers/`, `skills/`, the `*.md` docs, **`page.json`** (the skills-pkg manifest), and+   **`adom-bridge-fusion-manifest.json`** (the bridge manifest). Edit here, push here (+ GitHub backup).++2. **The Release (the zip) + the bridge manifest — consumed by ADOM-DESKTOP on `bridge_install`.**+   - The **Release zip** (`adom-bridge-fusion-v<ver>.zip`, uploaded via `adom-wiki release upload`) is the+     BUILT bridge (server + add-in). Binaries/zips are **Releases, NEVER git** (`*.zip` is gitignored and+     `repo push` silently skips it — see PUBLISHING.md).+   - The **bridge manifest** (`adom-bridge-fusion-manifest.json`, text, in git) tells adom-desktop **who you+     are**: name, version, the `url` pointing at the Release zip, `sha256`, `size`, verb prefixes, health+     endpoint. adom-desktop streams that zip by URL into its bridge cache and hash-verifies it.++3. **The adom-wiki pkg (`adom-wiki pkg install adom/adom-desktop-fusion-bridge`) — NOTHING BUT SKILLS.**+   It gives a user's container the skills to know how to talk to YOU and to Fusion. `page.json` is its+   manifest; `install.sh` copies the top-level `SKILL.md` + every `skills/*/` into `~/.claude/skills/`.+   **No binaries belong in this pkg** — that's what the Release is for.++### The install handshake (how a container gets your skills)+- An AI asks adom-desktop to install you → adom-desktop installs the **Release** (via the manifest) AND+  **tells the AI to `adom-wiki pkg install` your pkg** → the container gets ALL your skills.+- An AI may also `adom-wiki pkg install` you on its own. Then your skills **auto-update via the pkg+  mechanism** on every refresh. **THAT is the goal** — your skills always current, everywhere.++### The hero gets unlinked — re-check it+You have a hero we worked hard on (`screenshots/hero.png`, declared in `page.json`). **The wiki sometimes+UNLINKS it after certain updates (e.g. a `pkg publish`).** Every so often, verify `page.json` still+declares the hero and the page still shows it; **re-link it if it dropped.** TODO: file a bug to the+`adom/wiki` discussion about the hero getting unlinked on updates.++### Your STANDING JOB: document EVERY verb in skills+The skills pkg must document **absolutely every verb you support** — a **core skill** + **sub-skills** for+the extra areas. For each verb capture: what it does, **why** it exists, **when** to use it, why it's+**valuable**, and its **pitfalls**. Walk `fusion_describe` / `VERBS.md` and ensure each verb has skill+coverage. **Constantly refine and expand** — this is never "done." (Current coverage: fusion-electronics,+fusion-libraries, fusion-eagle-commands, fusion-aps-search/signin, fusion-onboarding; the full per-verb+sweep is ongoing.)++## How the add-in actually deploys (VERIFIED by shipping v1.5.1, 2026-06-28)+Fusion loads the add-in from a per-user dir Autodesk has MOVED across versions: **2025+ Fusion+scans `%APPDATA%\Roaming\Autodesk\FusionAddins\AdomBridge\`** and SILENTLY ignores the legacy+`%APPDATA%\Roaming\Autodesk\Autodesk Fusion[ 360]\API\AddIns\AdomBridge\` dirs (issue #63,+root-caused live: an add-in only in the legacy dir never loads). `install_addin.py` installs to ALL+locations (TARGET_CANDIDATES); if a future Fusion stops loading it, FIRST suspect the dir moved+again. ⛔ NEVER ask the user to restart Fusion / enable the add-in - YOU restart+(`fusion_stop`+`fusion_start`; `runOnStartup` does the rest). Legacy note (superseded):+the add-in previously lived only at `...\API\AddIns\AdomBridge\`,+NOT from this repo or the cache. `install_addin.py` (`_sync_directory`) copies `addin/AdomBridge` there.+**`bridge_install` only updates the CACHE (`method: in_place_merge`); it does NOT push the add-in into+Fusion.** And the sync canNOT overwrite the add-in while **Fusion is running** (it holds the files open),+so a bridge-server respawn alone does nothing. The exact sequence that worked (after the wiki publish):+1. `bridge_install {"manifestUrl": ".../adom-bridge-fusion-manifest.json"}` (add `"force": true` to re-merge).+   Its OWN output says: "AD reaps + respawns it from the new cache on the next call (no manual kill needed)."+   So do NOT run `bridge_kill` - AD restarts the bridge SERVER (new server.py/describe.py) automatically on+   the next verb. (I ran `bridge_kill` once here; it was pointless.)+2. **`fusion_stop`** (graceful; `fusion_kill` if wedged) - REQUIRED, releases the add-in file locks. Skipping this leaves the OLD add-in live.+4. Run the cache's `install_addin.py` (`cd <cache>\fusion360 && python install_addin.py`); it prints+   `Updated: commands\cloud_documents.py ...`.+5. **Verify by SHA256** (`Get-FileHash` on the Roaming file == `sha256sum` of that file unzipped from the+   published v<ver>.zip). NEVER use `findstr` - it false-negatives on these UTF-8 files (dash/box chars).+6. `fusion_start`, then confirm a `fusion_*` verb behaves new.+- A bumped `BRIDGE_VERSION` does NOT prove the running add-in changed - always hash-verify (step 5).+- NEVER hand-edit the cache or the deployed Roaming copy - edit this source, publish, reinstall.++## Canonical source = the wiki page (the ONE source of truth)+**`https://wiki.adom.inc/adom/adom-desktop-fusion-bridge`** (owner `adom`, public). A wiki+page IS a git repo; this is THE main repo for the bridge. AD installs the bridge from here+(its manifest + zip). Every version ships here.++- Local working clone: `~/project/adom-desktop-fusion-bridge`.+- Files on the page: `bridge.json`, `BRIDGE_VERSION`, `README.md`, `SKILL.md`, `aps.py`,+  `server.py`, handlers/addin, the `adom-bridge-fusion-manifest.json` + version zips+  (`adom-bridge-fusion-v<ver>.zip`), `adom-bridge-fusion-source.tar.gz`, and+  `adom-bridge-fusion.bundle` (portable full git history).++## GitHub backup — ALWAYS push it too+`github.com/adom-inc/adom-desktop-fusion-bridge` (renamed from the old `adom-bridge-fusion`+to match the wiki slug). It is a **backup mirror**. **On EVERY change you publish to the+wiki, also `git push` to this GitHub repo.** Never let them drift again (they were 6 commits+out of sync once — that's the confusion we're preventing). Wiki = canonical, GitHub = backup,+keep them in lockstep.++## The publish ritual (every version)+1. Edit in `~/project/adom-desktop-fusion-bridge`. Bump `BRIDGE_VERSION` + `bridge.json`+   `version` together.+2. `git commit` locally.+3. **Push to GitHub backup:** `git push origin main`.+4. Publish to the wiki via the raw `/files` API (the wiki CLIs are dead) — token+   `${ADOM_WIKI_TOKEN:-$(cat /var/run/adom/api-key)}`:+   `curl -X POST -H "Authorization: Bearer $TOK" -F "file=@<f>" \+    https://wiki.adom.inc/api/v1/pages/adom-desktop-fusion-bridge/files`+   Upload: the new `adom-bridge-fusion-v<ver>.zip`, the `adom-bridge-fusion-manifest.json`+   (`{manifest_version,name,version,url,sha256,size,released_at}`, url = absolute `/files/`+   path), `bridge.json`, `BRIDGE_VERSION`, a fresh `adom-bridge-fusion-source.tar.gz`+   (`git archive`) and `adom-bridge-fusion.bundle` (`git bundle create … --all`).+5. **ANON-VERIFY**: re-download the zip + sha256-match, fetch the manifest, confirm version.+6. Install on a Fusion box: `adom-desktop --target <laptop> bridge_install+   '{"manifestUrl":".../adom-bridge-fusion-manifest.json"}'`, then reap the old instance+   (`bridge_kill` on AD ≥1.8.187, else taskkill the PID on its runtimePort) so the new code+   loads, and verify via `bridge_log_read` / a `fusion_*` verb.++## The stale seed — do NOT publish from it+`adom-desktop` (private GitHub) → `plugins/fusion360/` was the bridge's ORIGINAL home and is+now a **frozen first-run fallback only** (AD bundles it offline). It is STALE and NOT+canonical. `release-bridge.sh` refuses to publish it. There is a `STALE.md` in that folder+pointing here. Never treat `plugins/fusion360` as the source; never publish from it.++Related: [[project_adom_desktop_fusion_bridge]], skills `fusion-aps-search`, `fusion-aps-signin`.++## Publishing a new version (zips are RELEASES, not git files)++Full guide: PUBLISHING.md. The one rule that bites everyone: the build **`.zip` is a release asset**+(`adom-wiki release upload <ref> <ver> <zip>`), NOT a git file - `*.zip` is gitignored and+`adom-wiki repo push` silently skips binaries (reports `ok`, but the URL 404s). Only the small+**manifest JSON** goes in the git repo (`adom-wiki repo push`), and its `url` points at the release+download URL (`/download/<org>/<slug>/<ver>/...zip`) with `sha256`/`size` matching the served asset.+Then `bridge_install {manifestUrl, force:true}`. Server-only change (server.py/describe.py/handlers) =+that's it (AD respawns the bridge server). Add-in change (addin/) = ALSO `fusion_stop` ->+`install_addin.py` -> hash-verify (Get-FileHash, not findstr) -> `fusion_start`. Community: fork the+page, release to your fork, `bridge_install` your manifest to test, then PR back.++## Publishing the SKILLS pkg (`adom-wiki pkg publish`) — the maze, SOLVED (2026-06-28)++The skills pkg (`adom-wiki pkg install adom/adom-desktop-fusion-bridge`) is SEPARATE from the Release.+Publishing it from this repo (which also holds `server.py`/`addin/`) was blocked by a maze; two fixes in+`page.json` unblock it:++1. **Make it skills-only with a `files` allowlist** — else the dep-checker scans `server.py` and flags its+   `import aps/describe/handlers/eagle_lbr/adsk/PIL` as ~20 "undeclared deps" (false positives the SERVER+   enforces, not just lint). Ship only what the pkg needs:+   ```json+   "files": ["SKILL.md","skills/**","screenshots/hero.png","README.md","VERBS.md","install.sh",+             "uninstall.sh","page.json","MAKING_LIBRARIES.md","LIBRARY_FINDINGS.md","PUBLISHING.md",+             "CREATING_BASIC_PARTS_LIBRARIES.md"]+   ```+   Use `skills/**` (a bare `skills/` matches nothing and errors).+2. **The publish DEMANDS a billboard hero** (`{type:billboard,headline,subhead,screenshot}`); it rejects+   the page's `{type:image,path}`. So set billboard ONLY to get past publish:+   ```json+   "hero": {"type":"billboard","headline":"Adom Desktop - Fusion 360 Bridge",+            "subhead":"Drive Fusion 360 from the cloud: electronics, exports, fast APS cloud search, and component libraries with real 3D",+            "screenshot":"screenshots/hero.png"}+   ```+   The image must be in the repo (curl `/files/screenshots/hero.png` if your working copy lacks it).+3. `adom-wiki pkg publish --skip-lint` (the "prompt-injection"/"no video" warnings are soft false+   positives). Bump `page.json` `version` each time; add `discovery_triggers` so AIs surface it.+4. **⛔ IMMEDIATELY REVERT THE HERO** (this is the bug John warned about). `pkg publish` overwrites the+   page's hero with the billboard form, which the PAGE renders as a generic template (your custom+   full-bleed `hero.png` shrinks to a thumbnail on the right). **Right after publishing, push the page+   hero back to the image form** — via `adom-wiki repo push` (NOT another `pkg publish`, which re-breaks it):+   ```bash+   # page.json: "hero": {"type":"image","path":"screenshots/hero.png"}+   adom-wiki repo push adom/adom-desktop-fusion-bridge --files page.json -m "restore full-bleed hero"+   ```+   The pkg stays published (registry snapshot is immutable); only the page display is restored.+5. **VERIFY**: `adom-wiki pkg install adom/adom-desktop-fusion-bridge` deploys skills to+   `~/.claude/skills/`, AND re-open the live page to confirm the **full-bleed custom hero renders** (not+   the thumbnail-in-template). Then `git commit` + `git push` `page.json` (image form) to GitHub for+   lockstep. NOTE: pkg version (`page.json`, 1.6.x) is a SEPARATE track from the Release/bridge version+   (`bridge.json`/`BRIDGE_VERSION`, 1.5.x). TODO: this publish↔hero conflict is a wiki bug — filed to+   `adom/wiki`.++## Bundle deep READMEs INTO each skill (so the installed AI actually reads them)++When a skill points at companion docs (a step-by-step guide, a findings log), **copy those READMEs INTO+the skill's own folder** (next to its `SKILL.md`) and link them with **plain local paths**+(`[GUIDE.md](GUIDE.md)`). Do NOT use repo-relative links (`../../GUIDE.md`) - they resolve in the repo but+**break once the skill installs** to `~/.claude/skills/<skill>/` (there is no repo root above it). And do+NOT rely only on wiki URLs - a remote fetch is a step the AI may skip; a file sitting right next to the+skill gets read. In the bundled copies, rewrite image refs to **absolute wiki URLs**+(`https://wiki.adom.inc/<org>/<slug>/files/<img>.png`) so screenshots still resolve. The repo-root copies+stay for the wiki Files tab; **re-sync the bundled copies on every publish** (see PUBLISHING.md). Net: the+installed skill carries the full content locally + always-current.++## Install-completeness detection (fusion_detect.py) — READ before touching install/readiness++Root-caused live on a fresh Hyper-V VM (John, 2026-07-14). The webdeploy layout is NOT obvious:++- `%LOCALAPPDATA%\Autodesk\webdeploy\production\<hash>\` — a COMPLETE install is ~615 files+  (**`Fusion360.exe`** ~880 KB + hundreds of DLLs + Qt/ Python/). Autodesk ALSO drops a tiny **8-file+  launcher STUB** dir next to it (icons + `FusionLauncher.exe` + a ~412-byte `FusionLauncher.exe.ini`).+  So a healthy install shows **two** hash dirs.+- **The completeness signal is `Fusion360.exe` (full size), NEVER the launcher or its `.ini`.** Two+  earlier "fixes" were WRONG: keying off `FusionLauncher.exe` existing → true mid-stream (it lands+  early) → premature `fusion_start` → *"Error Launching Streamed Application ... .ini missing or+  incomplete"*. Keying off `FusionLauncher.exe.ini` existing/size → also wrong: the **complete app dir+  has NO `.ini` at all**; only the throwaway stub has one, so a size gate would reject a healthy+  install and accept nothing.+- Helpers: `_app_dir_complete()` (Fusion360.exe ≥ 200 KB), `_any_app_complete()`, `_find_fusion_launcher()`+  (returns a launcher only when a complete app exists), `_incomplete_webdeploy_present()` (fresh-install+  "installing" signal = a launcher present but NO complete app anywhere; disk-state, because the+  streamer's tasklist presence is racy — flickered true only 3/25 polls during a real stream).+- `readiness`: gate `installed` on the app binary; `installing = _installer_running() OR+  _incomplete_webdeploy_present()`. **Poll to `installed:true`, not to `installing:false`** (the latter+  is racy). `_clean_incomplete_webdeploy()` no-ops when a complete app exists (never nuke the legit stub).++## Installer zip = RUNTIME ONLY (feedback from the AD thread, 2026-07-17)++The v1.6.76 zip was 16.7 MB - 15.3 MB of it demo MP4s, screenshots, and repo docs, streamed to+EVERY user's machine on bridge_install. Media and marketing belong on the WIKI PAGE (repo/Files),+never in the installer. Build the zip with the runtime whitelist (git ls-files MINUS+.mp4/.png/.jpg/.svg/.gif/.webm/.zip and screenshots/): server.py, aps.py, describe.py, handlers/,+addin/, resources/, skills/ (text), manifests. Result: 456 KB. If the zip is over ~1 MB,+something wrong is inside - list its contents before uploading.++## page.json `brief` vs `description` - they render in DIFFERENT places++The wiki puts these two fields in two different spots, and confusing them turns the Install card+into a README (John: "stop trying to turn that into a mine readme"):++- **`brief`** -> the **page-header one-liner** under the title. This is where the FEATURE COPY goes+  (what the bridge does: libraries, exports, APS search, modeling...).+- **`description`** -> the **Install card body**, directly above the `adom-wiki pkg install` command.+  This must say ONLY what the package install gives you. ~2 sentences. No feature list.++The canonical bridge `description`:++> "Installs this bridge's skills into your container so your AI knows how to drive the bridge. The+> bridge runtime itself is loaded by Adom Desktop from the release zip."++It matters beyond tidiness: users and AIs assume `pkg install` installs the BRIDGE. It does not - it+installs SKILLS into the container. The runtime is the release zip that AD streams via+`bridge_install`. Say that in the Install card, once, plainly.++Filed as SDK-wide asks on adom/adom-desktop: **#20** (every bridge page must state the pkg-vs-runtime+split) and **#21** (document + lint keeping the release zip and pkg tarball free of media bloat).++## Clicking without stealing focus: UIA first, SendInput last (John, 2026-07-22)++The background-launch invariant above covers windows we OPEN. This covers windows we CLICK. Same+principle, different verb set, and it is the one that bit us: `desktop_click` FOREGROUNDS.++> John, 2026-07-22: "try to NEVER bring windows to the foreground cuz its disruptive."++- `desktop_click` / `desktop_double_click` / `desktop_right_click` = **SendInput**. They+  **FOREGROUND** the target and move the real cursor. LAST RESORT only.+- `desktop_ui_click` / `desktop_ui_set` / `desktop_find_control` = **UIA Invoke/SetValue**.+  Programmatic: **no focus steal, no cursor move.** Chromium/Edge expose their a11y tree, so page+  buttons and form fields ARE reachable by accessible name. **Always try these first.**+- Use `_click_background_first()` in `server.py` - UIA first, foreground click only as fallback.+- The ONE documented exception: **Fusion's "Sign In" button is a webview control with no UIA+  node**, so only an image-space `desktop_click` reaches it. That does not license foregrounding+  anywhere else.+- Never raise a window to get attention. **Toast** (`_cli_notify_all`) or flash the taskbar.+- Restoring a MINIMIZED window before capturing it is fine (minimized windows screenshot as a+  ~237x39 title-bar sliver, so clicks land on nothing). Restore != raise-to-front.++### Human walls: toast the user AND tell the AI it can clear it itself++When only a human can clear something (2FA, credentials, a protocol dialog):++1. **Toast from CODE.** `_detect_signin_wall()` classifies the wall from window TITLES (cheap, no+   OCR, no foregrounding); `_notify_signin_wall()` fires a sticky toast. Response carries+   `notifyDelivered` so the AI knows not to nag.+2. **Still hand the AI a mechanical way to clear it.** A wall is not permission to give up - the+   Adom user wants everything automatic. `_wall_hint()` returns exact commands: for an emailed 2FA+   code, read it from Gmail via `adom-google`, then `fusion_signin_2fa {code}`, which types it via+   background UIA.+3. **If the tool that would automate it is missing, upsell it** rather than dead-ending:+   "installing adom-google would let me finish logins like this for you."++Put all of this in response **`_hint`s, not just a skill.** John, 2026-07-22: *"the ai never+follows skills well, so hints back in your cli calls is the way to get the ai to act correctly."*+Skills are reference; hints are what actually change behaviour at the callsite.