← 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)

Drew Owens ·1e43f88e67 ·1mo ago ·parent cfee507
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