Browse Source

docs: make proof-movie recipes preserve failures

Repair the shipped Unix, logging, subtitle, cursor, narration, and recorder guidance against the literal fake-boundary failures recorded for Task 4. The primary pipeline and subtitle recipe now fail fast, measured offsets reach subtitle generation, producer logging preserves the real status under the pipefail owner, and cursor mouseup restores the released state.

Document the accepted narration/cache contract, cooperative recorder cleanup limits, and the safe contracts-suite entrypoint without changing Windows recipes or the existing evidence and human-viewing gates. Include the controller-owned plan bookkeeping and record the executable RED/GREEN results while leaving independent fresh-reader trials pending.

Prompt: implement Task 4 focused executable movie-guide corrections after Tasks 1-3, using writing-skills and only fake producers, text fixtures, and fake DOM execution.

Verification: python3 .superpowers/review/pr2214/committee/recipe-probes.py; uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite contracts; git diff --check.
Drew Ritter 1 week ago
parent
commit
b206e0cbe1

+ 42 - 27
docs/superpowers/plans/2026-09-11-movie-committee-repairs.md

@@ -27,26 +27,29 @@
 
 **Interfaces:** Preserve manifest fields `id`, `text`, `wav`, `duration`, `synthesis`. Normalize scene text for identity using `" ".join(text.split())`, without dropping Unicode or punctuation. Resolve each manifest WAV relative to the narration directory. Transcript comparison is a separate, tolerant operation. Existing movie scenes retain their own source audio and omit narration offsets.
 
