app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
← Commit history
docs: 0.5.x rounds — quit ladder as built, import-temp leak rounds 6–8, ecosystem rows 43–44, README/SKILL guidance (code publish follows the live gates)
5 files changed
+657−10
README.md+165−1@@ -9,7 +9,7 @@ One native binary. No Python, no PowerShell, no interpreter to provision: ab spawns `swx-bridge.exe` directly (`spawn.kind: "exe"`) and it is serving in about ten milliseconds. -## What it does — 61 verbs+## What it does — 63 verbs | Area | Verbs | |---|---|@@ -57,6 +57,31 @@ holding the request. Classic desktop installs still attach directly over COM. `errorCode:"host_app_launch_blocked"` now means only that **no shortcut could be found** (`SWX_SOLIDWORKS_SHORTCUT` points the bridge at one). +**The launcher stalls, so the launch is a state machine** (v0.4.0, full write-up+in [`docs/LAUNCH-RESILIENCE.md`](docs/LAUNCH-RESILIENCE.md)). Measured on+2026-09-04: five of five launches through `cmd /c start` produced a launcher log+that stopped at `SecurityContext:` and never logged `Run Begin` — the chain sat+idle at ~50 MB forever — while every interactive launch of the SAME shortcut with+the SAME arguments succeeded. So `swx_start` now invokes ONE route, watches the+launcher's own log, and on a stall ends **only the chain it started itself**+before trying the next route (`explorer` → `ShellExecuteExW` → `Start-Process` →+`cmd /c start`), remembering the one that works. It also tracks the attempt on+disk, so calling `swx_start` during a launch reports it instead of starting a+second one — the promise the old hint made and the old code did not keep.++**The kill allowlist is two images, and the INVOKE — not the census — decides+what is ours.** The bridge may terminate `CATSTART.exe` and+`SWXDesktopLauncher.exe`, and only the instances descending from a root the+invocation itself handed back (`ShellExecuteExW`'s process handle, or+`Start-Process -PassThru`'s pid), with image AND creation time re-checked on the+open handle at the moment of the kill. Routes that cannot say what they started+(`explorer`, `cmdStart`) never clean up at all. It must **never** kill+`3DEXPERIENCELauncher*` — those are the Platform's own daemons, they are not+required for a launch (the successful one had no Backbone running), and a real+user launch rebuilds them. `sldworks.exe` is never killed here either: ending the+user's CAD session is `swx_kill`, which is gated behind their explicit opt-in.+There is no image-name kill and no tree kill anywhere in the launch path.+ **Exports are copies; save-as is a rename.** `swx_export*` uses `SaveAs3` in `Silent|Copy` mode, so the open document keeps its identity (v0.2 did not, and silently renamed it). `swx_save_as` is the deliberate rename; `copy:true` makes@@ -244,6 +269,145 @@ not create, and it never launches or stops SOLIDWORKS. ## Roadmap +- v0.5.4 (this): **adversary round 8 — the anchor was right, its failure was+ silent.** The drain compares a path SOLIDWORKS reported against a `%TEMP%` this+ process resolved, and the two can legitimately differ (ab may run under a+ service profile or a redirected TEMP, and the leaked temps were written by an+ earlier, dead bridge process). On a mismatch 0.5.3 matched nothing and reported+ `closed:0, done:true` with *"Every open STEP/IGES import temporary was closed"*+ — a confident wrong claim about the user's session, and the step an operator+ would have read as finished before the quit gate refused on open documents.+ Nothing was ever wrongly CLOSED; the report was wrong. Now: `tempRoot` is in the+ response, documents carrying the launcher's `…\swx<pid>\IC~~\<file>` tail under+ an unresolved directory are counted as `importTempsElsewhere` with a sample+ path, and a matched-nothing drain reports `done:false` with a hint saying what+ to point it at. The candidate roots are this process's `%TEMP%` **and** the+ interactive user's `HKCU\Environment\TEMP` expanded — a document under either+ qualifies, no root still refuses — because the user's registry value is the+ least invasive way to learn the temp directory of the session SOLIDWORKS is in.+ The 8.3 rule is now exact rather than merely tight: the mangled form needs its+ six-character head, the COLLISION form (`DREW~1` for `drew`, which is what a+ domain-joined machine really produces beside `drew.ADOM`) needs the whole name,+ and a loose prefix is rejected. 250 tests.+- v0.5.3: **adversary round 7 — the once-swept precondition, the+ one-sample exit, and the anchor that undid itself.** The drain's `%TEMP%` anchor+ was OR-ed with a shape test, which made it advisory: any path whose tail read+ `…\AppData\Local\Temp\swx<digits>\IC~~\<file>` qualified for a no-prompt close+ wherever it lived — a restored profile backup on another volume, another user's+ profile — and when `%TEMP%` was unreadable the shape alone decided. The resolved+ temp dir is now the whole location test (8.3-aware on both sides), and with no+ resolved root the drain refuses (`temp_root_unknown`) instead of reporting that+ it found nothing. In the ladder: the preconditions were swept ONCE before the+ loop while the code promised "before every rung", so a document opened while+ rung (a) settled was still open when rung (b) fired — they are now re-swept per+ rung, identity included. The exit was judged from ONE census read 1.5 s after+ the rung, so a real `ExitApp` that returns before the process goes read as+ "declined" and the ladder ESCALATED into an exiting application — it now polls+ up to the rung's own budget, and the simulator can model a slow exit. The dialog+ census could not see a 3DEXPERIENCE sign-in modal, because a web view's window+ belongs to a `swCefSubProc.exe` child; it now covers those pids, by parentage.+ The open-document count is cross-checked against the enumeration. Rungs (c)/(d)+ re-verify that the hwnd still belongs to the same process before sending+ anything. 248 tests.+- v0.5.2: **the graceful QUIT ladder, and the 0.5.1 adversary round.**+ `swx_stop` v1 called `ExitApp`, asked the COM running-object table whether+ SOLIDWORKS was still there, and reported a DECLINE as `success:true`. v2 is a+ measured ladder — rung (a) `ExitApp` through rung (f) the Restart Manager+ *without* `RmForceShutdown`, pinned at (a) — under three laws: **never a kill**+ (there is no terminate call in the stop path, and a test pins its absence),+ **never answer a dialog** (a new modal stops the ladder permanently and is+ reported, not clicked), and **nothing is disowned until the process is+ verifiably gone** (v1 cleared its registries first, so a declined quit left the+ bridge unable to save its own documents). Seven preconditions are re-measured+ per call and an UNKNOWN in any of them refuses; `force` waives the UNSAVED+ question only, never the open-document one. Every COM reference is released+ before and after each rung, and outstanding refs are named in the report,+ because a "decline" measured while we still held a reference is not evidence.+ Success is *this pid with this creation time absent from the census*, never a+ return value. Also folded in: the 0.5.1 adversary round (a drain that could+ close a user's saved file, a silent no-op sweep, ownership rows that could be+ reassigned to another document). `tools/quit-gate.py` is written; **no rung has+ ever run against a real SOLIDWORKS**, so `docs/QUIT-INVESTIGATION.md` §5.11+ and the ecosystem table both say *unproven live*.+- v0.5.1: **the 0.5.0 adversary round, plus busy-vs-dead.** `is_import_temp`+ proved the wrong thing — two tokens anywhere in the path, in any order — so a+ user's own saved file under folders they had named `Temp\swx-archive\…\IC~~\`+ qualified and would have been closed with no save prompt. It is now a segment+ walk requiring the launcher's contiguous `Temp\swx<pid>\…\IC~~\` layout.+ An unreadable document count is now UNKNOWN-and-truncated instead of falling+ through to the 30s enumeration it was meant to prevent, and a verb that cannot+ resolve its target says `document_list_unavailable` rather than "no such+ document". Making the mock report the realistic temp path (as `GetPathName`+ does) immediately exposed three more real defects it had hidden — including the+ bridge refusing to close the document it had just imported. And, from live+ evidence: `hung` now carries `verdict:"busy"|"dead"|"unknown"` with+ `cpuActiveSeconds`/`memoryDeltaMB`, because a session at 17.9 GB was reported+ hung for five minutes while burning 70% of a core, and it recovered. 202 tests.+- v0.5.0: **import / document-lifecycle robustness.** Root-caused a live+ 1342-document / 8 GB leak — the same two STEP fixtures imported over and over+ and never closed, the real root of the crate-build memory exhaustion.+ Enumeration now DEGRADES instead of wedging: `swx_docs`/`app_state` read+ `GetDocumentCount` first and, above a cap, return `{count, truncated, sample}`+ rather than marshalling thousands of handles for 30s and getting the com host+ killed. New `swx_close_import_temps` drains the leaked `…\Temp\swx<pid>\IC~~\…`+ conversion temps in the DEFAULT posture (the path is the ownership proof, and+ survives the pid change that made the leak un-ownable); `swx_import` asserts a+ net +1 and says the document stays open until closed. `swx_import`'s contract+ is unchanged. Ecosystem row 43 — we are alone. 191 tests.+- v0.4.4: **evidence bookkeeping.** The fourth adversary pass again found+ nothing consent-affecting — the process-side law has survived four rounds — and+ closed the last version of the same mistake: "our chain existed before this+ log" is true of *every* log written after our chain started, and the newest-+ first listing meant a second launch's log was preferentially read. A log is now+ chosen by AUTHORSHIP (the launcher stamps its writing thread's id on every+ line, and `thread_owner` resolves it to a process in our own chain), or by+ being the sole candidate with no foreign chain alive, or not at all. Plus:+ every per-route fact resets per route (`notConfirmed` was unreachable after+ route 1), `RouteTry` carries its own evidence so the gate's per-route table+ stops printing one row's value on every row, and the gate itself now exits 3+ on an unreadable probe instead of reporting a pass it could not see. 186 tests.+- v0.4.3: **the law applied to evidence.** A third adversary pass found+ no consent-affecting defect — it could not make the bridge terminate a process+ it did not start — but the bridge still adopted a stranger's EVIDENCE: a+ launcher log was claimed on "newer than the baseline" alone, so a launch the+ user started mid-watch supplied our `Run Begin` and taught the bridge that a+ route which started nothing was the one that works. A log is now ours only+ when our own attributed chain existed to have written it. Plus: a handle-proven+ root seeds the lineage walk even when the census cannot see it (a shim or+ wrapper would otherwise leave a real chain running under a "clean recovery"+ report), "never confirmed the root" became its own stop condition, and a shared+ temp filename that raced across HTTP worker threads was fixed. 181 tests.+- v0.4.2: **the attribution law.** A second adversary pass proved 0.4.1+ still terminated a user-started launcher chain, because "the first CATSTART+ after our invoke" was a guess dressed as an inference. The rule is now **no+ handle, no kill**: a process is ended only when the invoke itself handed back+ its root (`ShellExecuteExW`'s handle, `Start-Process -PassThru`'s pid) and it+ descends from that root; the two routes that cannot say what they started+ never clean up, and a route that leaves something unaccounted-for stops the+ machine. Plus: a failed launcher-log listing is UNKNOWN rather than "any log+ is ours" (it would have fabricated the gate's central measurement),+ `is_newer_than` consults both clocks and distrusts a stamp the filesystem+ contradicts, `timed_out` is carried explicitly instead of sniffed out of an+ error message, and a corrupt ownership registry falls back to its previous+ generation. 175 tests.+- v0.4.1: **the adversary round.** 14 failing tests written against+ 0.4.0's own claims, all now passing and part of the suite. The kill list is+ scoped by IDENTITY (baseline census + parent-pid lineage + a creation-time+ re-check on the open handle) instead of by clock, so a launch the user starts+ mid-flight is never swept up; `documents()` no longer believes an unreadable+ count; exhaustion needs the one signal that proves it rather than a memory+ threshold that flagged a healthy 13 GB session; `hung` needs a real COM+ timeout and not this bridge's own busy com host; a stale attempt from a dead+ bridge is marked abandoned; a cleanup that could not run says so and stops the+ machine; crash-reporter recovery ships DISABLED until the gate has seen a real+ UIA reply; and the registry and launch state are written atomically. 162 tests.+- v0.4.0: **launch resilience.** A launch state machine with stall+ detection, route fallback and a two-image kill allowlist; in-flight+ idempotence (a `swx_start` during a launch starts nothing); crash-reporter+ recovery, clicked once and by button name; honest `host_app_hung` /+ `host_app_exhausted` / `host_app_com_unreachable` reporting read WITHOUT COM;+ and bug #5 closed — the ownership registry is persisted and an unanswerable+ probe no longer disowns everything the bridge created. - v0.1: full verb surface in Python + a PowerShell COM sidecar, mock-tested. - v0.2: single Rust binary — 23 verbs, COM in a killable child process, crash recovery, a suite that drives the real binary.
SKILL.md+113−7@@ -43,6 +43,75 @@ adom-bridge --ai-thread "<your thread>" swx_bom '{"path":"C:\\designs\\gearbox.s to 240) blocks instead of polling. **Never invoke the launcher any other way meanwhile** — two launches means two sign-in windows on somebody's desktop.+ - **Calling `swx_start` during a launch launches NOTHING** (v0.4.0). It+ answers `stillLaunching:true, launchedNow:false` with the live `stage`+ (`launcherStarted` / `runBegin` / `helperSignIn` / `sldworksUp` /+ `attaching`), its age, and what to do — and it needs no `reason` while an+ attempt is in flight, so polling with it is always safe. `swx_status`+ carries the same `launch` block.+ - **A stalled launcher recovers itself.** The launcher chain sometimes starts+ and then stops at `SecurityContext:` without ever logging `Run Begin` — it+ then sits idle forever and SOLIDWORKS never appears. A supervisor inside+ the bridge watches the launcher's OWN log, and on a stall it ends **only+ the `CATSTART`/`SWXDesktopLauncher` chain it started itself** and tries the+ next launch route, remembering whichever one works. **There is nothing for+ you to retry, and nothing for you to clean up.**+ - **NO HANDLE, NO KILL — and the same rule for evidence.** A launcher log is+ read as this bridge's own only when the launcher's own writing-thread id+ resolves to a process in our chain, or it is the sole candidate with no+ other launch running; otherwise `unattributedLog:true` and it is not used.+ On a route that cannot say what it started, no log is attributed at all. One consequence worth knowing:+ if SOLIDWORKS comes up on `explorer` or `cmdStart`, the launch SUCCEEDS and+ the route is never remembered — `attached` proves SOLIDWORKS is usable, not+ that our route did it.+ - **NO HANDLE, NO KILL.** A process is only ever ended when the INVOKE ITSELF+ handed back its root (`ShellExecuteExW`'s process handle, or+ `Start-Process -PassThru`'s pid) and it descends from that root. The+ `explorer` and `cmdStart` routes cannot say what they started, so a stall on+ either reports `cleanup:"notAttributable"` and **terminates nothing** — the+ response says a launcher chain may remain that the bridge cannot identify,+ which the user can end from Task Manager. If a route leaves something the+ bridge cannot account for, the machine STOPS rather than starting another+ chain beside it.+ - **If SOLIDWORKS is running but unreachable, `swx_start` will not start a+ second one.** It answers `host_app_hung` (Windows says the window is not+ responding AND a COM call exceeded its budget — only the user can end it),+ `host_app_exhausted` (**`NewDocument` came back with no document** — the one+ signal that proves it; close the bridge's own documents, then ASK THE USER+ to restart, and do not retry in a loop), or `host_app_com_unreachable`+ (usually a sign-in window: ask the user to look at their screen).+ `swx_status.solidworks.memoryAdvisory` reports a very large session as an+ OBSERVATION — a 13 GB SOLIDWORKS can be perfectly healthy, and this bridge+ no longer calls one out of room on a threshold.+ - **"Not responding" is not "dead".** `swx_status.solidworks.verdict` is+ `busy` / `dead` / `unknown`, from the CPU and memory DELTA across two reads+ (`cpuActiveSeconds`, `memoryDeltaMB`). A session at 17.9 GB was+ not-responding with a COM timeout for five minutes while burning 70% of a+ core — and it recovered. `busy` means **tell the user to wait**; only `dead`+ (no CPU over ≥15s, memory flat, no dialog) means only they can end it. One+ reading never says dead — poll again.+ - **After a failed attempt the bridge waits** (`host_app_launch_backoff`,+ growing with each consecutive failure) and then tries ONE route rather than+ re-sweeping the plan. Retrying in a loop is how four launcher chains ended+ up on somebody's desktop; the honest move while it waits is to ASK THE USER+ to start SOLIDWORKS from their Start Menu.+ - **Crash-reporter recovery ships OFF** (`launch.crashRecovery:"off"`). When+ armed it clicks the crash reporter's "Restart SOLIDWORKS Design" button+ once — but only on a window OWNED by the SOLIDWORKS being launched, and only+ when ab structurally confirms the button exists. It is disabled by default+ because the reply shape it reads has never been seen on a real desktop.++- **NEVER kill the 3DEXPERIENCE launcher daemons.** `3DEXPERIENCELauncher.exe`,+ `3DEXPERIENCELauncherSysTray.exe` and `3DEXPERIENCELauncherService.exe` are+ the Platform's own background services and carry the user's Platform session.+ They are **not required** for a launch (a successful launch on 2026-09-04 ran+ with no Backbone at all), so killing one buys nothing and breaks something+ that is not yours — and **a real user launch rebuilds them**, which is the fix+ if they are genuinely broken. The bridge's own kill allowlist is exactly two+ images, `CATSTART.exe` and `SWXDesktopLauncher.exe`, and only instances it+ created itself, terminated by pid. Do not reach for `taskkill` /+ `process_kill` against any of this yourself; `sldworks.exe` included (that is+ `swx_kill`, and it is gated behind the user's explicit opt-in). - `errorCode:"host_app_launch_blocked"` now means only one thing: **the bridge could not FIND a shortcut**, not that the edition cannot be launched. `swx_status` reports `solidworks.shortcut` and@@ -78,6 +147,15 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke what tells you the model is what you meant. - Throwing it away is `swx_close_doc {"name":"Part1","save":false}`, or `swx_reset_workspace` to sweep every scratch document the bridge created.+- **`swx_import` opens one unsaved document and it STAYS OPEN** until you close+ or save it — exactly like `swx_new_document`. It reports `documentsOpened:1`+ and `netCountOk:true`. Importing in a loop without closing is what leaks+ documents (a live session was found holding 1342 STEP temps, 8 GB). If a count+ ever looks huge, `swx_docs` will say `truncated:true` with a `count` and a+ small `sample` instead of timing out — run **`swx_close_import_temps`**+ `{"reason":"…"}` to drain the leaked `…\Temp\swx<pid>\IC~~\…` conversion temps.+ That verb works in the default posture and can never touch a saved user+ document (a user file has a real path; a temp does not). ## The everyday flow @@ -191,13 +269,41 @@ adom-bridge --ai-thread "<thread>" swx_save_as '{"outputPath":"C:\\parts\\bracke and a document whose dirtiness SOLIDWORKS will not report is treated as UNSAVED, not clean. Reaching this verb at all needs the write opt-in, since it ends the user's session; a refusal closes nothing.-- **`swx_stop` fails loudly when SOLIDWORKS does not actually quit.** Asking is- not quitting: the host verifies by re-attaching, and if the application is- still there you get `success:false` with- `errorCode:"host_app_stop_declined"`. Almost always it is holding a- *"Save changes?"* modal nobody answered — `swx_get_app_state` names the- unsaved documents. **Do not reach for `swx_kill`**: that discards exactly the- work the dialog is asking about.+- **`swx_stop` quits ONLY an empty session, and proves the exit from the process+ census.** It is a measured ladder, pinned at rung (a) (`UserControl=false;+ ExitApp; release every COM reference`); rungs (b)-(f) exist and need an+ explicit `maxRung`, and none of them has ever run against a real SOLIDWORKS.+ Before it tries anything it re-measures seven preconditions, and **any one of+ them coming back UNKNOWN refuses** (`stop_preconditions_unknown`) — an+ unanswerable question is not permission. The commonest refusal is+ `open_documents_present`: **documents open means no**, and `force` does not+ waive it (`force` waives the UNSAVED question only). Success is *this pid with+ this creation time absent from the census* — never what `ExitApp` returned —+ and nothing the bridge owns is disowned until that is true, so a declined quit+ leaves you still able to save and close your own documents. If a dialog+ appears the ladder stops permanently and reports it+ (`stop_blocked_by_save_prompt` / `stop_blocked_by_dialog`): **this bridge never+ answers a dialog** — show the user `swx_state`'s background screenshot and let+ them answer. A decline is `success:false` with+ `errorCode:"host_app_stop_declined"` and the whole `rungsTried` list. **Do not+ reach for `swx_kill`**: it discards exactly the work the dialog is asking+ about, and on a Connected edition relaunching takes 1-3 minutes and can fail.+ `expectedSurvivorsAlive` (the 3DEXPERIENCE tray daemons, `ENOPLMCSAClient`,+ `EdmServerV6`) is CONSENT, not a leak — never end one. Every precondition is+ re-measured before EVERY rung, so a multi-rung ladder can refuse in the middle+ (`refusedMidLadder:true`) if a document appears while a rung is settling, and it+ never continues against a different process (`stop_identity_changed`).+- **`swx_close_import_temps` needs a resolved `%TEMP%`, and says so.** The drain+ identifies a temp by its LOCATION under this user's resolved temp directory —+ that path is the whole proof, because the close does not prompt. A path that+ merely looks like a temp (a restored profile backup, another user's profile) is+ not enough, and when `%TEMP%` cannot be resolved the verb refuses with+ `temp_root_unknown` rather than reporting that it found nothing to close. Read+ `tempRoot` and `importTempsElsewhere` in the reply: `closed:0` with+ `importTempsElsewhere:N` means N documents look like leaked temporaries but sit+ under a directory this bridge could not resolve (ab can run under a different+ profile or a redirected TEMP), so the drain is NOT done — `done:false` — and the+ hint says how to point it at the right root. - **`swx_close_window` is gated exactly like `swx_dismiss_blocking_dialogs`.** It refuses an application window (`errorCode:"window_is_application_frame"` — closing a SOLIDWORKS frame quits it, which is `swx_stop`'s job); it refuses a
docs/ECOSYSTEM-TABLE.md+10−2@@ -103,12 +103,13 @@ templates**, alisamsam **22**. | 35 | Host-ecosystem integration (Adom Bridge, Activity Log, skill) | ✓ native | **✓ native** | ✗ MCP clients only | ✗ | ✗ | ✗ | ✗ | | 36 | **Ownership persists across bridge/host restarts** (bridge-created docs stay closable without opt-in) | ✗ (no ownership model) | **✓ live 2026-09-08** — `owned-docs.json` keyed to the SOLIDWORKS process; proven create → host kill → close in default posture | ✗ | ✗ | ✗ | ✗ | ✗ | | 37 | **Honest UNKNOWN instead of a fabricated empty/zero** (doc list, counts, dialogs, interference) | partial | **✓ 0.4.x** — `docs_enumeration_failed`, `Option` counts, `unsaved_unknown`, `count:null` semantics in hints; 29 adversary tests | ✗ | ✗ | ✗ | ✗ | ✗ |-| 38 | **Hung / exhausted host detection** (Not Responding + real COM timeout; NewDocument-returns-nothing) | ✗ | **✓ 0.4.x** evidence-based `host_app_hung` / `host_app_exhausted`, memory advisory only (measured 17.4 GB hang, 13 GB healthy) — unproven on a live hang since fix | ✗ | ✗ | ✗ | ✗ | ✗ |+| 38 | **Hung / exhausted host detection, and BUSY vs DEAD** (Not Responding + real COM timeout; NewDocument-returns-nothing; CPU/memory delta over a sample) | ✗ | **✓ 0.5.1** evidence-based `host_app_hung` / `host_app_exhausted` + `verdict:"busy"\|"dead"\|"unknown"` from `cpuActiveSeconds`/`memoryDeltaMB` — measured live: 17.9 GB not-responding for ~5 min at ~70% of a core and it RECOVERED, so "not responding" now advises WAIT, not "end the task". One reading never says dead | ✗ | ✗ | ✗ | ✗ | ✗ | | 39 | **Stalled-launcher recovery with attribution law** (kill only a chain proven ours by handle; never daemons, never sldworks) | ✗ | **✓ shipped 0.4.4**, 4 adversary rounds, **live gate pending** | ✗ | ✗ | ✗ | ✗ | ✗ | | 40 | **Crash-reporter recovery** (click "Restart SOLIDWORKS Design" once, structured match, ledgered) | ✗ | **shipped, DISABLED by default** until the real control-tree reply is captured | ✗ | ✗ | ✗ | ✗ | ✗ | | 41 | **COM retry-on-reject** (busy host: RPC_E_CALL_REJECTED / RETRYLATER backoff on every call) | ? | **✓ live 0.3.3** — 50-read smoke gate, 0 under-reporting, 11 rejects absorbed | ✗ | ✗ | ✗ | ✗ | ✗ | | 42 | **Real-work proof** (multi-assembly build + BOM exported from the assembly, verified against a packet) | ✓ (electronics) | **✓ live** — NSF crate: 5 assemblies, 383 placements, 32-row BOM, 28/28 part numbers match | ✗ | ✗ | ✗ | ✗ | ✗ |-| 43 | **Import / resource lifecycle — no leaked temps, drainable, enumeration never wedges** | ✗ | **✓ 0.5.0** — enumeration degrades above a cap (`{count,truncated,sample}`, never a 30s timeout+kill); `swx_close_import_temps` drains STEP/IGES `…\Temp\swx<pid>\IC~~\…` temps in the DEFAULT posture (path is the ownership proof, survives a pid change); import asserts a net +1. Root-caused a live 1342-doc / 8 GB leak | ✗ | ✗ | ✗ | ✗ | ✗ |+| 43 | **Import / resource lifecycle — no leaked temps, drainable, enumeration never wedges** | ✗ | **✓ 0.5.0** — enumeration degrades above a cap (`{count,truncated,sample}`, never a 30s timeout+kill); `swx_close_import_temps` drains STEP/IGES `…\Temp\swx<pid>\IC~~\…` temps in the DEFAULT posture (path is the ownership proof, survives a pid change); import asserts a net +1. Root-caused a live 1342-doc / 8 GB leak. **0.5.3**: the resolved `%TEMP%` is the WHOLE location proof (8.3-aware, no shape-test fallback), and with no resolved root the drain refuses (`temp_root_unknown`) instead of closing nothing quietly. **0.5.4**: the report says which root it compared against (`tempRoot`) and counts documents that carry the launcher's tail but sit elsewhere (`importTempsElsewhere` + a sample), so a drain that matched nothing can no longer claim it drained everything — the mismatch case is real, because ab's `%TEMP%` need not be the one SOLIDWORKS used | ✗ | ✗ | ✗ | ✗ | ✗ |+| 44 | **Graceful QUIT that is measured, not assumed** (never a kill, never answers a dialog, proves the exit from the process census) | ✗ | **⚠ 0.5.3 mock-proven, UNPROVEN LIVE** — a rung ladder (a `ExitApp` → f Restart Manager *without* `RmForceShutdown`), pinned at rung (a); 7 re-measured preconditions where UNKNOWN refuses and `force` waives the UNSAVED question ONLY; every COM reference released before and after each rung, with outstanding refs named in the report; success = *this pid AND this creation time* absent from the census, never `GetActiveObject`; per-session children reaped while the documented platform daemons are left alone; a dialog stops the ladder permanently and is never answered; nothing disowned until the exit is verified. every precondition re-measured before EVERY rung (a mid-ladder refusal is a real outcome), the exit POLLED to the rung's budget rather than sampled once (a real `ExitApp` returns before the process goes), the dialog census covering the `swCefSubProc` web-view children by parentage, the count cross-checked against the enumeration, and the message rungs re-verifying window ownership before they send. `tools/quit-gate.py` proves the session was bridge-launched and has not been run | ✗ | ✗ | ✗ | ✗ | ✗ | *(42 numbered rows; rows 29–31 are the N/A block that accounts for most of the Fusion verb-count gap. Rows 36–42 added 2026-09-08 from the crate build and the launch-resilience rounds — the bar is "significantly more powerful than the MCP servers", so every robustness capability gets a row with its evidence level, not a slogan.)* @@ -236,6 +237,13 @@ value-per-effort: ## What is still unproven, and where +Row 44 is the newest and the loudest: **no rung of the quit ladder has ever run against a real+SOLIDWORKS.** `tools/quit-gate.py --baseline` is read-only and can be run any time; the rest ends+Drew's session and needs his go-ahead on an empty, bridge-launched one. Five of the six MCP+servers surveyed never attempt a quit at all, and the sixth reaches for a kill — so this is the+row where being *unproven* still puts us ahead of the field, and where being *wrong* would cost+the user a 1-3 minute relaunch that can fail. That asymmetry is why it ships pinned at rung (a).+ Nothing above changes the standing recommendation: **run `tools/live-smoke.py`** once SOLIDWORKS is back up, started from the 3DEXPERIENCE Platform. It is what converts the ⚠ mock cells in column B into ✓ live — or finds the next bug, which is what happened all three previous times. The assembly verbs
docs/IMPORT-LEAK.md+173@@ -92,3 +92,176 @@ registry the respawn cleared). Plus coverage in the e2e verb walk. The live 1342 still clear only on the next SOLIDWORKS restart (the launch gate needs one anyway); `swx_close_import_temps` is what prevents the next pile.++## Adversary round on 0.5.0 → shipped in 0.5.1++Four findings, one of them data loss in the DEFAULT posture.++**F1 (HIGH — data loss). `is_import_temp` proved the wrong thing.** It asked only+that `\temp\swx` and `\ic~~\` each appear SOMEWHERE in the path, in any order, in+any folders. So a user's own saved file qualified — `D:\Temp\swx-archive\2026\IC~~\customer-final.sldprt`,+`…\Temp\swxnotes\IC~~\keep.sldprt`, or anything with an `IC~~` folder ABOVE a+`Temp\swx…` one — and `cmd_close_import_temps` closes what qualifies with+`CloseDoc`, which does not prompt.++It is now a SEGMENT WALK requiring the launcher's actual contiguous layout: a+`Temp` segment, then **immediately** a `swx<digits>` segment (the pid; the 8.3+short form `SWX107~1` counts), then an `IC~~` segment BELOW it, then a file.+Case-insensitive, either slash. The pid VALUE is deliberately not checked — the+1342 leaked temps belonged to a dead bridge process, and scoping the drain to+"documents open in the session we are attached to" is what makes it useful.++**F2 (MEDIUM). An unreadable count fell through to the full enumeration** —+`count.map(..).unwrap_or(false)` — which is the 30s wedge the cap exists to+prevent, arriving exactly when the session is under the memory pressure that made+the count unreadable. `None` is now UNKNOWN-and-truncated: `{count:null,+truncated:true}` with a hint saying null is not zero, and no marshal is attempted.+`app_state` the same, so ownership is not pruned against it either. And a verb+that cannot resolve its target now refuses with **`document_list_unavailable`**+instead of the confident wrong "no open document is titled X".++**F3 (MEDIUM). The mock reported `path:""` for a temp** while the real+`GetPathName` returns the `IC~~` path, and the mock drain matched a FLAG while the+real one matched the PATH — so `is_import_temp`, the write gate's exists-check,+`save_as` and path-keyed ownership were never exercised on a realistic temp path.+F1's subject had no e2e test. The mock now reports the synthetic temp path+everywhere `GetPathName` would (`doc_info` and `reported_path`) and its drain+matches through the same `is_import_temp`.++Making the mock honest immediately exposed **three real defects it had been+hiding**, all of which existed on the live machine:++* the com host's write veto refused `close_import_temps` outright (its ownership+ check resolved a target and found it foreign), so the drain could never run in+ the default posture — the whole point of the verb. It is now an explicit,+ justified exemption on the host's allowlist, which is *why* F1's tightening+ matters: the path proof is the only guard left.+* `swx_import` recorded only the TITLE in the ownership registry while the open+ document reported a path, so the two never matched and the bridge refused to+ close **the document it had just imported**. It now records the path — through a+ new `remember_scratch_at`, not `rename_scratch`, because `rename_scratch` finds+ its target by title and would have reassigned the recorded path of a *different*+ owned document sharing the imported title (SOLIDWORKS titles an imported STEP+ `<stem>.SLDPRT`, so importing beside a saved part of that name is ordinary).+* the mock's `host_owns` branched on `saved`, so an unsaved-but-pathed temp was+ looked up as pathless. It now keys on the path SOLIDWORKS would report, which is+ what the real backend has always done.++**F4 (LOW). The `ActiveDoc` recovery branch** can report another concurrent+import's title/path. No behaviour change: it now sets+`titleMayBeAnotherConcurrentImport:true` and says so in the hint.++## Adversary rounds 6 and 7 → the anchor becomes the whole proof (0.5.2, 0.5.3)++**Round 6, D1 (HIGH, data loss).** The proof was still only about SHAPE: two+tokens in a path, so a user's own saved file under folders *they* had named+(`…\Temp\swx-archive\2026\IC~~\customer-final.sldprt`) qualified for a `CloseDoc`+with no save prompt. 0.5.2 replaced it with a segment walk requiring the+launcher's contiguous layout — `<TEMP>\swx<digits>\IC~~\<file>`, `IC~~` as a+folder and not a file, `swx` followed by DIGITS (not `swxnotes`) — and anchored it+to the resolved `%TEMP%`.++**Round 7, D-1 (HIGH, data loss).** The anchor had a fallback that undid it:++```rust+matches_resolved || canonical // canonical = the parent ends appdata\local\temp+```++The `canonical` arm made the anchor advisory. Any path whose tail read+`…\AppData\Local\Temp\swx<digits>\IC~~\<file>` qualified **wherever it lived** — a+restored profile backup on another volume (`E:\Backups\drew\AppData\Local\Temp\…`),+another user's profile, a copy somebody took of a leaked temp they wanted to keep.+And because `matches_resolved` was `unwrap_or(false)`, when `%TEMP%` could not be+read the shape test was the ONLY test: the one place in this bridge where an+unreadable precondition guessed instead of refusing, authorising a close with no+save prompt.++0.5.3:++* **the resolved `%TEMP%` is the whole location test.** The `canonical` arm is+ deleted. The pid folder's parent must BE the resolved temp directory, segment+ for segment.+* **both sides are resolved, not merely compared.** `GetPathName` can hand back+ `C:\Users\DREW~1\AppData\Local\Temp\…` for the very directory `%TEMP%` reports+ as `C:\Users\drew\AppData\Local\Temp`, so segments match when they are equal+ **or** one is the other's 8.3 short form — by the real rule (six characters of+ the name with spaces and periods removed, plus `~n`), so `a~1` cannot stand in+ for `adversary`. `GetLongPathName` is deliberately not used: it answers only for+ a path that still exists on disk, and this rule is asked about documents whose+ temp file may already be deleted while the document is still open.+* **no root, no drain.** `temp_root() == None` matches nothing, and+ `swx_close_import_temps` refuses with `temp_root_unknown` rather than reporting+ `closed:0, done:true` — which, on a session holding hundreds of leaked temps, is+ a lie a caller acts on.+* **the rule is now testable off Windows.** `SWX_TEMP_ROOT` names the resolved+ root, and in mock mode `temp_root()` answers with the simulated launcher's+ `C:\Users\drew\AppData\Local\Temp` instead of this host's `/tmp` — because the+ suite runs on Linux, and a location rule that cannot be exercised here is the+ one that has now twice been the data-loss rule. The fault file+ `mock-temp-root-unknown` models an unreadable root so the refusal has a test.++## Adversary round 8 → the anchor was right, its FAILURE was silent (0.5.4)++**F (MEDIUM, wrong report — not wrong action).** `temp_root()` resolved the+BRIDGE process's `%TEMP%`, and the drain then required the document's path to sit+exactly under it. That is the correct rule, and it closed the data-loss hole. But+the two directories can legitimately differ: the com host inherits Adom Bridge's+environment, and ab may run under a service profile, a redirected `TEMP` or a+different account from the interactive session SOLIDWORKS lives in — and the 1342+temps this verb exists for were written by a **different, dead** bridge process,+whose `%TEMP%` 0.5.3 simply assumed was ours.++On a mismatch the drain matched nothing and answered:++```json+{"success": true, "closed": 0, "done": true,+ "_hint": "Every open STEP/IGES import temporary was closed."}+```++with three of them open. Nothing was wrongly closed — the miss is the safe+direction for the CLOSE — but the REPORT was a confident wrong claim about the+user's session, and an operator running launch → drain → quit would have read+step 2 as finished and been surprised by the quit gate refusing on open+documents.++0.5.4 makes the failure loud instead of silent:++* **`tempRoot` and `tempRoots` are in the response**, so a mismatch is+ diagnosable from the output on a machine where nobody can attach a debugger.+* **near misses are counted.** `has_import_temp_tail` recognises the launcher's+ own `…\swx<digits>\IC~~\<file>` tail *wherever* it lives; documents that carry+ it and fail the root test are reported as `importTempsElsewhere: N` with an+ `importTempsElsewhereSample` path.+* **a matched-nothing drain does not claim completion.** With `N > 0` and+ `closed == 0` the verb reports `done: false` and a hint that says what+ happened, why nothing was closed (the resolved temp directory is the whole+ proof, and the close does not prompt), and what to do: read the sample path and+ restart the bridge with `SWX_TEMP_ROOT` set to the directory above the+ `swx<pid>` folder, or ask the user to close them in SOLIDWORKS.+* **two candidate roots, with a documented precedence.** `SWX_TEMP_ROOT`, when+ set, is the whole set. Otherwise the set is this process's `%TEMP%` (`TEMP`,+ `TMP`, `TMPDIR`) **and** the interactive user's `HKCU\Environment\TEMP`+ expanded, each included only if readable; a document under **either**+ qualifies, and an empty set still refuses with `temp_root_unknown`. Reading that+ registry value is the least invasive way to learn the temp directory of the+ session SOLIDWORKS is in: its own `GetTempPath` cannot be called from another+ process, and reading its PEB for its environment block is invasive,+ version-fragile and needs rights this bridge should not want.++**One note answered differently from how it was raised.** Pass 8 also observed+that accepting `drew~1` as the 8.3 form of `drew` is a widening, since Windows+does not mangle a name that already fits 8.3, and suggested restricting the tilde+form to segments longer than 8 characters or containing a space or period. That is+right about *mangling* and wrong about the other case Windows generates a tilde+name for: a **collision**. When two entries in a directory claim the same short+name, the short name of the first becomes the whole name plus `~n` — and the+standard example is a profile directory on a domain-joined machine, where+`C:\Users\drew` sits beside `C:\Users\drew.ADOM` and the short name of `drew`+really is `DREW~1`. Restricting to the mangled form would make the drain MISS real+leaked temps on exactly the machine class this bridge ships to, which is the wrong+direction for a verb that exists because 1342 temps went unclosed. So the rule is+now **exact rather than merely tighter**: the mangled form requires the+six-character head (`ADVERS~1` for `adversary`, so `a~1` is rejected), the+collision form requires the whole name (`DREW~1` for `drew`), and neither admits a+loose prefix — which was the actual looseness worth removing. Pass 7's positive+control stays green because the case it pins is real.
docs/QUIT-INVESTIGATION.md+196@@ -1275,6 +1275,202 @@ Neither is about quitting, both belong to the same posture: `verbs.rs` only clears on the success arm). **Clear the host registry after a census-confirmed exit, never before the verification.** +### 5.11 As built — `swx_stop` v2, shipped in 0.5.2, hardened in 0.5.3 (mock-proven, **live-unproven**)++Written after the code, from the code. Where the build deviates from §5.1–§5.10+the deviation is named here rather than quietly absorbed, and the doc above is+left as it was written — it is the design record, this is the delivery record.++**Where the pieces actually live**++| Piece | File | Why there |+|---|---|---|+| The ladder's rules, as pure functions over measured facts | `src/quit.rs` | testable without a machine: the rung table, budgets, `SESSION_CHILDREN`/`EXPECTED_SURVIVORS`/`UNJUDGED`, `Identity`, `process_gone`, `reap_report`, dialog classification, `check_preconditions`, `RungResult` |+| The Windows gestures | `src/quit.rs::win` | `send_wm_close` (`SendMessageTimeout`), `post_sc_close` (`PostMessage WM_SYSCOMMAND SC_CLOSE`), `restart_manager_shutdown` |+| The rung executor, inside the COM apartment | `src/comhost/com.rs::cmd_quit_rung` | rungs (a) and (b) are COM calls and must run on the STA thread that owns the object |+| Reference release + accounting | `src/comhost` `release_com` / `com_refs` | see below: the ladder is meaningless while this process holds a reference |+| Preconditions, the loop, and the measurement | `src/verbs.rs::swx_stop` | ~300 lines, and the only place the two halves meet |+| The simulated ladder | `src/comhost/mock.rs` (`quit-script.json`) | one branch per rung outcome, so every path in the loop is exercised on Linux |+| The census / window / liveness seam | `src/launchos.rs` | **DEVIATION from §5.6**, which put the window census in the com host |++**DEVIATION 1 — the window census is in `launchos`, not the com host.** §5.6+placed `windows_of_pid` / `IsHungAppWindow` / `GetMenuState` beside the COM code.+It is in the OS seam instead, for one reason: the whole point of these readings+is that they work when COM does *not*. The com host is exactly the process that+is blocked when SOLIDWORKS is wedged — asking it "is there a dialog?" through the+same stdio channel that a hung `ExitApp` is occupying would make the dialog check+unanswerable precisely when it matters. In `launchos` it is also `FakeOs`-backed,+so every dialog branch is testable.++**DEVIATION 2 — rung (e) ships as `skipped`, always.** `EXIT_MACRO_SHA256` is+`None` and `exit_macro_path()` resolves nothing, so the rung reports+`result:"skipped"` with the reason. A `.swp` is a binary OLE container: it cannot+be authored, reviewed or hashed on this Linux host, and a rung that runs VBA+inside the user's session on the strength of a file nobody has read is not a rung+this bridge is willing to take. When a reviewed two-line macro exists, setting+that constant is the whole change.++**DEVIATION 3 — the precondition ORDER was inverted relative to §5.2.** The+checks that do **not** require identifying the process run first:++> P1 open documents (and a truncated enumeration) → P2 unsaved → **P7 identity** →+> P3 dialog → P4 busy → P5 responding → launch-in-flight++because a fact that is *definitely true* is more useful to a caller than an+unknown. With Drew's 5 documents open and a census that cannot answer, "5+documents are open, close them first" is actionable; "I cannot identify the+process" is not, and the first version reported the second.++**DEVIATION 4 — error-code names.** §5.8 proposed `save_prompt_open`; shipped as+`stop_blocked_by_save_prompt`, with `stop_blocked_by_dialog` for every other+blocking modal, so a caller can tell "the user's work is in the question" from+"SOLIDWORKS is asking something else". `open_documents_present` shipped as+specified. The full set: `host_app_not_running`, `open_documents_present`,+`unsaved_documents`, `stop_blocked_by_save_prompt`, `stop_blocked_by_dialog`,+`host_app_busy`, `host_app_hung`, `launch_in_flight`,+`stop_preconditions_unknown`, `unknown_rung`, `user_writes_disabled`, and+`host_app_stop_declined` when every rung was tried and measured and it is still+there.++**DEVIATION 5 — `stillRunning` is still in the response.** v1 shipped it and+callers read it; v2 keeps it, but MEASURED from the final census and **null**+(never `false`) when the census could not be read. An unreadable census is not+proof of an exit — which is the one place in this verb where the+empty-to-confident collapse would have been most expensive.++**The three laws, as enforced**++1. **Never a kill.** There is no terminate call anywhere in the stop path. Rung+ (f) passes literal `0` where `RmForceShutdown` would go, and `tests/quit.rs`+ pins the absence: every rung's action string is asserted free of+ kill/terminate/force wording (`force` is allowed only in the phrase+ "RmForceShutdown OMITTED", which is the assertion that it is not passed).+2. **Never answer a dialog.** A new top-level `#32770` stops the ladder+ permanently and becomes `blockingDialog` in the response; the hint sends the+ caller to `swx_state`'s background screenshot and to the user. Nothing is+ clicked. `src/abwin.rs::classify` was widened so "Save Modified Documents" —+ the real title, which the classifier had been returning `unknown` for —+ classifies as `save_changes`.+3. **Nothing is disowned until the process is verifiably gone.** The scratch+ registry, `owned-docs.json` and the com host's own `created` set are cleared+ only on the success arm, after `process_gone` is `Some(true)`. This closes+ §5.10's second finding: a declined quit used to leave the bridge unable to+ save or close its own documents.++**Release before, release after.** Every rung is preceded by `release_com` and+followed by `release_com` + `com_refs`; if any reference is still outstanding at+the measurement, the rung's `detail` says so verbatim+(*"WARNING: N COM reference(s) were still outstanding at the measurement"*). A+rung that "declined" while this process still held a reference is not evidence+about SOLIDWORKS, and the report must not let that be mistaken for one.++**How success is measured.** Never by `GetActiveObject`, never by what the rung+returned — and a rung that works tears the COM channel down underneath the call+that asked for it, so a transport error is explicitly *not* a verdict. After each+rung: release, count references, settle (`SWX_QUIT_SETTLE_MS`, default 1500 ms),+re-take the process census, and ask whether **this pid with this creation time**+is absent. `None` (census unreadable) is `Unmeasured`, which is a failure, not a+quit.++**What the response carries:** `stopped`, `stillRunning`, `winningRung`,+`rungsTried[]` (rung, action, result, `processGone`, `elapsedSec`, `newWindows`,+`dialog`, `sessionChildrenStillAlive`, `expectedSurvivorsAlive`,+`undocumentedStillAlive`, `detail`, `hostError`), `preconditions`,+`processCensusBefore` / `processCensusAfter`,+`expectedSurvivorsAreNotAFailure`, `blockingDialog`, `maxRung`,+`elapsedSeconds`, `windowEtiquette:"background"`. Every refusal writes a+`solidworks_quit_refused` ledger line, every rung a `solidworks_quit_rung`, and a+verified exit a `solidworks_stopped` — v1 wrote nothing at its front gate, so+"we tried and were told no" was invisible afterwards.++**Pinned at rung (a).** `DEFAULT_MAX_RUNG = "a"`. Rungs (b)–(f) are reachable+only by an explicit `maxRung`, and (f) additionally by `allowRestartManager:true`+— its OS semantics are *save without prompting*, the one rung whose failure mode+is a silent write rather than a visible dialog.++**Adversary pass 7 changed four things about the ladder** (branch+`adversary/0.5.2`, commit 8ae06d2). The pass found no way through the measurement+discipline itself — `process_gone`, the `null` `stillRunning`, the disown+ordering, the reference accounting, the dialog and UIPI stops, and the rung-(f)+identity requirement all held — and two things that did not:++* **The preconditions were swept ONCE, before the loop** (Q-1), while+ `check_preconditions`' own comment promised "immediately before each rung". On+ a multi-rung ladder the verified-zero-documents invariant therefore held for+ rung (a) alone: a document opened while rung (a) settled was still open when+ rung (b) fired `RunCommand(swCommands_App_Exit)` at the session — which is+ exactly how the "a save prompt at a verified zero is impossible" reasoning+ becomes false. The sweep is now a re-usable `StopEvidence::sweep` and runs at+ the top of every iteration; a mid-ladder failure refuses with the same codes+ the front gate uses, carries `refusedMidLadder:true` and the rungs already+ tried, and its ledger line records `afterRungs`. The re-sweep also compares the+ identity, so a ladder can never continue against a DIFFERENT process+ (`stop_identity_changed`).+* **The exit was judged from ONE census read 1.5 s after the rung** (Q-2). A real+ `ExitApp` returns while SOLIDWORKS is still tearing a session down — ten CEF+ subprocesses, three out-of-process servers, a journal to close — so a slow but+ perfectly good exit read as `declined`, and the ladder's answer to a decline is+ to ESCALATE: a window message into an application that was already exiting. The+ measurement now polls at the settle interval up to the rung's own budget,+ stopping early only on the two answers that end a ladder (gone, or a dialog),+ and the rung's `detail` says how many reads it took.+ `SWX_QUIT_EXIT_WAIT_SECS` shortens the wait for the suite and the gate. The+ simulator gained `exitAfterMs`, so a slow exit is a TESTED path rather than a+ comment — without it the mock hid the divergence completely.++Three more, all about seeing less than we claimed to:++* **The dialog census could not see a swCef modal** (Q-3). The 3DEXPERIENCE+ sign-in, licence and cloud-save windows are web views, and a web view's hwnd+ belongs to `swCefSubProc.exe` — a child of `sldworks.exe` with its own pid. "No+ dialog is on screen" was provable while the platform held the one modal most+ likely to be up. `quit::dialog_census_pids` now returns the SOLIDWORKS pid plus+ every `swCefSubProc.exe` whose **ppid is ours** (never by class name, so+ another session's web view is never adopted), the sweep censuses all of them,+ ONE unreadable pid makes the whole answer UNKNOWN, and the response reports+ `dialogCensusPids`.+* **P1 accepted `Some(0)` without the cross-check the Python gate already did**+ (Q-4). `GetDocumentCount` and `GetDocuments` are two different COM reads;+ `check_preconditions` now refuses as UNKNOWN when they disagree, because when+ they disagree neither is evidence and "zero" is the answer we are least+ entitled to assume.+* **Rungs (c)/(d) sent to an hwnd captured at precondition time** (Q-5). Handles+ are recycled, and the rung immediately before them exists to make that window+ go away — so `WM_CLOSE` could land on somebody else's window. The com host now+ re-verifies `GetWindowThreadProcessId(hwnd) == pid` **and** that pid's creation+ time immediately before sending, and skips the rung if either fact is not+ exactly right (`quit::hwnd_still_owned`, unit-tested for the recycled-pid,+ wrong-owner and unreadable cases).++And two reporting fixes: every rung now carries `censusReadable` beside its three+reap lists (three empty lists otherwise read as "everything was reaped and+nothing survived", which is the most reassuring possible rendering of no+information at all), and `tools/quit-gate.py` now **proves** the session was+bridge-launched instead of asserting it in its docstring — `launch.attributed` and+`launch.runBeginSeen` must both be true and the live `sldworks.exe` must have been+created at or after that attempt's `invokedAt`, with any unreadable one a refusal.++**Adversary pass 8 (branch `adversary/0.5.3`, commit eccccdc) cleared the quit+ladder and the launch machine for the live gate** at `maxRung "a"`, and found one+thing — in the DRAIN, not the ladder: a `%TEMP%` mismatch made+`swx_close_import_temps` report a finished drain when it had matched nothing (see+`docs/IMPORT-LEAK.md`). It matters here because the drain is step 2 of the deploy+sequence and the quit gate is step 3: a silent no-op at step 2 would have surfaced+as this gate refusing on `open_documents_present`, with the operator believing the+session was already empty. The gate's step 1 now also NAMES the open documents that+carry the launcher's temp tail — read-only, by applying the pattern to the paths+`swx_docs` already returned, because the only action this gate may take is the+`swx_stop` call.++**What is proven, and what is not.** 16 tests in `tests/quit.rs` plus the e2e+stop paths cover: every rung outcome branch, every refusal code, the unknown+refusals, the disown ordering, dialog classification, the survivor/child+partition, the slow-exit path, the per-rung re-sweep, the CEF-inclusive dialog+census, and a pin that nothing in the stop path ever kills anything. **Not one+rung has ever run against a real SOLIDWORKS.** The ecosystem table lists graceful+quit as *unproven live* for that reason, and `tools/quit-gate.py --baseline` is+the read-only first half of settling it.+ --- ## 6. This environment: why the BRIDGE is the right place to do this