← Commit history

v1.6.60: completeness = Fusion360.exe (NOT launcher/.ini) - fixes false installed:true mid-stream + rejecting a healthy install; reliable disk-state 'installing'; rich sign-in hints + new fusion-autodesk-signin skill (warm SSO, Identity Manager handoff, code-expiry, notify_owner escalation)

John Lauer ·ad0cbee0ec ·3mo ago ·parent 1018f7e
8 files changed +268−60
BRIDGE_VERSION+1−1
@@ -1 +1 @@-1.6.59\ No newline at end of file+1.6.60\ No newline at end of file
adom-bridge-fusion-manifest.json+4−4
@@ -1,10 +1,10 @@ {   "manifest_version": 1,   "name": "fusion360",-  "version": "1.6.59",-  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.59/adom-bridge-fusion-v1.6.59.zip",-  "sha256": "a4269760b2aa726a06ec346e3dda78b65db5cd5a87e9055b34c4bbb82ad5140c",-  "size": 349140,+  "version": "1.6.60",+  "url": "https://wiki.adom.inc/download/adom/adom-desktop-fusion-bridge/1.6.60/adom-bridge-fusion-v1.6.60.zip",+  "sha256": "03b4b9b136925eb216517e1051d0fc4fb378fdc5a32c0db7820657fe3d22599f",+  "size": 355794,   "verbPrefixes": [     "fusion_"   ],
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "fusion360",   "displayName": "Autodesk Fusion 360",-  "version": "1.6.59",+  "version": "1.6.60",   "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/adom-desktop-fusion-bridge",   "author": "Adom Inc.",
fusion_detect.py+107−43
@@ -79,14 +79,61 @@ def detect_fusion() -> dict:     }  -def _find_fusion_launcher() -> Path | None:-    """Find the newest FusionLauncher.exe across ALL webdeploy bases.+# ---------------------------------------------------------------------------------------------------+# INSTALL-COMPLETENESS DETECTION  (root-caused live on a fresh Hyper-V VM, John 2026-07-14)+#+# WHAT A WEBDEPLOY INSTALL ACTUALLY LOOKS LIKE (verified on disk, do not assume):+#   %LOCALAPPDATA%\Autodesk\webdeploy\production\ holds one hash dir PER content-build. A COMPLETE+#   Fusion install is ~615 files (Fusion360.exe ~880 KB + hundreds of DLLs + Qt/ + Python/ + ...).+#   BUT Autodesk ALSO drops a tiny 8-file launcher STUB dir next to it: icons + FusionLauncher.exe+#   + a small FusionLauncher.exe.ini (~412 bytes). So at steady state there are TWO hash dirs.+#+# THE TRAP that broke the fresh VM (and the two earlier "fixes" that were WRONG):+#   * Keying "installed" off FusionLauncher.exe EXISTING -> true mid-stream (launcher lands early) ->+#     premature fusion_start -> "Error Launching Streamed Application ... .ini missing/incomplete".+#   * Keying off FusionLauncher.exe.ini existing / its SIZE -> ALSO wrong: the COMPLETE app dir has+#     NO .ini at all; only the throwaway stub carries a 412-byte .ini. A size gate would REJECT a+#     perfectly good install and ACCEPT nothing. (Measured it: complete app dir = 615 files, no .ini.)+#+# THE RELIABLE SIGNAL: the real application binary Fusion360.exe. It only exists (full size) when the+#   payload finished streaming. Gate "installed" on Fusion360.exe, NEVER on the launcher or its .ini.+# ---------------------------------------------------------------------------------------------------+_FUSION_APP_EXE = "Fusion360.exe"     # the ACTUAL app binary (~880 KB); the completeness marker+_MIN_APP_EXE_BYTES = 200_000          # guards against a 0-byte placeholder the streamer writes early+++def _app_dir_complete(subdir: Path) -> bool:+    """A production hash dir is a COMPLETE Fusion install ONLY when it contains the real app binary+    Fusion360.exe at full size. This is process-independent and version-stable. Do NOT substitute+    FusionLauncher.exe (lands early, mid-stream) or FusionLauncher.exe.ini (absent from the complete+    app dir; only the launcher stub has one). See the block comment above for the on-disk anatomy."""+    exe = subdir / _FUSION_APP_EXE+    try:+        return exe.exists() and exe.stat().st_size >= _MIN_APP_EXE_BYTES+    except OSError:+        return False -    Checks the per-user (%LOCALAPPDATA%) AND the system-wide (%ProgramFiles%,-    from `--globalinstall`) locations. Prefers a COMPLETE install (one that also-    has FusionLauncher.exe.ini) and, among those, the newest by mtime.-    """-    candidates = []  # (has_ini, mtime, launcher_path)++def _any_app_complete() -> bool:+    """True if ANY webdeploy hash dir holds a complete Fusion (Fusion360.exe). This is the single+    source of truth for 'Fusion is installed'."""+    for base in _WEBDEPLOY_BASES:+        try:+            if base.exists():+                for subdir in base.iterdir():+                    if subdir.is_dir() and _app_dir_complete(subdir):+                        return True+        except OSError:+            continue+    return False+++def _find_fusion_launcher() -> Path | None:+    """Return the FusionLauncher.exe to launch, but ONLY when a COMPLETE install exists (a hash dir+    with Fusion360.exe). Otherwise None -> detect_fusion() reports NOT installed, and readiness+    surfaces the streamer as installing. Prefers the launcher living beside the newest complete app;+    falls back to any FusionLauncher.exe on disk, then to Fusion360.exe itself."""+    complete_dirs = []   # (mtime, subdir)     for base in _WEBDEPLOY_BASES:         if not base.exists():             continue@@ -95,41 +142,63 @@ def _find_fusion_launcher() -> Path | None:         except OSError:             continue         for subdir in subdirs:-            launcher = subdir / "FusionLauncher.exe"-            if not launcher.exists():+            if _app_dir_complete(subdir):+                try:+                    mtime = subdir.stat().st_mtime+                except OSError:+                    mtime = 0.0+                complete_dirs.append((mtime, subdir))++    if not complete_dirs:+        return None                                   # nothing complete -> NOT installed+    complete_dirs.sort(key=lambda c: c[0], reverse=True)   # newest complete install wins+    newest = complete_dirs[0][1]+    launcher = newest / "FusionLauncher.exe"+    if launcher.exists():+        return launcher+    # complete app dir lacks its own launcher (unusual) - use any FusionLauncher.exe, else the app exe+    for _mt, subdir in complete_dirs:+        cand = subdir / "FusionLauncher.exe"+        if cand.exists():+            return cand+    return newest / _FUSION_APP_EXE+++def _incomplete_webdeploy_present() -> bool:+    """Disk-state (process-INDEPENDENT) 'a fresh install is actively streaming' signal: some hash dir+    has FusionLauncher.exe but NO complete Fusion360.exe exists ANYWHERE yet. Why disk-state and not a+    tasklist check: the Autodesk streamer spawns short-lived per-chunk worker processes, so+    _installer_running() reads False most of a live multi-GB stream (flickered True only 3 of 25 polls+    during a real install, John 2026-07-14). Why gate on 'no complete app anywhere': once the app IS+    complete, the leftover launcher stub (FusionLauncher.exe, no Fusion360.exe) is NORMAL and must NOT+    read as 'still installing'. (An auto-UPDATE that streams a new build beside a complete old one is+    handled separately by fusion_update_in_progress(), not here.) Never raises."""+    if _any_app_complete():+        return False                                  # a complete install exists -> not installing+    for base in _WEBDEPLOY_BASES:+        try:+            if not base.exists():                 continue-            has_ini = (subdir / "FusionLauncher.exe.ini").exists()-            try:-                mtime = subdir.stat().st_mtime-            except OSError:-                mtime = 0.0-            candidates.append((has_ini, mtime, launcher))--    if not candidates:-        return None-    # Only a COMPLETE install (FusionLauncher.exe.ini present) counts as installed. A hash dir with-    # FusionLauncher.exe but NO .ini is a half-streamed/corrupt install - returning it made-    # detect_fusion() report installed:true mid-stream, so a fusion_start fired into it and hit-    # "Error Launching Streamed Application ... .ini is missing or incomplete" (fresh Hyper-V VM,-    # John, 2026-07-14). If nothing is complete, report NOT installed (readiness then surfaces the-    # streamer as installing).-    complete = [c for c in candidates if c[0]]-    if complete:-        complete.sort(key=lambda c: c[1], reverse=True)   # newest complete install wins-        return complete[0][2]-    return None---_MIN_VALID_INI_BYTES = 512   # the truncated launcher .ini that broke the fresh VM was 412 bytes+            for subdir in base.iterdir():+                if subdir.is_dir() and (subdir / "FusionLauncher.exe").exists():+                    return True                       # launcher present, no complete app -> streaming+        except OSError:+            continue+    return False   def _clean_incomplete_webdeploy() -> list:-    """Remove webdeploy production hash dirs that are INCOMPLETE: a FusionLauncher.exe present but its-    FusionLauncher.exe.ini missing or truncated (< _MIN_VALID_INI_BYTES). These are the residue of an-    interrupted stream that throws 'Error Launching Streamed Application'. Only ever called AFTER a-    launch failure (we already know something is corrupt), so aggressive cleanup is safe. Returns the-    dir names removed. Never raises."""+    """Remove the residue of an INTERRUPTED stream so the next fusion_install_fusion re-streams clean.+    Only ever called AFTER a launch failure (we already know something is corrupt).++    SAFETY GUARD: if a COMPLETE install (Fusion360.exe) exists anywhere, do NOTHING and return [] -+    the launcher-only stub dir beside a complete app is a LEGIT part of the install, not corruption,+    and nuking it would break a working Fusion. Only when NO complete app exists (a genuinely half-+    streamed state) do we remove the partial launcher-bearing dirs. Never raises. (John 2026-07-14:+    the old .ini-size test would have deleted the legit stub of a healthy install.)"""     import shutil+    if _any_app_complete():+        return []                                     # healthy install present - never touch it     removed = []     for base in _WEBDEPLOY_BASES:         if not base.exists():@@ -141,12 +210,7 @@ def _clean_incomplete_webdeploy() -> list:         for subdir in subdirs:             if not (subdir / "FusionLauncher.exe").exists():                 continue-            ini = subdir / "FusionLauncher.exe.ini"-            try:-                ok = ini.exists() and ini.stat().st_size >= _MIN_VALID_INI_BYTES-            except OSError:-                ok = False-            if not ok:+            if not _app_dir_complete(subdir):         # partial: launcher but no real app binary                 try:                     shutil.rmtree(subdir, ignore_errors=True)                     removed.append(subdir.name)
server.py+28−11
@@ -21,7 +21,7 @@ from pathlib import Path # Ensure the plugin root is on the path sys.path.insert(0, str(Path(__file__).parent)) -from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress, find_licensing_dialog, family_windows+from fusion_detect import detect_fusion, ensure_fusion_running, wait_for_addin, _is_fusion_running, fusion_update_in_progress, find_licensing_dialog, family_windows, _incomplete_webdeploy_present from handlers.open_design import handle_open_design from handlers.close_fusion import handle_close_fusion, handle_fusion_stop, handle_fusion_kill from handlers.dismiss_recovery import dismiss_recovery_dialog@@ -1136,7 +1136,12 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:     # (found live on a fresh VM 2026-07-05: NO_WEBDEPLOY on disk, yet readiness said     # ready). detect_fusion() is a fast filesystem scan, so pay it every call.     raw_installed = bool(detect_fusion().get("installed"))-    streaming = _installer_running()+    # `streaming` must be RELIABLE. _installer_running() is a tasklist check, but the Autodesk streamer+    # spawns short-lived per-chunk workers, so it reads False most of a live multi-GB stream (flickered+    # True only 3 of 25 polls during a real install, John 2026-07-14). OR in the disk-state signal - a+    # webdeploy hash dir with FusionLauncher.exe but a missing/truncated .ini - which is present for the+    # WHOLE stream. Now `installing` is trustworthy, not racy.+    streaming = _installer_running() or _incomplete_webdeploy_present()     running = _is_fusion_running() if raw_installed else False     # FRESH-INSTALL COMPLETENESS GUARD (John caught this live 2026-07-14 on a fresh Hyper-V VM):     # the streamer writes FusionLauncher.exe BEFORE it finishes writing FusionLauncher.exe.ini, so@@ -1255,15 +1260,27 @@ def _handle_fusion_readiness(fusion_info: dict, args: dict) -> dict:                 "10-30 min). Meanwhile, wrap modeling/export verbs in a short retry/catch-loop to "                 "ride the up-windows rather than treating an intermittent 'not running' as fatal.")     elif needs_signin:-        hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, "-                "add-in cannot serve) - NOT ready. AUTOMATE the sign-in (see the fusion-onboarding "-                "skill for the proven sequence: desktop_screenshot_window the Fusion hwnd and READ "-                "its screenshots[] array - owned popups ride back as child shots; UIA-click 'Sign In', "-                "'Continue with Google' (warm SSO), then isTrusted-click 'Open Product' + 'Always "-                "allow'+'Open' on the protocol overlay), then poll until ready:true. NEVER enter the "-                "password/2FA yourself; if the flow truly needs the user (password/2FA prompt), call "-                "fusion_notify_owner {title, body} - it toasts their MAIN computer cross-AD. Last "-                "resort only.")+        hint = ("Fusion is RUNNING but stuck at the first-run Autodesk SIGN-IN (main UI blocked, add-in "+                "cannot serve) - NOT ready. FULL PLAYBOOK: the `fusion-autodesk-signin` skill. Proven "+                "sequence + the pitfalls that cost hours on a fresh Hyper-V VM (John 2026-07-14): "+                "(1) BLANK white webview on first launch? fusion_stop then fusion_start - the sign-in "+                "renders 'Welcome to Fusion' + a 'Sign In' button only after a clean relaunch. "+                "(2) ASK the user FIRST (AskUserQuestion): do they have a warm Google/Apple/Microsoft "+                "session for the account LINKED to their Autodesk login? Offer to drive it. A truly "+                "fresh box has NO warm browser session, so 'Continue with Google' still hits a password "+                "wall. (3) Click Fusion 'Sign In' -> it opens the DEFAULT browser (Edge) to the OAuth. "+                "Edge windows DO exist even if PowerShell EnumWindows looks empty - that is CLIXML "+                "progress noise; set $ProgressPreference='SilentlyContinue', and just "+                "desktop_screenshot_window the Edge hwnd (PrintWindow works backgrounded). "+                "(4) In the browser: 'Continue with Google' -> type the user's EMAIL (not secret) -> if "+                "it asks for a PASSWORD/2FA, STOP and fusion_notify_owner {title, body} to toast their "+                "MAIN computer so THEY type it; NEVER enter password/2FA yourself. (5) Handoff back to "+                "Fusion is via the 'Autodesk Identity Manager' protocol - Edge shows a 'This site is "+                "trying to open...' overlay: tick 'Always allow' + click 'Open'. (6) SPEED MATTERS: the "+                "sign-in code EXPIRES in ~2 min; on a high-latency VM slow screenshot/click round-trips "+                "let it expire ('Sign-in request expired') and you loop. Minimize steps; if it expires, "+                "click Fusion 'Sign In' again for a FRESH code (the browser session stays warm). "+                "Then poll until ready:true.")     elif ready and stale_info.get("addinStale"):         hint = stale_info["_staleHint"]     elif ready:
skills/fusion-autodesk-signin/SKILL.mdadded+100
@@ -0,0 +1,100 @@+---+name: fusion-autodesk-signin+description: Drive the first-run Autodesk sign-in that blocks a freshly installed Fusion 360, from a headless/remote Adom desktop (pup/adom-desktop). Covers the whole browser OAuth handoff and every pitfall that cost hours on a fresh Hyper-V VM — blank sign-in webview, invisible Edge windows, warm Google/Apple/Microsoft SSO, the Autodesk Identity Manager protocol handoff, fast-expiring sign-in codes, and escalating the password/2FA step to the human without ever typing it. Trigger words — fusion signing in, welcome to fusion, sign in - autodesk, needsSignin, fusion stuck signing in, autodesk oauth, fusion login blocked, drive fusion sign in, fusion sign in on fresh box.+---++# Fusion 360 Autodesk sign-in — the drive-it playbook++A freshly installed Fusion launches to a first-run **Autodesk sign-in** that BLOCKS the main thread,+so the AdomBridge add-in never serves and `fusion_readiness` reports `ready:false, needsSignin:true`.+This skill gets you from that wall to `ready:true`. It is the durable record of a live drive on a+fresh Hyper-V VM (John, 2026-07-14); every step below is a thing that actually went wrong once.++**The one hard rule:** you NEVER type the user's Autodesk/Google password or 2FA. You may type their+**email address** (not a secret) and click every button. When a real secret is needed, you escalate+to the human with `fusion_notify_owner` and let THEM type it.++## Where you are (confirm first)+`fusion_readiness` → `installed:true, running:true, ready:false, needsSignin:true`. The Fusion window+title is "Signing in - Autodesk Fusion" or "Welcome to Fusion". Get its hwnd (Get-Process Fusion360+→ MainWindowHandle) and `desktop_screenshot_window {hwnd}` to see it. `desktop_screenshot_window` uses+PrintWindow, so it captures backgrounded/occluded windows and their owned child dialogs — use it, not+a screen grab.++## Step 0 — ASK the user before you drive (AskUserQuestion)+Sign-in needs a human decision, so ask up front — do NOT silently start clicking:+- **Do they have a warm browser session** (Google / Apple / Microsoft) for the account **linked to+  their Autodesk login?** If yes, "Continue with <provider>" clicks through with no password.+- **On a truly fresh box there is usually NO warm session** — the provider will demand a password.+  Confirm they're OK finishing the password/2FA step themselves (you'll toast them when it's time).+- Offer the choice: you drive the clicks and escalate only the secret step, OR they'd rather remote+  in and do it. Respect their answer.++## Step 1 — Blank white webview? RESTART Fusion.+The very first launch after install often renders the sign-in webview **blank white** (just a title+bar, no button). Do not fight it: `fusion_stop` then `fusion_start`. After a clean relaunch it renders+"Welcome to Fusion" + a real **Sign In** button. (This single restart unblocked the whole flow.)++## Step 2 — Click Fusion's "Sign In" (it opens the browser)+The sign-in webview exposes **0 accessibility controls** (it's Chromium), so click by IMAGE+coordinates: `desktop_screenshot_window {hwnd}` returns a `shotId`; then+`desktop_click {space:"image", shotId, x, y, hwnd}` — passing `hwnd` FOCUSES that window first, which+is essential (a click without it lands on whatever is foreground). Maximize the window first+(`desktop_set_window_state {hwnd, state:"maximize"}`) so the button is unambiguous. Fusion says "We+will take you to your web browser to sign in" and opens the **default browser** (Edge) to the OAuth.++## Step 3 — Find the Edge OAuth window (it IS there)+The Edge window can look invisible: `Get-Process msedge` shows background procs and your window+enumeration may print nothing. That "nothing" is almost always **PowerShell CLIXML progress noise+eating your output**, NOT a missing window. Fixes:+- Put `$ProgressPreference='SilentlyContinue'` at the TOP of the PS script.+- Enumerate `Chrome_WidgetWin` class windows (the OAuth tab is titled "Sign in - Autodesk"); a 2-tab+  window reads as "... and 1 more page".+- Then `desktop_screenshot_window {hwnd}` the Edge window — PrintWindow renders it regardless.+- The OAuth URL is handed to Edge over IPC, so it is NOT in any msedge command line — don't look there.++## Step 4 — Drive the OAuth (warm SSO path)+On the Autodesk sign-in page (`signin.autodesk.com/?flowId=...`): click **"Continue with Google"**+(or Apple/Microsoft, matching the linked account). On the provider page, click the email field and+`desktop_type` the user's **email** (e.g. [email protected] — an address, not a secret), click **Next**.+- If the provider recognizes a warm session → it proceeds with no password. +- If it shows **"Enter your password"** or a 2FA challenge → **STOP**. This is the human wall.++## 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+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.)++## Step 6 — The handoff back to Fusion (Autodesk Identity Manager)+After auth, the browser lands on `signin.autodesk.com/idmgr/callback#code=...` ("You're signed in").+The code is delivered to Fusion by the **Autodesk Identity Manager** protocol, so Edge pops a+**"This site is trying to open Autodesk Identity Manager"** overlay: tick **"Always allow"** and click+**"Open"**. (Always-allow means later sign-ins skip the prompt.) If instead you see a big+**"Open Product"** button, click it.++## Step 7 — SPEED: the code expires (~2 min)+The single biggest time-sink on a slow/remote box: each screenshot+click round-trip can take 20-30s,+and the Autodesk sign-in code **expires in about 2 minutes** ("Sign-in request expired"). If you dawdle+across the tabs, it dies and you loop forever. Mitigations:+- Do the fewest possible round-trips between clicking Fusion "Sign In" and the callback.+- Once the Autodesk/browser session is warm (after the first successful auth), a fresh Fusion+  "Sign In" click completes almost instantly — so if a code expires, just click Fusion "Sign In"+  AGAIN for a fresh code; you don't redo the whole login.+- If Edge tabs get stuck on a stale expired-callback URL, clear them (`taskkill /F /IM msedge.exe` —+  browser-specific and safe; Autodesk cookies persist so it stays warm) and re-click Fusion "Sign In".++## Step 8 — Confirm+Poll `fusion_readiness` until `ready:true` (needsSignin clears, the add-in responds). Now Fusion is+drivable: `fusion_open_lbr`, `fusion_build_library_3d`, exports, GPU/graphics checks, etc.++## Escalation ladder (in order)+1. Warm-SSO click-through (no secret) — try first.+2. Type the user's EMAIL only; let the provider's warm session finish it.+3. `fusion_notify_owner` for a genuine password/2FA/UAC/physical wall — the ONLY time a human is needed.+4. Ask the user to remote in — last resort if toasting doesn't land.++## Related+- `fusion-onboarding` — the install that precedes this (streams Fusion, then hands off here).+- `fusion-bridge-dev` — how the bridge detects installed/installing/ready (Fusion360.exe completeness).
skills/fusion-bridge-dev/SKILL.md+22
@@ -296,3 +296,25 @@ skill gets read. In the bundled copies, rewrite image refs to **absolute wiki UR (`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).
skills/fusion-onboarding/SKILL.md+5
@@ -56,6 +56,11 @@ the sign-in page has **Create account** (free) — and Fusion offers a **free tr tier that's enough for viewing and a lot of work. **Don't auto-enter their password/2FA; do drive everything else.** +> **Use the dedicated `fusion-autodesk-signin` skill for the full drive** — it has the complete,+> battle-tested sequence and every pitfall (blank webview → restart Fusion, invisible-Edge / CLIXML+> noise, warm Google/Apple/MS SSO, the Identity Manager protocol overlay, fast-expiring codes, and+> `fusion_notify_owner` escalation). The summary below is the mechanism; that skill is the how-to.+ ### The mechanism (why naive clicking fails) Fusion's "Sign In" opens the **default browser** to an Autodesk OAuth page. When auth completes the browser lands on `signin.autodesk.com/idmgr/callback#code=…`, whose JS posts the code to Fusion's