-- [ ] Add failing tests before code: accepted two-scene render, forced rejection of first scene, exception in second, normal retry never reuses rejected bytes; strict unavailable verification withdraws acceptance; unchanged accepted settings still cache. Inject failures in synthesis and duration and assert published entries cannot point at rejected replacements. Preserve failed bytes as evidence.
-- [ ] Publish manifest atomically (`temporary.write_text(..., encoding="utf-8"); temporary.replace(manifest_path)`) and withdraw acceptance before overwriting any referenced WAV. Publish each accepted scene only after transcript/ASR gates and duration succeed. Preserve the existing bounded attempts and no-narration CLI behavior. Do not silently create a fallback acceptance record.
-- [ ] Add unsupported-comparison tests for unrelated Chinese/Japanese (including short and mixed-script text), symbols-only scripts, multiline text, accented Latin, and spaced Cyrillic. Use Unicode casefold/normalization and whitespace-preserving tokenization. Explicitly detect scripts requiring segmentation; do not infer support from average word length or introduce CJK character thresholds. Unsupported comparisons report unavailable/nonzero under `--drift-check` and `--verify on`; `auto` warns/allows unavailable ASR, `off` bypasses ASR. The mandatory chat transcript gate never accepts unsupported comparison. Preserve supported-word thresholds and empty/missing speech rejection.
-- [ ] Preflight required ffprobe before synthesis, with a meaningful missing-tool test. Keep existing mocked tests portable by mocking this boundary.
-- [ ] Add safe assembly tests: removed narration ignores leftover WAV and creates no offset; required narration with missing manifest/entry/WAV or changed text fails before encoding; accepted referenced WAV is selected even when named differently; movie scenes retain own audio and no narration offset. Validate all narration requirements before encoding the first segment. Update existing media fixture declarations/manifests to reflect the contract without running those media suites.
-- [ ] Correct movie segment inputs: probe audio-stream presence, supply anullsrc only for silent source movies, explicitly map selected source video/audio, and fit both width and requested inner height before padding. Test input selection and computed geometry with mocked probes/encoder, including 2560x1080 into 1920x1080. No real encoding.
-- [ ] Correct percent-bearing sequence paths in assembly and checker: escape only literal directory percent signs (or use controlled cwd plus fixed basename), retaining the intended `%08d`/numbering placeholder. Test the path helper/selected input-output target with byte sentinels; never sample media.
-- [ ] Run safe narration and new assembly/path contract tests; retain existing cache/drift regressions. Self-review and commit.
+- [x] Add failing tests before code: accepted two-scene render, forced rejection of first scene, exception in second, normal retry never reuses rejected bytes; strict unavailable verification withdraws acceptance; unchanged accepted settings still cache. Inject failures in synthesis and duration and assert published entries cannot point at rejected replacements. Preserve failed bytes as evidence.
+- [x] Publish manifest atomically (`temporary.write_text(..., encoding="utf-8"); temporary.replace(manifest_path)`) and withdraw acceptance before overwriting any referenced WAV. Publish each accepted scene only after transcript/ASR gates and duration succeed. Preserve the existing bounded attempts and no-narration CLI behavior. Do not silently create a fallback acceptance record.
+- [x] Add unsupported-comparison tests for unrelated Chinese/Japanese (including short and mixed-script text), symbols-only scripts, multiline text, accented Latin, and spaced Cyrillic. Use Unicode casefold/normalization and whitespace-preserving tokenization. Explicitly detect scripts requiring segmentation; do not infer support from average word length or introduce CJK character thresholds. Unsupported comparisons report unavailable/nonzero under `--drift-check` and `--verify on`; `auto` warns/allows unavailable ASR, `off` bypasses ASR. The mandatory chat transcript gate never accepts unsupported comparison, including existing cached chat clips under off/auto. Preserve supported-word thresholds and empty/missing speech rejection.
+- [x] Preflight required ffprobe before synthesis, with a meaningful missing-tool test. Keep existing mocked tests portable by mocking this boundary.
+- [x] Add safe assembly tests: removed narration ignores leftover WAV and creates no offset; required narration with missing manifest/entry/WAV or changed text fails before encoding; accepted referenced WAV is selected even when named differently; movie scenes retain own audio and no narration offset. Validate all narration requirements before encoding the first segment. Update existing media fixture declarations/manifests to reflect the contract without running those media suites.
+- [x] Correct movie segment inputs: probe audio-stream presence, supply anullsrc only for silent source movies, explicitly map selected source video/audio, and fit both width and requested inner height before padding. Test input selection and computed geometry with mocked probes/encoder, including 2560x1080 into 1920x1080. No real encoding.
+- [x] Correct percent-bearing sequence paths in assembly and checker: escape only literal directory percent signs (or use controlled cwd plus fixed basename), retaining the intended `%08d`/numbering placeholder. Test the path helper/selected input-output target with byte sentinels; never sample media.
+- [x] Run safe narration and new assembly/path contract tests; retain existing cache/drift regressions. Self-review and commit.
 
 ### Task 2: Keep every subtitle inside its scene and select the supplied track
 
-**Files:** `skills/proving-it-works-with-a-movie/scripts/make-subtitles`, `scripts/burn-subtitles`; `tests/proving-it-works-with-a-movie/test_subtitles.py` or a separate safe subtitle contract module.
+**Files:** `skills/proving-it-works-with-a-movie/scripts/make-subtitles`, `scripts/burn-subtitles`, `scripts/check-movie` (SRT text parser only); `tests/proving-it-works-with-a-movie/test_subtitles.py` or a separate safe subtitle contract module, safe checker policy/parser regressions, and `run-tests.py`.
 
 **Interfaces:** Manifest schema stays unchanged. Assembly offsets select membership and start times; manual offsets retime selected scenes without reintroducing omitted scenes. Task 1 makes assembly offsets refer only to accepted narration.
 
