Browse Source

fix(movie): release probe resources after evidence failures

Drew Ritter 3 tuần trước cách đây
mục cha
commit
a2e7743020

+ 0 - 134
.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-report.md

@@ -1,134 +0,0 @@
-Task 1 native Windows feasibility report — DONE_WITH_CONCERNS
-
-The bounded mechanism gate passed on Ballmer with actual ordinary-user execution for PowerShell 5.1, PowerShell 7 and Git Bash. The final aggregate has 40 passed checks, zero failed/unavailable checks. This result includes the controller-approved explicit same-page snapshot checkpoint for TUI capture. It does not establish general resize/redraw decoding or general screencast freshness. Those remain required production fixes/tests, particularly capture freshness in Tasks 8–9. Independent controller review remains required before dependent implementation proceeds.
-
-Work stayed in `/Users/drewritter/.paseo/worktrees/2mmrq9t5/movie-os-compatibility`, branch `feat/movie-os-compatibility`, implementation base `0b22f618`. Changed files are `tests/proving-it-works-with-a-movie/probe-windows.py` and this report. No skill prose or bulk port changes; no subagents/reviewers dispatched; no push, PR or merge. Read the Task 1 brief, global constraints, terminal excerpt, probe context, applicable AGENTS.md, the imported three regression scripts, and TDD/verification/debugging/worktree/Paseo skills. Did not read the full plan.
-
-The inspected PR #2214 was rechecked as OPEN against `dev`, exact head `f6617db1517488d4a8b66925519d5ffbeef965b3`; imported movie skills/tests matched that revision. An immutable import archive is retained as `task-1-evidence/inspected-import.tar`, SHA256 `a4abb54f8ecf34acd44d7a4eb912492c59d749fef73b473cac95e2da25d70225`. The controller's original 18-assertion baseline was independently repeated here: 4 assemble, 5 narration and 9 checker assertions passed, none skipped.
-
-Launch host: `workerbee`, macOS. Recording host: `ballmer`, Windows 11 Pro 10.0.26200, native x64. This host observation imposes no Windows 11-only production requirement. WSL and the other OS acceptance jobs belong to later/controller work; they were not used to substitute for this native gate.
-
-Remote root is exactly `C:\Users\drew\movie-os-task1-20260909`. Portable prerequisites are under `tools`, archives under `downloads`, uv environments under `cache`, and all sessions under `evidence`. Local evidence root is `.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/`; its `remote/` directory contains retrieved remote artifacts. These large artifacts remain ignored in Git and available locally/remotely; source and report are committed. Every local SSH call has a named `.ps1`, `-command.json` containing the actual encoded argv, `.stdout.txt` and `.stderr.txt` under the local evidence root.
-
-Inventory inspected PATH and relevant installed user locations before provisioning. WindowsApps Python/Bash aliases were unused. No PATH, profiles, execution policy, firewall, credentials, services or account membership changed. No management network was disabled. Existing Chrome and Git Bash were reused.
-
-| Prerequisite | Actual executable, relative to remote root unless absolute | Actual version/result |
-| --- | --- | --- |
-| uv | `tools\uv\uv.exe` | `uv 0.12.12 (c4be69153 2026-09-09 x86_64-pc-windows-msvc)` |
-| Native Python | `tools\python\cpython-3.12.14-windows-x86_64-none\python.exe` | CPython 3.12.14, `win32`, AMD64, 64-bit pointers |
-| PowerShell 5.1 | `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` | `5.1.26100.9168`, 64-bit |
-| PowerShell 7 | `tools\pwsh\pwsh.exe` | `7.6.6`, 64-bit |
-| ttyd | `tools\ttyd.exe` | `ttyd version 1.7.7-40e79c7`, PE x64 despite upstream asset name `ttyd.win32.exe` |
-| FFmpeg/ffprobe | `tools\ffmpeg\ffmpeg-9.0.1-essentials_build\bin\{ffmpeg,ffprobe}.exe` | `9.0.1-essentials_build-www.gyan.dev` |
-| Git Bash | `C:\Program Files\Git\bin\bash.exe` | Bash `5.3.9(1)-release (x86_64-pc-cygwin)`, Git 2.54.0.windows.1 |
-| Browser | `C:\Program Files\Google\Chrome\Application\chrome.exe` | Chrome 152.0.7977.65 |
-| CDP client | PEP 723 uv script environment | websocket-client 1.9.0 |
-
-`remote/prerequisites.json` retains actual command arrays, stdout/stderr, exit codes, binary SHA256 and PE machine `0x8664`. Commands exercised `uv --version`, native Python identity, `ffmpeg -version/-filters/-encoders/-devices`, `ffprobe -version`, `ttyd --version/--help`, each PowerShell version separately, Git Bash version and native tools from Git Bash. Native FFmpeg reported libx264, AAC, subtitles and `--enable-libass`; all prerequisite checks passed. Git Bash invoked native Windows Python and FFmpeg explicitly, not WSL or MSYS Python.
-
-Downloads and actual SHA256 values below are retained in `remote/downloads.jsonl` and the remote `downloads` directory. Expected digests came from the corresponding release metadata/checksum/PyPI records. Python/wheel downloads were repeated after an interrupted tool call with identical digests; both records remain in the log. FFmpeg's official download page links the Gyan Windows distribution used here ([FFmpeg downloads](https://ffmpeg.org/download.html)).
-
-| Download origin | SHA256 |
-| --- | --- |
-| [uv 0.12.12 archive](https://github.com/astral-sh/uv/releases/download/0.12.12/uv-x86_64-pc-windows-msvc.zip) | `3d54912924c36e862c14f427d04f2ed70a99e8001d1c30caa101f6d5711626d5` |
-| [PowerShell 7.6.6 archive](https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-x64.zip) | `02fe458be20493fbdf43f61ea20610b811ee6c738ab1676c61b9cfcd1a33c860` |
-| [ttyd 1.7.7 executable](https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.win32.exe) | `e33a27501b10b96981335bcba938b1145c7f52551a343e72160f00ab71832b37` |
-| [Gyan release essentials](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip) | `fec81ae03971d9dd4be3ebe02e263bd2ec1d789483f931bdba5f5715e65da2e9` |
-| [Astral CPython 3.12.14, 20260901](https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.12.14%2B20260901-x86_64-pc-windows-msvc-install_only_stripped.tar.gz) | `7c45c9622400d578709a9b2cddbe8124cc21d382409d9f13406d706d28e31b14` |
-| [websocket-client 1.9.0 wheel](https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl) | `af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef` |
-
-SSH transport executed as High-integrity `ballmer\drew`. Those earlier results remain elevated evidence, not ordinary-user acceptance. Direct token inventory returned TokenElevationType=1 and TokenElevation=1; TokenLinkedToken failed with Win32 error 1312. No account was created or reconfigured. The controller established existing desktop Paseo execution in task terminal `1496200b-5545-4d85-af31-d01df72a3c11`, named `movie-standard-user-probe`, cwd the remote root. Initial controller evidence is `standard-runner-token.json` and local `artifacts/ballmer-runner-inventory/` beside this report. Each acceptance supervisor then independently queried its own token through Win32: elevation type 3, elevated false, integrity SID `S-1-16-8192`, desktop session 1, `ballmer\drew`, Administrators deny-only. These results are in each `probe.json` or crash session `control/ready.json`, including complete `whoami /all` output. Token queries followed [Microsoft's GetTokenInformation contract](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation).
-
-The matching installed remote Paseo CLI submitted the launch command into that Medium terminal while SSH itself remained High:
-
-```powershell
-& 'C:\Users\drew\AppData\Local\Programs\Paseo\resources\bin\paseo.cmd' terminal send-keys '1496200b-5545-4d85-af31-d01df72a3c11' '<powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand plus actual UTF16LE base64>' Enter --json
-```
-
-The literal base64/argv is retained in each `*-submit-command.json`, not reconstructed in this report. Task-owned `*-launch.ps1` artifacts execute native Python `subprocess.Popen` argument arrays for the three uv probes and wait for them in the persistent terminal. No additional agent or service was launched. The newer local Paseo CLI's workspace SDK mismatch with the older host was bypassed using the already installed matching CLI, without changing the installation.
-
-The common probe command was the following, with the actual selected absolute shell and a new session directory in each launch artifact:
-
-```powershell
-$env:UV_PYTHON_INSTALL_DIR='C:\Users\drew\movie-os-task1-20260909\tools\python'
-$env:UV_CACHE_DIR='C:\Users\drew\movie-os-task1-20260909\cache'
-$env:UV_NO_CONFIG='1'
-$env:PYTHONIOENCODING='utf-8'
-& 'C:\Users\drew\movie-os-task1-20260909\tools\uv\uv.exe' run --python 'C:\Users\drew\movie-os-task1-20260909\tools\python\cpython-3.12.14-windows-x86_64-none\python.exe' --script 'C:\Users\drew\movie-os-task1-20260909\probe-windows.py' --serve --ttyd 'C:\Users\drew\movie-os-task1-20260909\tools\ttyd.exe' --browser 'C:\Program Files\Google\Chrome\Application\chrome.exe' --shell 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --shell-kind powershell51 --directory 'C:\Users\drew\movie-os-task1-20260909\evidence\standard-normal-powershell51' --launch-host workerbee
-```
-
-Separate SSH/harness calls used the same uv prefix with `--phase first`, later `--phase second`, then `--phase prepare-cleanup` and `--phase close`, supplying `--shell-kind` and `--directory`. The first phase returned while native Python's `time.sleep(45)` was still in flight. The later phase began another take, inspected that same pending request, resized, waited for completion and checked the persisted variable. Exact separate-call transcripts are `standard-normal-<shell>-{first,second,prepare,close}-call.*`. The ordered request, ack and result files remain in each session's `control` directory. Request IDs are monotonic, a run ack precedes completion, and another run cannot take input while a prior run is pending.
-
-The probe uses an unnamed, non-inheritable Windows Job with kill-on-close and no breakaway. ttyd and isolated Chrome are created suspended, assigned before resume, and tracked with retained native handles and creation times. Supervisor and sentinel remain outside the child Job. Actual x64 structure sizes were STARTUPINFOW 104, PROCESS_INFORMATION 24, BASIC_LIMIT 64, IO_COUNTERS 48 and EXTENDED_LIMIT 144. The code terminates an unresumed root if assignment/resume fails; no unowned-root fallback exists. Deliberately induced assignment failure is not part of the Task 1 runtime matrix; the future ownership task still needs its broader error-injection coverage.
-
-Actual ttyd arguments include `-i 127.0.0.1 -p <owned-port> -W -m 1 -w <session-directory>` followed by the explicitly selected shell. Chrome uses an isolated session profile, loopback CDP, `--headless=new`, initial `about:blank` and 1600x900 window. The one page enables CDP Network before navigation, identifies its own terminal WebSocket request ID, observes binary opcode 2 with leading output byte `0` (`30` hex), and applies the exact requested `ttyd_output` decoder. Input, readiness screenshot, parsed completions and screencast metadata all refer to that same page/socket/session. The required `assert_probe` contract remains verbatim.
-
-| Actual standard session result | PowerShell 5.1 | PowerShell 7 | Git Bash |
-| --- | --- | --- | --- |
-| Recorded shell PID before/after | 12680 / 12680 | 14484 / 14484 | MSYS 1420 / 1420 |
-| Session nonce before/after | `13ec77781da74219aa6ef3b1ca5f0548` | `d427b90065644f579858711962e89bb4` | `75d88a3a44e641719b45ba7218ebca46` |
-| First driver returned (epoch s) | 1788994016.157899 | 1788994016.108129 | 1788994016.157899 |
-| Later inspection, same pending run | 1788994039.2253277 | 1788994039.1947393 | 1788994039.2253277 |
-| Delayed run completed | 1788994060.7506654 | 1788994060.513397 | 1788994061.1033065 |
-| Persisted variable | `persisted-λ` | `persisted-λ` | `persisted-λ` |
-| Actual columns | 217 → 167 | 217 → 167 | 217 → 167 |
-| Normal cleanup seconds | 1.890 | 1.859 | 1.875 |
-| Cancel cleanup seconds | 1.984 | 1.984 | 2.047 |
-| Browser-loss cleanup seconds | 0.063 | 0.047 | 0.047 |
-| Supervisor-crash cleanup seconds | 0.250 | 0.250 | 0.203 |
-
-Each cleanup checked parent/child/grandchild heartbeats, disappearance through native handles, no advancing heartbeat after shutdown, and unrelated sentinel survival. Crash observers opened/validated native handles before killing the actual supervisor with exit 99; every sentinel also advanced its heartbeat after that kill, then was cleaned through its verified handle. A final audit checked 609 recorded PID/creation-time observations across sessions, found no remaining original processes, and explicitly distinguished reused PIDs. `remote/final-process-audit.json` retains those observations. Git Bash's MSYS shell PID is never used as a Win32 process handle/PID assumption.
-
-All three extra-client attempts were refused with `WebSocketConnectionClosedException: Connection to remote host was lost.` and no HTTP status. Evidence retains `accepted=false`, `candidate_continuity=false`, plus a nonce/PID-bearing round trip on the original page. All three browser-loss tests observed Win32 10054 (`An existing connection was forcibly closed by the remote host`); active commands became interrupted with `shell_success=null`, and no successful continuity was claimed. The browser was terminated only through its owned native handle.
-
-All 27 selected command cases completed with their expected semantics: ten cases per PowerShell version and seven for Git Bash. Native exit 0 followed by a failing cmdlet and native exit 7 followed by a successful cmdlet proved that cmdlet outcomes do not inherit stale native status. PowerShell direct terminating/nonterminating errors were false; parse failure preserved `raw_shell_success=true` separately from `parse_error=true`, semantic `shell_success=false` and the actual ParseException. The expression wrapper `(Write-Error 'probe expression wrapper')` was true in PS5.1 and false in PS7; its error text remains recorded. Explicit external shell script `exit 9` returned native 9. The producer/logging failure preserved native/producer exit 7: PowerShell shell status false; Bash logger shell status true with pipeline statuses `[7,0]` and producer success false. Commands and attribution are in `standard-outcomes-*/outcomes.json`; raw observed streams are alongside them. These are atomic-beat observations, not a claim to find every internally ignored error in arbitrary scripts.
-
-Actual RED/GREEN history and retained defects:
-
-- Initial `python3 tests/proving-it-works-with-a-movie/probe-windows.py` before recorder implementation exited 1 with `KeyError: 'shells'` (`red-unimplemented.txt`). Initial `assert_outcomes({})` exited 1 with `KeyError: 'native_success'` (`red-outcomes.txt`). No invented passing runtime fixtures were substituted.
-- Original `startup-*` runs for all three shells obtained the page WebSocket then closed without output; screenshots showed Reconnecting. Keeping binary/Job/CDP constant and adding explicit ttyd `-w` passed nonce/PID/cwd readiness in `cwd-*`. Upstream [ttyd PR #1502](https://github.com/tsl0922/ttyd/pull/1502), independently read by the controller, initializes command-line/cwd pointers to NULL alongside MSVC work. That supports the cwd correction without proving the internal cause of our runtime failure. Our ttyd log did not emit upstream error 123; no such claim is made.
-- Separate probe implementation defect: the initial PowerShell prompt error baseline was unset and emitted `InvokeMethodOnNull` at prompt line 8. `prompt-diagnostic-powershell51` and original `outcomes-*` retain this. Initializing `$Error.Count` and guarding null records corrected it. Corrected PS baseline evidence remains separate from ttyd's explicit-cwd prerequisite.
-- Original long completion lines wrapped at 218 columns and ConPTY duplicated a boundary character while emitting cursor repositioning sequences (`outcomes-gitbash`). Its raw `network.jsonl`/`terminal.bin` and failed result remain. Completion payloads now use base64 chunks of at most 60 characters on separate lines. Earlier elevated dimensions were 218→167; actual standard dimensions were 217→167. A preliminary update/aggregate wrongly generalized 218 to standard runs; retrieved size messages corrected that (`aggregate-first-full.txt`). No arbitrary resize/redraw decoder is claimed, and no VT-emulation dependency was added.
-- ParseException initially left raw $? true and broke expected error attribution in `outcomes-v2-*`. The final prompt preserves raw status plus request-attributed parse failure; `outcomes-v3-*` elevated and `standard-outcomes-*` Medium cases passed.
-- The original extra-client handler expected an HTTP error and failed on EOF in `session-powershell51`. It now retains EOF refusal and requires the original-page round trip. Nothing unavailable became green merely because the attempted connection threw.
-- Initial normal cleanup asserted handle exit immediately after Job membership reached zero; Chrome handles could still be unsignaled. Retained `session-v2-*` failures show stopped worker heartbeats and surviving sentinel. A bounded wait on retained handles corrected this; elevated `final-*` and standard cleanup evidence retain before/after results.
-- An elevated `final-powershell7` run also hit `PermissionError(13, 'Permission denied')` reading `heartbeats\child.json` during atomic replacement, despite successful cleanup. A bounded 0.5s access-error retry now retries the actual file operation; persistent errors still raise, never returning empty/old evidence. Standard runs passed. One original concurrent CDP connection exceeded a 0.1s handshake (`cancel-gitbash`); connection setup now allows 5s, with receive polling still 0.1s.
-- An SSH command exceeded Windows command-line length; the exact script is now copied as a task-owned artifact and invoked by a short encoded ScriptBlock wrapper. PS5 native `-c` quoting was corrected using a base64 Python payload. Initial ZIP export encountered a pre-1980 profile timestamp; exports now exclude browser profiles and preserve only probe evidence. Failed commands remain in their named outputs.
-- Initial aggregate inspection assumed cleanup-ended ownership takes had an `end_request` summary count. Their real frame manifests were present; the aggregate now validates those persisted files directly while retaining explicit counts for requested end-take operations. The failed first aggregate is retained.
-
-The active-TUI capture defect required separate visual review. `standard-normal-powershell51` successfully ran the TUI and its end PNG showed the screen, but its active-take JPEGs stopped before that screen. A one-second application-ready hold in `standard-tui-*` still left blank-ending PowerShell JPEGs. Those are retained failures; neither the command result nor the final PNG was accepted as the active-pixel proof.
-
-The controller approved a bounded correction: after actual `tui-ready.json` and a visual hold, use an explicit `snapshot` control request on the same page and existing CDP connection, before sending `q`; still require an inspected active-take JPEG before `q`. `tui-diagnostic-*` proved live PNGs while request 2 was pending. Final `tui-verified-*` also logged CDP sends, receives, frame acknowledgments and polling timeouts. I inspected `interactive/000009.jpg` in all three: each visibly contains `Native Windows TUI λ` and `[q] Finish this screen`, with no `TUI_EXIT:q` yet. `active-tui-pixel-inspection.json` retains exact JPEG hashes, frame metadata, request IDs and the explicit visual finding. The aggregate verifies those exact bytes and timings; it does not pretend to perform OCR.
-
-| Final TUI evidence (epoch seconds) | PS5.1 | PS7 | Git Bash |
-| --- | --- | --- | --- |
-| Application ready | 1788994874.380406 | 1788994874.7009904 | 1788994874.6822324 |
-| Snapshot request 3; run 2 pending | 1788994875.4311693 | 1788994875.854709 | 1788994875.7615943 |
-| Inspected active JPEG 9 received | 1788994875.7732553 | 1788994875.8755016 | 1788994875.7821376 |
-| `q` request 4 dispatched | 1788994875.9168005 | 1788994876.04243 | 1788994875.9338365 |
-| Received events during ready→snapshot hold | 12 | 9 | 7 |
-| Receive timeout polls during hold | 8 | 8 | 8 |
-| Screencast acks sent and replied during hold | 4 | 3 | 2 |
-
-The receiver demonstrably kept draining and acknowledging during the driver wait. The final traced run also contained earlier TUI frames before its snapshot, so it does not establish the cause of earlier stalls or prove that the snapshot alone caused a redraw. The feasibility claim is specifically the inspected checkpoint method. General capture freshness is a REQUIRED production fix/test in Tasks 8–9; stalled takes must not pass production acceptance. Resize-aware completion decoding is likewise required later. The controller explicitly retained these constraints and the possibility of another redraw/compositor interaction.
-
-Final artifacts and reproducibility:
-
-- `remote/probe.json` is the complete aggregate, generated from actual retained reports/control/frame files. Per-session reports intentionally keep unrelated checks unavailable; only the aggregate can open the complete gate.
-- `remote/standard-outcomes-{powershell51,powershell7,gitbash}/`: recorded outcomes, raw CDP messages, terminal bytes, screenshots and frames.
-- `remote/standard-normal-*`: separate-call two takes, resize, variable/nonce/PID continuity, extra-client refusal, normal close.
-- `remote/standard-cancel-*`, `remote/standard-loss-*`, `remote/standard-crash-*`: separate cancellation, browser-loss and crash/heartbeat/sentinel evidence. Crash result is `crash.json`, because the killed supervisor cannot write a final report.
-- `remote/tui-verified-*`: accepted active TUI JPEGs, `tui-live.png`, `cdp-trace.jsonl`, control requests/acks/results and final normal cleanup. `standard-tui-*` and `tui-diagnostic-*` preserve focused attempts.
-- Earlier `startup-*`, `cwd-*`, `outcomes-*`, `outcomes-v2-*`, `outcomes-v3-*`, `session-*`, `session-v2-*`, `cancel-*` and `final-*` remain explicitly elevated historical evidence.
-- Main retrieved archive: remote `C:\Users\drew\movie-os-task1-20260909\evidence-final.zip`, local `task-1-evidence/evidence-final.zip`, 2,323 files, 160,448,403 bytes, SHA256 `ba7438c8686183ceb1f55dd32e62bed3fc57d082bc642f5487561aca364da024`.
-- Capture supplement: remote `C:\Users\drew\movie-os-task1-20260909\evidence-capture-final.zip`, local same basename under `task-1-evidence`, 650 files, 23,823,967 bytes, SHA256 `4b1186772fec51d3581390ae4dc462811d5b6b7c86018fc36b0062de384a844a`. The supplement overlaps earlier focused artifacts; it is not 650 additional unique sessions/files.
-- Local `parser-replay.json`: replayed actual standard outcome streams in seven-byte chunks (22, 22 and 16 records). Truncated completion framing produced no record; damaged framing produced no successful record. Runtime browser-loss records separately demonstrate interrupted/null outcomes when completion is absent.
-- `aggregate-before-retrieval.txt`, `aggregate-first-full.txt`, `aggregate-before-tui-retrieval.txt` retain incomplete/failed aggregate iterations. Missing artifacts fail closed. `aggregate-final.txt` retains the complete passing output.
-
-The main standard runtime source snapshot is `probe-standard-runtime-a107564b5e34.py`, full SHA256 `a107564b5e341c84d5ae21bc9ecbb7c41cd1544752597c78f0cde5dae37489c2`. Focused traced capture used `probe-final-runtime-be36b8182b5c.py`, full SHA256 `be36b8182b5caa1dc071dad3bd075cc44d40a3b6fe7b3282b6da739b44259d40`. Subsequent final-source changes only add validation of the manually inspected pixel artifact's hash/timing in aggregation, its hashlib import, and explicit scope/production requirements in aggregate metadata; they do not alter the exercised Windows mechanism.
-
-Final verification commands are `python3 -m py_compile tests/proving-it-works-with-a-movie/probe-windows.py`, `python3 tests/proving-it-works-with-a-movie/probe-windows.py --aggregate .superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote`, and `--assert-result` against that generated `remote/probe.json`. Aggregate output: `gate=passed`, 40 passed checks; assert-result exits 0. The three imported `test-*.sh` scripts returned `4 passed, 0 failed`, `5 passed, 0 failed`, and `9 passed, 0 failed`; stdout/stderr and actual argv are retained in `imported-final-checks.json` and `test-*.sh.final.*`. No missing-tool SKIP was counted as passing.
-
-Self-review confirms the exact user assertion/decoder seams, native executable arrays, single-page observation, raw versus semantic PowerShell status, native ownership rather than MSYS PID assumptions, bounded shutdown and unavailable/error handling. The file is deliberately a self-contained feasibility test harness, not production modules. Its aggregation expects the named evidence layout and specific observed widths. It does not implement arbitrary VT redraw, robust production capture freshness, the complete OS support matrix, movie assembly/narration portability or comprehensive Win32 failure injection. Those are remaining production work, not omitted green Task 1 runtime checks. The existing task-owned Paseo terminal is left idle and available for later tasks as requested; recorder processes and sentinels are gone. Tools/downloads/evidence remain available under the authorized remote root.
-
-Final native validation also ran the committed-source aggregate and assert-result commands on Ballmer, both exiting 0 (`final-ballmer-aggregate-assert.*`). Local negative checks rejected deletion of a required crash check and corruption of the actual persisted-variable result. These are negative mutations of retained evidence, not fabricated successful runtime checks. The original Task 1 requirements and imported movie code remained unchanged.

+ 161 - 0
docs/superpowers/reports/2026-09-09-proof-movie-os-compatibility.md

@@ -0,0 +1,161 @@
+# Native Windows feasibility gate — Task 1
+
+The bounded mechanism gate passed on Ballmer under an actual Medium-integrity ordinary-user token for PowerShell 5.1, PowerShell 7 and Git Bash: **40/40 checks**, including 27 command-outcome cases. That evidence belongs to commit `49bc2932857071896fb764688594709a1b494eaa`; the cleanup error-path correction below has separate verification. This is a feasibility probe, not the bulk OS port. The TUI result uses the controller-approved same-page snapshot checkpoint; general capture freshness and resize/redraw decoding remain required production work.
+
+Public implementation: [probe-windows.py](../../../tests/proving-it-works-with-a-movie/probe-windows.py). Focused ownership tests: [test-probe-cleanup.py](../../../tests/proving-it-works-with-a-movie/test-probe-cleanup.py). These files and this index remain in Git after the temporary SDD workspace is removed. Generated frames, screenshots and archives are outside core and currently retained at the locations below; durable evidence consolidation is pending.
+
+## Actual environment and prerequisites
+
+Launch host was `workerbee` (macOS); recording host was `ballmer`, Windows 11 Pro 10.0.26200, native x64. This observation does not impose a Windows 11-only production requirement. Remote task root is exactly `C:\Users\drew\movie-os-task1-20260909`. Existing Chrome and Git Bash were reused after inventory. Portable prerequisites, cache and test directories are task-owned; machine PATH, profiles, firewall, credentials, services, account membership and management networking were not changed.
+
+| Tool | Observed version | Executable relative to task root unless absolute |
+| --- | --- | --- |
+| Python | CPython 3.12.14, win32 AMD64, 64-bit pointers | `tools\python\cpython-3.12.14-windows-x86_64-none\python.exe` |
+| uv | 0.12.12, c4be69153 | `tools\uv\uv.exe` |
+| PowerShell 5.1 | 5.1.26100.9168 | `C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe` |
+| PowerShell 7 | 7.6.6 | `tools\pwsh\pwsh.exe` |
+| Git Bash | 5.3.9(1)-release, Git 2.54.0.windows.1 | `C:\Program Files\Git\bin\bash.exe` |
+| ttyd | 1.7.7-40e79c7, PE x64 | `tools\ttyd.exe` |
+| Chrome | 152.0.7977.65 | `C:\Program Files\Google\Chrome\Application\chrome.exe` |
+| FFmpeg/ffprobe | 9.0.1-essentials_build-www.gyan.dev | `tools\ffmpeg\ffmpeg-9.0.1-essentials_build\bin` |
+| CDP client | websocket-client 1.9.0 | uv script environment |
+
+The [prerequisite inventory](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/prerequisites.json) retains argv, stdout/stderr, exit codes and executable hashes. Native FFmpeg exercised libx264, AAC and subtitles/libass support. Git Bash invoked native Windows Python and FFmpeg explicitly.
+
+| Retained download origin | Actual SHA256 |
+| --- | --- |
+| [uv 0.12.12](https://github.com/astral-sh/uv/releases/download/0.12.12/uv-x86_64-pc-windows-msvc.zip) | `3d54912924c36e862c14f427d04f2ed70a99e8001d1c30caa101f6d5711626d5` |
+| [PowerShell 7.6.6](https://github.com/PowerShell/PowerShell/releases/download/v7.6.6/PowerShell-7.6.6-win-x64.zip) | `02fe458be20493fbdf43f61ea20610b811ee6c738ab1676c61b9cfcd1a33c860` |
+| [ttyd 1.7.7](https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.win32.exe) | `e33a27501b10b96981335bcba938b1145c7f52551a343e72160f00ab71832b37` |
+| [Gyan FFmpeg release essentials](https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip) | `fec81ae03971d9dd4be3ebe02e263bd2ec1d789483f931bdba5f5715e65da2e9` |
+| [Astral CPython 3.12.14, 20260901](https://github.com/astral-sh/python-build-standalone/releases/download/20260901/cpython-3.12.14%2B20260901-x86_64-pc-windows-msvc-install_only_stripped.tar.gz) | `7c45c9622400d578709a9b2cddbe8124cc21d382409d9f13406d706d28e31b14` |
+| [websocket-client 1.9.0 wheel](https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl) | `af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef` |
+
+The download records and checksums are in [downloads.jsonl](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/downloads.jsonl); the portable files remain under the remote `downloads` and `tools` directories. The FFmpeg project [links the Gyan Windows distribution](https://ffmpeg.org/download.html).
+
+SSH itself ran as High-integrity `ballmer\drew`; those early results remain elevated. Its linked-token query failed with Win32 1312. Acceptance instead ran in existing desktop Paseo terminal `1496200b-5545-4d85-af31-d01df72a3c11`, `movie-standard-user-probe`, desktop session 1. Every acceptance supervisor independently recorded `ballmer\drew`, elevation type 3, elevated false, integrity SID `S-1-16-8192`, Administrators deny-only. Full token evidence is in each session report. No High-integrity result substitutes for ordinary-user acceptance.
+
+## Bounded observations
+
+The same Chrome page enables CDP Network observation before navigation, identifies its own ttyd socket, and supplies command input, output observation and recording. Native children are created suspended, assigned to an unnamed non-inheritable kill-on-close Windows Job, then resumed. There is no unowned-root fallback. The supervisor and unrelated sentinel remain outside that Job; Win32 ownership uses retained handles and creation times, never Git Bash's MSYS PID as a native PID.
+
+Two separate harness calls started and ended takes around a still-pending 45-second command, then verified the same shell PID/nonce and `persisted-λ`. Actual columns were **217 → 167** in all three Medium sessions; earlier elevated runs were 218 → 167. Completion payload lines are bounded to **60 base64 characters**. Truncated framing produced no record, damaged framing no successful record; browser loss produced interrupted/null success. This is not arbitrary resize/redraw decoding.
+
+All 27 command cases retained actual status attribution. A ParseException can coexist with raw PowerShell `$? = true`; `raw_shell_success` remains separate from request-attributed `parse_error=true` and semantic `shell_success=false`. The Bash logging pipeline retained `[7,0]` and producer exit 7. Extra ttyd clients were refused with EOF, and the original page still completed a nonce/PID round trip. Browser loss retained Win32 10054 and interrupted outcomes.
+
+| Cleanup, seconds | PowerShell 5.1 | PowerShell 7 | Git Bash |
+| --- | --- | --- | --- |
+| Normal | 1.890 | 1.859 | 1.875 |
+| Cancellation | 1.984 | 1.984 | 2.047 |
+| Browser loss | 0.063 | 0.047 | 0.047 |
+| Supervisor crash | 0.250 | 0.250 | 0.203 |
+
+These original successful paths checked parent/child/grandchild heartbeats, retained process-handle exit, stopped heartbeats and sentinel survival followed by scoped sentinel cleanup. The final original audit checked 609 PID/creation-time observations and found no remaining original processes, distinguishing reused PIDs. The review nevertheless found an error-path leak, addressed below.
+
+The accepted TUI JPEG is `interactive/000009.jpg` in each `tui-verified-*` directory. Implementer, reviewer and controller inspected active-take pixels showing `Native Windows TUI λ` and `[q] Finish this screen`, while run request 2 remained pending and before `q` request 4. The explicit same-page `snapshot` request 3 occurred after application readiness. [Pixel inspection metadata](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/active-tui-pixel-inspection.json) preserves hashes and timing.
+
+| TUI event, epoch seconds | PS5.1 | PS7 | Git Bash |
+| --- | --- | --- | --- |
+| Application ready | 1788994874.380406 | 1788994874.7009904 | 1788994874.6822324 |
+| Snapshot request 3 | 1788994875.4311693 | 1788994875.854709 | 1788994875.7615943 |
+| Inspected JPEG received | 1788994875.7732553 | 1788994875.8755016 | 1788994875.7821376 |
+| q request 4 | 1788994875.9168005 | 1788994876.04243 | 1788994875.9338365 |
+
+CDP trace proves the receiver continued draining/acknowledging during the ready-file wait/hold: 12/9/7 received events, 8/8/8 timeout polls and 4/3/2 acknowledged screencast frames. Some final-run TUI frames preceded the checkpoint, so this does not establish why earlier takes stalled or claim the snapshot alone caused a redraw. Acceptance is scoped to the inspected checkpoint method.
+
+## Retained failures and corrections
+
+- With the same ttyd binary, Job and CDP page, initial `startup-*` sessions disconnected without output. Adding explicit `ttyd -w <session-directory>` made `cwd-*` readiness pass. Upstream [ttyd PR 1502](https://github.com/tsl0922/ttyd/pull/1502) initializes command-line/cwd pointers to NULL; that supports the prerequisite without establishing the internal cause of our failure. No error 123 was observed in our ttyd log.
+- The probe's initial PowerShell prompt separately raised `InvokeMethodOnNull` because its error baseline was unset. Initializing and guarding the baseline corrected this; original and corrected evidence is retained separately from ttyd's cwd prerequisite.
+- Original Git Bash completion framing wrapped at 218 columns and ConPTY duplicated a character at a cursor repositioning boundary. Raw `outcomes-gitbash/network.jsonl` and `terminal.bin` retain the failure. Bounded multiline framing corrected the tested dimensions without adding a VT emulator.
+- Earlier active TUI takes lacked the TUI pixels despite a successful command and end PNG; a one-second hold still failed in PowerShell. `standard-normal-powershell51`, `standard-tui-*` and `tui-diagnostic-*` preserve those failures. The live PNG alone was never accepted instead of the active-take assertion.
+- Original handle-exit timing and transient heartbeat access failures are retained in elevated `session-v2-*` and `final-powershell7`. Bounded waits/retries corrected those observed cases; persistent access failures remain errors, motivating the review fix below.
+
+## Review fix: cleanup evidence failures
+
+Review of `49bc2932` found that snapshot, heartbeat or pending-result errors could escape before owned-resource cleanup. The correction protects evidence collection, always attempts terminal/Job release, gives the outside-job sentinel an independent finalizer, records stage-specific diagnostics, and returns failure when evidence is missing or reporting fails. An unknown process snapshot is `null`, not an empty successful ownership observation.
+
+The first actual Medium RED run occurred before the cleanup edit: snapshot, persistent heartbeat and pending-result failures each left three owned workers and the sentinel alive. Terminal-close failure left three workers alive, although its sentinel was already released; report-write failure escaped after resources exited. The normal base-source case released resources but returned `None`, failing only the new boolean result contract. Thus 0/6 is not a claim that all six scenarios leaked. Independent verifier teardown happened only after these observations were captured.
+
+A strengthened test replaced the synthetic post-release job error with native `TerminateJobObject` access denied (Win32 5), retaining real kill-on-close handle release. It again produced 0/6 against the unchanged base source, then **6/6 against the fix**, actual test-process exit 0. Each injected failure returned false, kept `normal_cleanup=failed`, preserved diagnostics and left no owned processes or sentinel alive. The successful fixture returned true. All cases independently recorded Medium SID `S-1-16-8192`, elevated false, elevation type 3, session 1.
+
+| Focused GREEN case | Cleanup seconds | Retained diagnostic stages |
+| --- | --- | --- |
+| Snapshot failure | 0.422 | `snapshot`; ownership snapshot remains null |
+| Persistent heartbeat access failure | 0.484 | `heartbeats_before`, `heartbeats_after`, `heartbeats_verify` |
+| Pending-result write failure | 0.468 | `pending_reply` |
+| Terminal failure plus native Job termination failure | 0.468 | `terminal_close`, `job_close` |
+| Final report write failure | 0.469 | `report_write`; absent report is not accepted |
+| Successful native ownership fixture | 0.453 | None |
+
+A fresh real ttyd/Chrome PowerShell 5.1 session under Medium closed in **2.891 seconds**, cleanup passed, supervisor launcher exit 0. A second real Medium session deliberately created a directory at its final `probe.json` path before close. The actual native replace failed with Win32 5; the supervisor exited **1**, stderr/stdout retained `report_write`, and the summary marked cleanup failed. An independent observer retained 19 native process handles before close (including supervisor and sentinel), verified all signaled, and checked stopped worker heartbeats. The remaining `probe.tmp` is an unsuccessful pre-error write, not a canonical passing report. No unrelated 27 outcome cases or 18 imported baseline assertions were rerun in this fix round.
+
+| Focused evidence | Result |
+| --- | --- |
+| [Initial pre-edit RED](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/fix-round1-red/cleanup-tests.json) | Expected 0/6, test process exit 1 |
+| [Native-fault RED against base](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/fix-round1-red-native/cleanup-tests.json) | Expected 0/6, exit 1 |
+| [GREEN ownership cases](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/fix-round1-green/cleanup-tests.json) | 6/6, exit 0; SHA256 `58c687f146fdb5063892dc0de1157d6f9915a4a11be3eb412dc5298b147e48a7` |
+| [Successful live close](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/fix-round1-live-powershell51/probe.json) | Cleanup passed, launcher exit 0 |
+| [Live report failure observer](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/fix-round1-live-report-failure-powershell51/failure-exit-observation.json) | Supervisor exit 1, failed cleanup, released resources |
+| [Fix-round archive](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/evidence-fix-round1.zip) | 265 files, 3,473,557 bytes; SHA256 `e22564f54e7bde79de3e2ea904ccab9d0faffa825c17f1e80c0beb78ff69ffab` |
+
+New probe SHA256: `b7f8a47bb09c9d4ed08642f8292df34a51b78a3040e0d6a3798a76286405ce90`. Focused test SHA256: `d85988949955a4f67fcc724a81c616b7f3e57c819f0a314632ab89355b4c89d1`. The initial RED test snapshot, hash `05c92c9ddfa9a4e05615634f9cbe80798f04856e994b3302ee7ad73decd6d20c`, is retained separately. The fix archive is also at `C:\Users\drew\movie-os-task1-20260909\evidence-fix-round1.zip`; exact new remote session directories are `evidence\fix-round1-{red,red-native,green}`, `evidence\fix-round1-live-powershell51` and `evidence\fix-round1-live-report-failure-powershell51`.
+
+An intervening SSH/SCP reset prevented the initial fix upload/submission before execution. A bounded read-only `hostname` retry succeeded; the unchanged remote base-source hash was verified before upload. These transport failures are separate from runtime test outcomes. No host/network repair was attempted. The task terminal remains available; probe resources have been released.
+
+## Reproduction and artifact index
+
+Run the following in a native **Medium-integrity** PowerShell terminal after copying the public probe and focused test beside the task-owned prerequisites. Choose new output directories; the probe refuses unsafe reuse. These are explicit argument arrays/paths, not PATH changes.
+
+```powershell
+$taskRoot = 'C:\Users\drew\movie-os-task1-20260909'
+$python = "$taskRoot\tools\python\cpython-3.12.14-windows-x86_64-none\python.exe"
+$uv = "$taskRoot\tools\uv\uv.exe"
+$probe = "$taskRoot\probe-windows.py"
+$env:UV_PYTHON_INSTALL_DIR = "$taskRoot\tools\python"
+$env:UV_CACHE_DIR = "$taskRoot\cache"
+$env:UV_NO_CONFIG = '1'
+$env:PYTHONIOENCODING = 'utf-8'
+& $python "$taskRoot\test-probe-cleanup.py" --directory "$taskRoot\evidence\cleanup-reproduction"
+
+& $uv run --python $python --script $probe --serve --ttyd "$taskRoot\tools\ttyd.exe" --browser 'C:\Program Files\Google\Chrome\Application\chrome.exe' --shell 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction" --launch-host workerbee
+```
+
+Keep `--serve` alive. In separate harness calls with the same task environment, run the phases below against that directory. `first` returns while its command is still pending; invoke `second` before the 45 seconds elapse. For PS7 or Git Bash use their absolute executable above and matching `--shell-kind powershell7` or `gitbash` on every invocation. `interactive` exercises the approved snapshot checkpoint. Separate fresh sessions exercise `cancel` and `browser-loss` instead of `close`.
+
+```powershell
+& $uv run --python $python --script $probe --phase first --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction"
+& $uv run --python $python --script $probe --phase second --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction"
+& $uv run --python $python --script $probe --phase interactive --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction"
+& $uv run --python $python --script $probe --phase prepare-cleanup --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction"
+& $uv run --python $python --script $probe --phase close --shell-kind powershell51 --directory "$taskRoot\evidence\session-reproduction"
+```
+
+Actual Medium launch commands were submitted through the already installed matching CLI, `C:\Users\drew\AppData\Local\Programs\Paseo\resources\bin\paseo.cmd terminal send-keys 1496200b-5545-4d85-af31-d01df72a3c11 '<encoded PowerShell command>' Enter --json`. SSH transport remained High; the daemon-owned terminal supplied the Medium token. The exact UTF16LE commands, launch scripts, subprocess argv and outputs are preserved as `*-submit-command.json`, `*-launch.ps1`, `*-launcher.json` and named stdout/stderr files, not inferred from this example.
+
+Local temporary evidence root is `.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/`; `remote/` mirrors remote `C:\Users\drew\movie-os-task1-20260909\evidence`. Links below currently target that ignored workspace. They will cease to resolve after SDD deletion unless evidence is consolidated first; source and this report remain reviewable independently.
+
+| Evidence | Contents |
+| --- | --- |
+| [Aggregate](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/probe.json) | Original 40 passed checks; per-session unrelated checks remain unavailable |
+| [Main archive](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/evidence-final.zip) | 2,323 files, 160,448,403 bytes; outcomes, continuity, ownership, prerequisites and original failures |
+| [Capture supplement](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/evidence-capture-final.zip) | 650 files, 23,823,967 bytes, overlapping earlier artifacts; accepted TUI pixels/traces |
+| [PS5.1 active TUI JPEG](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/tui-verified-powershell51/interactive/000009.jpg) | Inspected while request 2 pending, before q |
+| [PS7 active TUI JPEG](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/tui-verified-powershell7/interactive/000009.jpg) | Same acceptance method |
+| [Git Bash active TUI JPEG](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/tui-verified-gitbash/interactive/000009.jpg) | Same acceptance method |
+| [Original process audit](../../../.superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/final-process-audit.json) | 609 PID/creation-time observations, no remaining original processes |
+
+Main archive SHA256: `ba7438c8686183ceb1f55dd32e62bed3fc57d082bc642f5487561aca364da024`. Capture supplement SHA256: `4b1186772fec51d3581390ae4dc462811d5b6b7c86018fc36b0062de384a844a`. Remote copies are `$taskRoot\evidence-final.zip` and `$taskRoot\evidence-capture-final.zip`. Original runtime source snapshots are retained locally as `probe-standard-runtime-a107564b5e34.py` (SHA256 `a107564b5e341c84d5ae21bc9ecbb7c41cd1544752597c78f0cde5dae37489c2`) and `probe-final-runtime-be36b8182b5c.py` (SHA256 `be36b8182b5caa1dc071dad3bd075cc44d40a3b6fe7b3282b6da739b44259d40`). Commit `49bc2932` probe SHA256 is `515f382ecb960c6111649784e8ecb23d66f2ac67f7f2cdbb498760a2679bf67b`.
+
+For command outcomes, use the full launch command above with `--outcomes` instead of `--serve`, a fresh directory, and each selected shell. For a supervisor-crash check, start a fresh `--serve`, run `prepare-cleanup`, then use `& $uv run --python $python --script $probe --crash-check --directory "$taskRoot\evidence\session-reproduction"` against that crash session. The observer validates retained native identities before killing the supervisor and releases the sentinel afterward. Inspect active TUI JPEGs before claiming their pixels; a successful phase alone does not establish the visual assertion.
+
+The probe's `--help` exposes these modes. Original complete aggregate replay commands are below; they validate retained evidence, not a new Windows runtime execution:
+
+```sh
+python3 tests/proving-it-works-with-a-movie/probe-windows.py --aggregate .superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote
+python3 tests/proving-it-works-with-a-movie/probe-windows.py --assert-result .superpowers/sdd/2026-09-09-proof-movie-os-compatibility/task-1-evidence/remote/probe.json
+```
+
+## Required production follow-through
+
+General screencast freshness is a **required production fix and test in Tasks 8–9**; stalled takes must not pass. Resizing requires a production shell adapter/supervisor that handles completion framing through resize/redraw, with damaged/missing framing remaining unknown/interrupted. Further work includes broader ownership failure injection, the full OS support matrix, and assembly/narration/verification portability. This probe does not establish those results. The Task 1 mechanism gate and independent review must finish before the bulk port begins.

+ 112 - 57
tests/proving-it-works-with-a-movie/probe-windows.py

@@ -942,69 +942,124 @@ class Supervisor:
             if self.report.get("browser_loss_requested"):
                 self.report["checks"]["browser_loss"] = {"status": "passed", "continuity": False, "outcome": "interrupted", "observed_error": failure}
         finally:
-            self.cleanup(failure)
-        return 1 if failure and not self.report.get("browser_loss_requested") else 0
+            cleanup_ok = self.cleanup(failure)
+        return 1 if not cleanup_ok or (failure and not self.report.get("browser_loss_requested")) else 0
 
     def cleanup(self, failure):
         terminal, job = self.terminal, self.terminal.job
         started = time.monotonic()
-        handles = job.snapshot()
-        before_heartbeats = heartbeats(self.args.directory / "heartbeats")
-        if self.pending is not None:
-            result = {"outcome": "interrupted" if failure or self.report.get("close_operation") else "unknown",
-                      "shell_success": None, "native_exit_code": None, "shell_error": failure,
-                      "command": self.pending["command"]}
-            self.results[str(self.pending["id"])] = result
-            self.reply(self.pending["id"], result)
-        try:
-            if terminal.cdp and not failure:
-                if terminal.take:
-                    terminal.end_take()
-                terminal.key("CTRL_C")
-                deadline = time.monotonic() + 1.5
-                while time.monotonic() < deadline:
-                    terminal.cdp.pump()
-                terminal.type("exit")
-                deadline = time.monotonic() + 1.5
-                while time.monotonic() < deadline and not terminal.closed:
-                    terminal.cdp.pump()
-        except Exception as error:
-            self.report["graceful_shutdown_diagnostic"] = repr(error)
+        mode = "cancel_cleanup" if self.report.get("close_operation") == "cancel" else "normal_cleanup"
+        self.report["checks"][mode] = {"status": "failed"}
+        diagnostics = self.report["cleanup_diagnostics"] = []
+        handles = before = after = stable = None
+        remaining, sentinel_alive = None, False
+
+        def attempt(stage, operation):
+            try:
+                return operation()
+            except BaseException as error:
+                diagnostic = {"stage": stage, "error": repr(error), "traceback": traceback.format_exc()}
+                diagnostics.append(diagnostic)
+                # Preserve diagnostics even when the evidence directory is unwritable.
+                try:
+                    print(json.dumps({"cleanup_error": diagnostic}), file=sys.stderr, flush=True)
+                except (OSError, ValueError):
+                    pass
+                return None
+
         try:
-            terminal.close()
-            # Job membership can reach zero before Windows signals every exiting
-            # process handle. Retain and wait the verified handles as well.
-            self.report["initial_unsignaled_handles"] = [item["pid"] for item in handles if job.k.WaitForSingleObject(item["handle"], 0) != 0]
-            deadline = started + 8
-            while time.monotonic() < deadline and any(job.k.WaitForSingleObject(item["handle"], 0) != 0 for item in handles):
-                time.sleep(0.05)
-            remaining = [item["pid"] for item in handles if job.k.WaitForSingleObject(item["handle"], 0) != 0]
-            self.report["owned_children_remaining"] = remaining
-            self.report["unrelated_sentinel_alive"] = self.sentinel is not None and self.sentinel.poll() is None
-            self.report["shutdown_seconds"] = time.monotonic() - started
-            after_heartbeats = heartbeats(self.args.directory / "heartbeats")
-            time.sleep(0.4)
-            stopped = after_heartbeats == heartbeats(self.args.directory / "heartbeats")
-            self.report["cleanup"] = {"before": before_heartbeats, "after": after_heartbeats,
-                                      "heartbeats_stopped": stopped,
-                                      "owned_handles": [{k: v for k, v in item.items() if k != "handle"} for item in handles]}
-            mode = "cancel_cleanup" if self.report.get("close_operation") == "cancel" else "normal_cleanup"
-            passed = not remaining and self.report["unrelated_sentinel_alive"] and stopped and set(before_heartbeats) == {"parent", "child", "grandchild"} and self.report["shutdown_seconds"] < 10
-            self.report["checks"][mode] = {"status": "passed" if passed else "failed"}
+            handles = attempt("snapshot", job.snapshot)
+            before = attempt("heartbeats_before", lambda: heartbeats(self.args.directory / "heartbeats"))
+            if self.pending is not None:
+                result = {"outcome": "interrupted" if failure or self.report.get("close_operation") else "unknown",
+                          "shell_success": None, "native_exit_code": None, "shell_error": failure,
+                          "command": self.pending["command"]}
+                self.results[str(self.pending["id"])] = result
+                attempt("pending_reply", lambda: self.reply(self.pending["id"], result))
+
+            def graceful_shutdown():
+                if terminal.cdp and not failure:
+                    if terminal.take:
+                        terminal.end_take()
+                    terminal.key("CTRL_C")
+                    deadline = time.monotonic() + 1.5
+                    while time.monotonic() < deadline:
+                        terminal.cdp.pump()
+                    terminal.type("exit")
+                    deadline = time.monotonic() + 1.5
+                    while time.monotonic() < deadline and not terminal.closed:
+                        terminal.cdp.pump()
+
+            attempt("graceful_shutdown", graceful_shutdown)
         finally:
-            for item in handles:
-                job.k.CloseHandle(item["handle"])
-            if self.sentinel:
-                self.sentinel.terminate()
-                self.sentinel.wait(timeout=5)
-            self.report.update(terminal_client_count=len(terminal.socket_ids), observed_session_id=terminal.session,
-                               filmed_session_id=self.takes[0]["session"] if self.takes else None,
-                               socket_ids=terminal.socket_ids, observed_frames=terminal.frames,
-                               terminal_sizes=terminal.terminal_sizes, framing_chunk_columns=60,
-                               framing_scope="Observed dimensions only; arbitrary resizing/redraw is unverified",
-                               job_structure_sizes=job.sizes, launches=terminal.launches)
-            write_json(self.args.directory / "probe.json", self.report)
-            print(json.dumps({"finished": str(self.args.directory), "checks": self.report["checks"], "failure": failure}), flush=True)
+            try:
+                try:
+                    attempt("terminal_close", terminal.close)
+                finally:
+                    # Terminal.close may fail before reaching the job. Job.close
+                    # releases its kill-on-close handle even if termination fails.
+                    if job.handle:
+                        attempt("job_close", job.close)
+                    for name, resource in (("terminal_output", terminal.output), ("network_log", terminal.raw)):
+                        attempt(name + "_close", resource.close)
+                    if terminal.cdp:
+                        attempt("cdp_close", terminal.cdp.ws.close)
+                        attempt("cdp_trace_close", terminal.cdp.trace.close)
+                if handles is not None:
+                    def wait_handles():
+                        self.report["initial_unsignaled_handles"] = [p["pid"] for p in handles if job.k.WaitForSingleObject(p["handle"], 0) != 0]
+                        deadline = started + 8
+                        while time.monotonic() < deadline and any(job.k.WaitForSingleObject(p["handle"], 0) != 0 for p in handles):
+                            time.sleep(0.05)
+                        return [p["pid"] for p in handles if job.k.WaitForSingleObject(p["handle"], 0) != 0]
+
+                    remaining = attempt("owned_handle_wait", wait_handles)
+                sentinel_alive = attempt("sentinel_status", lambda: self.sentinel is not None and self.sentinel.poll() is None)
+                after = attempt("heartbeats_after", lambda: heartbeats(self.args.directory / "heartbeats"))
+                time.sleep(0.4)
+                stable = attempt("heartbeats_verify", lambda: heartbeats(self.args.directory / "heartbeats"))
+            finally:
+                try:
+                    for item in handles or []:
+                        def release_handle(item=item):
+                            if not job.k.CloseHandle(item["handle"]):
+                                raise ctypes.WinError(ctypes.get_last_error())
+                        attempt("owned_handle_close", release_handle)
+                finally:
+                    # Sentinel cleanup is independent of every job/terminal step.
+                    try:
+                        if self.sentinel:
+                            attempt("sentinel_terminate", self.sentinel.terminate)
+                    finally:
+                        if self.sentinel:
+                            attempt("sentinel_wait", lambda: self.sentinel.wait(timeout=1))
+
+        stopped = after is not None and stable is not None and after == stable
+        self.report.update(owned_children_remaining=remaining, unrelated_sentinel_alive=sentinel_alive,
+                           shutdown_seconds=time.monotonic() - started)
+        self.report["cleanup"] = {"before": before, "after": after, "heartbeats_stopped": stopped,
+                                  "owned_handles": None if handles is None else
+                                  [{k: v for k, v in item.items() if k != "handle"} for item in handles]}
+        passed = (not diagnostics and remaining == [] and sentinel_alive is True and stopped
+                  and before is not None and set(before) == {"parent", "child", "grandchild"}
+                  and self.report["shutdown_seconds"] < 10)
+        self.report["checks"][mode] = {"status": "passed" if passed else "failed"}
+        self.report.update(terminal_client_count=len(terminal.socket_ids), observed_session_id=terminal.session,
+                           filmed_session_id=self.takes[0]["session"] if self.takes else None,
+                           socket_ids=terminal.socket_ids, observed_frames=terminal.frames,
+                           terminal_sizes=terminal.terminal_sizes, framing_chunk_columns=60,
+                           framing_scope="Observed dimensions only; arbitrary resizing/redraw is unverified",
+                           job_structure_sizes=job.sizes, launches=terminal.launches)
+        attempt("report_write", lambda: write_json(self.args.directory / "probe.json", self.report))
+        if diagnostics:
+            self.report["checks"][mode] = {"status": "failed"}
+        prior_errors = len(diagnostics)
+        attempt("summary_write", lambda: print(json.dumps({"finished": str(self.args.directory),
+                "checks": self.report["checks"], "failure": failure, "cleanup_diagnostics": diagnostics}), flush=True))
+        if len(diagnostics) != prior_errors:
+            self.report["checks"][mode] = {"status": "failed"}
+            attempt("report_write", lambda: write_json(self.args.directory / "probe.json", self.report))
+        return passed and not diagnostics
 
 
 def submit_request(args):

+ 153 - 0
tests/proving-it-works-with-a-movie/test-probe-cleanup.py

@@ -0,0 +1,153 @@
+#!/usr/bin/env python3
+"""Focused native Windows cleanup failures, verified with real process handles."""
+import argparse
+import contextlib
+import ctypes
+import hashlib
+import importlib.util
+import json
+from pathlib import Path
+import subprocess
+import sys
+import time
+import traceback
+from unittest.mock import patch
+
+
+def exercise(probe, root, case):
+    directory = root / case
+    supervisor = probe.Supervisor(argparse.Namespace(directory=directory, shell_kind="powershell51",
+                                                     launch_host="workerbee"))
+    terminal, job = supervisor.terminal, supervisor.terminal.job
+    real_close = terminal.close
+    handles = []
+    observed = {"case": case, "passed": False, "token": supervisor.report["environment"]["token"]}
+
+    def fail(label):
+        def injected(*args, **kwargs):
+            raise PermissionError(f"injected persistent {label} failure")
+        return injected
+
+    try:
+        job.spawn([sys.executable, str(Path(probe.__file__).resolve()), "--worker", "parent",
+                   "--directory", str(directory / "heartbeats")], directory, directory / "worker.log")
+        supervisor.sentinel = subprocess.Popen([sys.executable, str(Path(probe.__file__).resolve()),
+                                               "--worker", "sentinel", "--directory", str(directory / "sentinel")],
+                                              stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+        deadline = time.monotonic() + 10
+        while time.monotonic() < deadline:
+            before = probe.heartbeats(directory / "heartbeats")
+            if set(before) == {"parent", "child", "grandchild"} and probe.heartbeats(directory / "sentinel"):
+                break
+            time.sleep(0.05)
+        assert set(before) == {"parent", "child", "grandchild"}, before
+        handles = job.snapshot()  # Independent verifier retains handles before injecting faults.
+        assert {entry["pid"] for entry in before.values()} <= {entry["pid"] for entry in handles}
+        sentinel_handle = job.open_process(supervisor.sentinel.pid)
+        try:
+            member = ctypes.c_long()
+            assert job.k.IsProcessInJob(sentinel_handle, job.handle, ctypes.byref(member))
+            assert member.value == 0, "Sentinel must be outside the owned job"
+        finally:
+            job.k.CloseHandle(sentinel_handle)
+        supervisor.pending = {"id": 1, "command": "native heartbeat parent fixture"}
+        supervisor.report["close_operation"] = "close"
+        observed["before"] = before
+        with contextlib.ExitStack() as faults:
+            if case == "snapshot":
+                faults.enter_context(patch.object(job, "snapshot", fail("snapshot")))
+            elif case == "heartbeat":
+                faults.enter_context(patch.object(probe, "heartbeats", fail("heartbeat access")))
+            elif case == "pending_reply":
+                faults.enter_context(patch.object(supervisor, "reply", fail("pending reply")))
+            elif case == "terminal_job":
+                faults.enter_context(patch.object(terminal, "close", fail("terminal close")))
+
+                def termination_failure(*args):
+                    ctypes.set_last_error(5)  # Access denied; real CloseHandle must still kill the job.
+                    return 0
+
+                faults.enter_context(patch.object(job.k, "TerminateJobObject", termination_failure))
+            elif case == "report_write":
+                real_write = probe.write_json
+
+                def report_failure(path, value):
+                    if path.name == "probe.json":
+                        raise PermissionError("injected persistent report write failure")
+                    return real_write(path, value)
+
+                faults.enter_context(patch.object(probe, "write_json", report_failure))
+            started = time.monotonic()
+            try:
+                observed["cleanup_return"] = supervisor.cleanup(None)
+            except BaseException as error:
+                observed.update(escaped_error=repr(error), escaped_traceback=traceback.format_exc())
+            observed["cleanup_seconds"] = time.monotonic() - started
+        observed["owned_alive_after_cleanup"] = [p["pid"] for p in handles if job.k.WaitForSingleObject(p["handle"], 0) != 0]
+        observed["sentinel_alive_after_cleanup"] = supervisor.sentinel.poll() is None
+        after = probe.heartbeats(directory / "heartbeats")
+        time.sleep(0.3)
+        observed["heartbeats_stopped"] = after == probe.heartbeats(directory / "heartbeats")
+        observed["probe_report_exists"] = (directory / "probe.json").exists()
+        observed["report"] = supervisor.report
+        assert observed["owned_alive_after_cleanup"] == [], observed
+        assert not observed["sentinel_alive_after_cleanup"], observed
+        assert observed["heartbeats_stopped"], observed
+        assert observed["cleanup_seconds"] < 10, observed
+        assert "escaped_error" not in observed, observed
+        assert observed["cleanup_return"] is (case == "normal"), observed
+        expected_status = "passed" if case == "normal" else "failed"
+        assert supervisor.report["checks"]["normal_cleanup"]["status"] == expected_status, observed
+        if case != "normal":
+            assert supervisor.report["cleanup_diagnostics"], observed
+            expected = {"snapshot": {"snapshot"}, "heartbeat": {"heartbeats_before", "heartbeats_after"},
+                        "pending_reply": {"pending_reply"}, "terminal_job": {"terminal_close", "job_close"},
+                        "report_write": {"report_write"}}[case]
+            assert expected <= {d["stage"] for d in supervisor.report["cleanup_diagnostics"]}, observed
+        assert observed["probe_report_exists"] is (case != "report_write"), observed
+        if case != "report_write":
+            assert probe.read_json(directory / "probe.json")["checks"]["normal_cleanup"]["status"] == expected_status
+        observed["passed"] = True
+    except BaseException as error:
+        observed.update(assertion_error=repr(error), assertion_traceback=traceback.format_exc())
+    finally:
+        # Emergency verifier teardown is outside the code under test. The above
+        # observations are captured before it, so RED cannot borrow this cleanup.
+        try:
+            real_close()
+        finally:
+            if supervisor.sentinel and supervisor.sentinel.poll() is None:
+                supervisor.sentinel.terminate()
+                supervisor.sentinel.wait(timeout=5)
+            for process in handles:
+                job.k.WaitForSingleObject(process["handle"], 5000)
+                job.k.CloseHandle(process["handle"])
+        probe.write_json(directory / "test-observation.json", observed)
+    return observed
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--directory", type=Path, required=True)
+    parser.add_argument("--probe", type=Path, default=Path(__file__).with_name("probe-windows.py"))
+    args = parser.parse_args()
+    if sys.platform != "win32":
+        parser.error("Native Windows is required; this test cannot be skipped green")
+    args.directory.mkdir(parents=True, exist_ok=False)
+    spec = importlib.util.spec_from_file_location("windows_probe", args.probe)
+    probe = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(probe)
+    cases = [exercise(probe, args.directory, case) for case in
+             ("snapshot", "heartbeat", "pending_reply", "terminal_job", "report_write", "normal")]
+    report = {"probe_sha256": hashlib.sha256(args.probe.read_bytes()).hexdigest(),
+              "test_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+              "cases": cases, "passed": sum(case["passed"] for case in cases), "total": len(cases)}
+    probe.write_json(args.directory / "cleanup-tests.json", report)
+    print(json.dumps({"passed": report["passed"], "total": report["total"],
+                      "cases": [{key: c.get(key) for key in ("case", "passed", "owned_alive_after_cleanup",
+                                                             "sentinel_alive_after_cleanup", "escaped_error")} for c in cases]}), flush=True)
+    return 0 if report["passed"] == report["total"] else 1
+
+
+if __name__ == "__main__":
+    sys.exit(main())