-- [ ] Add failing timing tests by parsing emitted SRT: five chunks in 0.5 seconds followed immediately by another scene; one chunk in 12 seconds; mixed-length chunks; submillisecond/invalid duration handling; empty cut; nonzero manual offsets. Assert every word survives, ordered positive millisecond cue intervals stay within the measured scene, the final cue covers the narration end within rounding, and reported end agrees with emitted cues.
-- [ ] Allocate proportional durations over the entire scene. Readability limits can guide splitting/allocation but cannot overflow or truncate the measured interval. Coalesce chunks if there are fewer representable milliseconds than chunks; reject unrepresentable/nonpositive durations clearly rather than emit invalid cues. Remove the serialization fallback that extends collapsed cues by one second.
-- [ ] Preserve assembly membership selection and partial manual offsets. Validate offsets keys that have no manifest entry with a named nonzero error; do not reject manifest entries omitted from the assembled cut. Add a failing mismatch regression before this change.
-- [ ] Add safe replacement-track tests for explicit soft mode and hard-burn fallback by inspecting selected stream maps, including an input already containing subtitles and a video without audio. Explicitly select `0:v:0`, optional source audio, and `1:s:0` from the supplied SRT. Preserve truthful fallback diagnostics.
-- [ ] Run only safe subtitle classes (existing offset/path tests and BOM/fallback cases plus new contract tests). Self-review and commit.
+- [x] Add failing timing tests by parsing emitted SRT: five chunks in 0.5 seconds followed immediately by another scene; one chunk in 12 seconds; mixed-length chunks; submillisecond/invalid duration handling; empty cut; nonzero manual offsets. Assert every word survives, ordered positive millisecond cue intervals stay within the measured scene, the final cue covers the narration end within rounding, and reported end agrees with emitted cues.
+- [x] Allocate proportional durations over the entire scene. Readability limits can guide splitting/allocation but cannot overflow or truncate the measured interval. Coalesce chunks if there are fewer representable milliseconds than chunks; reject unrepresentable/nonpositive durations clearly rather than emit invalid cues. Remove the serialization fallback that extends collapsed cues by one second.
+- [x] Preserve assembly membership selection as the manifest/offset intersection, including unknown offset keys producing no cues, and partial manual offsets. Enforce accepted membership in Task 1's assembly handoff tests rather than changing this existing subtitle CLI contract.
+- [x] Add one safe producer/consumer regression that invokes narrate, assemble, and make-subtitles sequentially using their real files and mocked synthesis/probing/encoding. Remove an opening scene's narration on rerun: its old WAV survives as evidence, assembly omits that audio/offset, and the remaining scene's caption starts at its measured assembly offset. Assert emitted manifest/offset/SRT data and chosen inputs, not full commands.
+- [x] Add safe replacement-track tests for explicit soft mode and hard-burn fallback by inspecting selected stream maps, including an input already containing subtitles and a video without audio. Explicitly select `0:v:0`, optional source audio, and `1:s:0` from the supplied SRT. Preserve truthful fallback diagnostics.
+- [x] Repair `subtitle_end` to read cue timing lines rather than any caption text containing an arrow. Root reproduced a valid cue with `Follow source --> destination.` raising ValueError. Add text-only parser tests for literal arrows, fake timestamps in caption text, actual malformed cue timing, empty subtitles, and multiple cues; keep the checker's existing last-cue policy unchanged.
+- [x] Register a `contracts` test-runner suite for `test_*contract*.py`, so the new safe regression files run through the normal entrypoint and are included in `all`. Keep only mocked/text-based tests in those modules. Verify by executing `--suite contracts`, never `--suite all` in this pass.
+- [x] Run only safe subtitle classes (existing offset/path tests and BOM/fallback cases plus new contract tests). Self-review and commit.
 
 ### Task 3: Own recorder resources from acquisition through shutdown
 
@@ -54,27 +57,39 @@
 
 **Interfaces:** Preserve `serve/run/key/watch/close`, prompt markers, session files, exit 1 on failure and exit 2 only for a live unfinished command, 5 fps timing, and 1600x900 viewport. Preserve logs; only remove the recorder's own profile and readiness state.
 
-- [ ] Add startup-failure tests with fake Popen handles and no processes: first/second log open, ttyd launch, browser launch, session metadata write, terminal log open, later connection failure. Verify each acquired process is cleaned, handles close, failed startup cannot remain ready, and profile cleanup is owned. Register resources immediately under an encompassing try/finally (or ExitStack); no general process framework.
-- [ ] Resolve session directory before child arguments, cwd, and profile construction. Test a relative path against the actual child argv/cwd relationship using fake processes.
-- [ ] Retire PID metadata after serve cleanup and remove stale ready state; test repeated close after cleanup kills nothing and retains logs. Make profile-removal failure truthful. Avoid creating a new cross-platform PID-identity protocol.
-- [ ] Add no-record observation tests for connection/session loss using fake CDP and log text. Poll connection/liveness while waiting; a dead session is failure, never `running`. A live timed-out command still returns 2. Do not consume prompt status incorrectly or lose completed native exit status.
-- [ ] Test timing using a fake clock, capture callback returning byte tokens, and mocked writes: a final capture crossing a one-second endpoint must fill exactly the bounded 5 fps slots; also cover hold endpoint, normal completion and mid-capture stalls. No real image/frame inspection or integration tests.
-- [ ] Fix ST-terminated OSC stripping and test both BEL/ST markers in actual visible-text parsing. Emit ASCII-escaped stdout JSON (or explicit UTF-8) and test a CP1252 stream with René and λ; keep UTF-8 JSON files.
-- [ ] Register real-session test cleanup immediately after Popen so failed setUp cannot leak it; do not run SessionTests in this pass. Run safe prompt, serve-argument, and new recorder contract tests; self-review and commit.
+- [x] Add startup-failure tests with fake Popen handles and no processes: first/second log open, ttyd launch, browser launch, session metadata write, terminal log open, later connection failure. Verify each acquired process is cleaned, handles close, failed startup cannot remain ready, and profile cleanup is owned. Register resources immediately under an encompassing try/finally (or ExitStack); no general process framework.
+- [x] Resolve session directory before child arguments, cwd, and profile construction. Test a relative path against the actual child argv/cwd relationship using fake processes.
+- [x] Make serve the cleanup owner: close writes the existing stop request and waits with one overall 30-second deadline for completed cleanup, instead of killing historical numeric PIDs. Honor stop requests inside startup pump/retry loops too. Retire PID metadata atomically only after confirmed process and profile cleanup and remove stale ready state; test repeated close after cleanup kills nothing and retains logs. A wait timeout, unreadable/missing metadata, unavailable serve, or failed profile removal is not successful cleanup and returns nonzero. The deadline accommodates an active bounded CDP call, both five-second child waits, and profile release. Avoid creating a new cross-platform PID-identity protocol.
+- [x] Add no-record observation tests for connection/session loss using fake CDP and log text. Poll connection/liveness while waiting; a dead session is failure, never `running`. A live timed-out command still returns 2. Do not consume prompt status incorrectly or lose completed native exit status.
+- [x] Test timing using a fake clock, capture callback returning byte tokens, and mocked writes: a final capture crossing a one-second endpoint must fill exactly the bounded 5 fps slots; also cover hold endpoint, normal completion and mid-capture stalls. No real image/frame inspection or integration tests.
+- [x] Fix ST-terminated OSC stripping and test both BEL/ST markers in actual visible-text parsing. Emit ASCII-escaped stdout JSON (or explicit UTF-8) and test a CP1252 stream with René and λ; keep UTF-8 JSON files.
+- [x] Register real-session test cleanup immediately after Popen so failed setUp cannot leak it; do not run SessionTests in this pass. Retain the tests' real owned-process assertions when shutdown clears PID metadata: snapshot owned IDs for assertions while the session is live rather than iterating an empty completed list. Run safe prompt, serve-argument, and new recorder contract tests; self-review and commit.
 
 ### Task 4: Make shipped recipes preserve failures and measured timing
 
-**Files:** `skills/proving-it-works-with-a-movie/SKILL.md`, `assembling.md`, `rendering-from-a-log.md`, `recording-motion.md`, `recording-a-terminal.md`, `narrating.md`; focused documentation test evidence under ignored review/SDD directories.
+**Files:** `skills/proving-it-works-with-a-movie/SKILL.md`, `assembling.md`, `rendering-from-a-log.md`, `recording-motion.md`, `recording-a-terminal.md`, `narrating.md`, `tests/proving-it-works-with-a-movie/README.md`; focused documentation test evidence under ignored review/SDD directories.
 
 **Interfaces:** Documentation describes Tasks 1-3's existing formats and corrected behavior. Existing evidence standards and Windows shell recipes remain intact. This is a focused reference correction, not a broad skill-policy rewrite.
 
-- [ ] Use writing-skills. Preserve the before-change fresh-reader reference trial and execute the original recipes with fake producer commands and log fixtures to record failures. No real media or OS recording.
-- [ ] Put `set -euo pipefail` in the primary Unix pipeline's executing Bash scope, retaining all five commands and offsets. In the assembling subtitle example pass `--offsets-json segments/offsets.json` and stop if subtitle production fails. Explain current scene plus accepted manifest, relative WAV reference, and movie own-audio behavior in one paragraph.
-- [ ] Put the producer-to-tee logging pipeline under the shell that owns pipefail, preserving the real producer exit status and printed STARTED/FINISHED/EXIT_STATUS markers. Execute the recipe with failing and passing fake producers; assert shell status and log content without matching a large rendered command.
-- [ ] Restore the cursor transform on mouseup. Execute the documented JavaScript with a minimal fake DOM/event dispatcher and assert repeated press/release state changes; no browser or pixels.
-- [ ] State that session directories, like take directories, must be new or empty on retries. Document unsupported transcript verification and existing retry behavior accurately without adding a user approval gate.
+- [x] Use writing-skills. Preserve the before-change fresh-reader reference trial and execute the original recipes with fake producer commands and log fixtures to record failures. No real media or OS recording.
+- [x] Put `set -euo pipefail` in the primary Unix pipeline's executing Bash scope, retaining all five commands and offsets. In the assembling subtitle example pass `--offsets-json segments/offsets.json` and stop if subtitle production fails. Explain current scene plus accepted manifest, relative WAV reference, and movie own-audio behavior in one paragraph.
+- [x] Put the producer-to-tee logging pipeline under the shell that owns pipefail, preserving the real producer exit status and printed STARTED/FINISHED/EXIT_STATUS markers. Execute the recipe with failing and passing fake producers; assert shell status and log content without matching a large rendered command.
+- [x] Restore the cursor transform on mouseup. Execute the documented JavaScript with a minimal fake DOM/event dispatcher and assert repeated press/release state changes; no browser or pixels.
+- [x] State that session directories, like take directories, must be new or empty on retries. Explain that close requests cleanup from serve, waits up to 30 seconds, and reports failure when the owner is unavailable or cleanup fails; hard-killing serve can leave its children and stale readiness, so these signals cannot establish a live owner. Document unsupported transcript verification and existing retry behavior accurately without adding a user approval gate.
+- [x] Document the new `--suite contracts` entrypoint as safe mocked/text checks, distinct from existing media/session suites and live acceptance.
 - [ ] Fresh-reader candidate trials use the same bounded scenarios as baseline, then execute supplied commands with fake boundaries. Preserve both failures and successes; do not call this full skill evaluation. Self-review and commit documentation plus concise results in this plan.
 
+Task 4 executable snippet result: the preserved baseline returned success after
+an assembly failure and a failed logged producer, emitted subtitles at zero
+instead of the measured two-second offset, and left the cursor pressed after
+mouseup. After the focused guide edits, the fake-boundary harness preserves
+assembly exit 41 and logger exit 23, stops later producers, retains prior
+outputs, runs all five stages on success, emits the subtitle interval at
+`00:00:02,000`, and restores the cursor on two releases. The safe `contracts`
+entrypoint passes 66 tests. Independent fresh-reader candidate trials remain
+pending after the documentation commit; these snippet checks are not full skill
+evaluation or live movie acceptance.
+
 ## Final verification and review
 
 Run the safe accumulated contract selection once after all code changes. Have an independent reviewer read the full accumulated PR from `fd02874aa5c55ba3c2bca431253b48e0e4c8be5a` through the final head, including docs/spec and tests, and resolve concrete remaining findings. Push only to Ada's `import/proving-it-works-skill` branch after passing review, verify #2214's remote head, and reply to the four current external threads with exact evidence. Do not merge. Drew's viewing remains final acceptance.

+ 7 - 0
skills/proving-it-works-with-a-movie/SKILL.md

@@ -40,6 +40,7 @@ five tools with `uv run --script`; Windows does not execute their Unix shebangs.
 The Unix sequence:
 
 ```bash
+set -euo pipefail
 # $SKILL_DIR is this skill's own directory - the "Base directory for this
 # skill" path printed when it loads. Installed as a plugin that is
 # $CLAUDE_PLUGIN_ROOT/skills/proving-it-works-with-a-movie
@@ -51,6 +52,12 @@ The Unix sequence:
 "$SKILL_DIR/scripts/check-movie"    movie.mp4      # nonzero exit: do not ship
 ```
 
+For each current narrated non-movie scene, downstream tools accept only a
+manifest entry whose text matches after collapsing whitespace while preserving
+case and punctuation, and whose WAV path is relative to the narration directory.
+`kind: movie` scenes retain their source audio and receive no narration offset,
+even if the scene contains a `narration` field.
+
 It samples picture and sound on one timeline and fails the movie when the
 action is crammed into the first seconds while narration keeps talking, when
 the picture never changes, when the audio is silent, or when a narrated

+ 3 - 1
skills/proving-it-works-with-a-movie/assembling.md

@@ -63,7 +63,9 @@ a PR comment, or a phone — and keep the `.srt` beside the movie as the
 sidecar the checker reads (and as the searchable transcript).
 
 ```bash
-scripts/make-subtitles narration/manifest.json movie.srt
+set -euo pipefail
+scripts/make-subtitles narration/manifest.json movie.srt \
+  --offsets-json segments/offsets.json
 scripts/burn-subtitles silent-cut.mp4 movie.srt movie.mp4
 ```
 

+ 11 - 2
skills/proving-it-works-with-a-movie/narrating.md

@@ -41,8 +41,11 @@ the first run needs network access to download dependencies and the ASR model.
 | `--verify auto` (CLI default) | Tries ASR for Piper and `openai-chat`; reports unavailable ASR but allows the clip. Skips ASR for `openai`. Detected drift still fails. |
 | `--verify off` | Skips ASR. |
 
-The `openai-chat` engine's returned transcript is also checked when rendering,
-regardless of the ASR mode. That transcript does not prove what the WAV contains.
+For `openai-chat`, word-comparison support is mandatory in every ASR mode,
+including cache reuse. During synthesis its returned transcript must contain
+speech and pass that comparison; an unsupported script also withdraws cached
+acceptance before reuse. `--verify off` bypasses only local ASR. The returned
+transcript still does not prove what the WAV contains.
 
 What it measures is **missing or invented content**, not exact words, and
 that distinction is load-bearing. A small ASR mangles unusual names — ours
@@ -61,6 +64,12 @@ Changing any of these re-renders the clip. Clips without recorded synthesis
 settings also re-render; an unchanged clip can be reused and still receives
 any requested ASR verification.
 
+A newly synthesized clip gets at most two attempts when synthesis or a
+supported transcript/ASR comparison fails. A cached clip is checked once; if
+rejected, its manifest acceptance is withdrawn and a later invocation can
+synthesize a replacement. Failed candidate files remain as evidence rather
+than being deleted to make a retry appear clean.
+
 ## The verbatim gate — required
 
 Never trust the generator's own account of what it produced. Verify the

+ 13 - 2
skills/proving-it-works-with-a-movie/recording-a-terminal.md

@@ -14,6 +14,11 @@ browser. Run `serve` in a background task your harness keeps alive, the way
 the visual companion server runs; a one-shot shell that kills its children
 on return ends the session.
 
+Use a new or empty `SESSION` directory for every session, including retries,
+just as every take directory must be new or empty. `serve` refuses a nonempty
+session directory before launching anything, preserving the prior session's
+logs and evidence.
+
 It needs uv, ttyd, and Chrome or Edge on PATH, or `--ttyd` and `--browser`.
 `--shell powershell51|powershell7|gitbash` picks the filmed shell; the shell
 you type these commands into is a separate choice. Replace the sample
@@ -126,8 +131,14 @@ before input is sent, preserving the earlier take.
 
 Long work spans takes exactly as on Unix: film the command being issued with
 a short `--seconds`, do other things, then `watch` the result as a new take.
-The shell, its variables and its cwd persist across calls until `close`,
-which kills ttyd, the browser and everything they started.
+The shell, its variables and its cwd persist across calls until `close`.
+`close` asks the running `serve` command to clean up its owned browser, ttyd,
+shell descendants, readiness file, and profile, then waits up to 30 seconds
+for confirmation. It returns failure when `serve` is unavailable, descendant
+cleanup cannot be confirmed, or profile cleanup fails; it never kills numeric
+PIDs copied from old session metadata. Hard-killing `serve` can leave children
+and stale readiness behind, so a surviving browser or `ready.json` cannot by
+itself establish that a live owner remains.
 
 The viewport is fixed at 1600×900 with a 17 px font. Look at
 `SESSION/ready.png` before filming; `serve` refuses a blank canvas, the same

+ 2 - 0
skills/proving-it-works-with-a-movie/recording-motion.md

@@ -40,6 +40,8 @@ document.addEventListener("mousemove", e => {
 }, true);
 document.addEventListener("mousedown",
   () => ring.style.transform = "translate(-50%,-50%) scale(.6)", true);
+document.addEventListener("mouseup",
+  () => ring.style.transform = "translate(-50%,-50%)", true);
 ```
 
 Type at human pace too (~55ms/char, longer after punctuation). Instant text

+ 9 - 7
skills/proving-it-works-with-a-movie/rendering-from-a-log.md

@@ -33,13 +33,15 @@ a passing run and pollutes the evidence.
 
 ```bash
 bash -o pipefail -c '
-  printf "RUN_KIND=<name>\n";
-  printf "STARTED_AT="; date -u +%Y-%m-%dT%H:%M:%SZ;
-  <the real command>;
-  rc=$?;
-  printf "FINISHED_AT="; date -u +%Y-%m-%dT%H:%M:%SZ;
-  printf "EXIT_STATUS=%s\n" "$rc"; exit "$rc"
-' 2>&1 | tee evidence/run.log
+  {
+    printf "RUN_KIND=<name>\n";
+    printf "STARTED_AT="; date -u +%Y-%m-%dT%H:%M:%SZ;
+    <the real command>;
+    rc=$?;
+    printf "FINISHED_AT="; date -u +%Y-%m-%dT%H:%M:%SZ;
+    printf "EXIT_STATUS=%s\n" "$rc"; exit "$rc"
+  } 2>&1 | tee evidence/run.log
+'
 ```
 
 Keep each producer plus its `tee` under one `pipefail` owner, or a failing

+ 5 - 0
tests/proving-it-works-with-a-movie/README.md

@@ -5,6 +5,7 @@ Run a suite from the repository root:
 ```sh
 uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite assembly
 uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite checker
+uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite contracts
 uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite narration
 uv run --script tests/proving-it-works-with-a-movie/run-tests.py --suite all
 ```
@@ -16,6 +17,10 @@ them; any skip then makes the run fail.
 The assembly sine wave is only a synthetic timing fixture. The narration drift
 inputs exercise text comparison only. Neither is speech/ASR acceptance.
 
+The `contracts` suite is the safe entrypoint for mocked process/media boundaries
+and text fixtures. It does not run the existing media/session suites, inspect a
+finished movie, or replace live acceptance and the required human viewing gate.
+
 The `terminal` suite starts a real ttyd session and skips where ttyd or a
 Chrome-family browser is missing. It films `bash` by default on Unix and
 `powershell51` on Windows; set `MOVIE_TEST_SHELL` to `powershell51`,