# Proof audit pass 1: ACP terminal exit publication

PR [#28078](https://git.cafe/pingdotgg/t3code/pull/28078), branch `fix/issue-28067`.
Reviewed head [390fce27e6](https://git.cafe/pingdotgg/t3code/commit/390fce27e6fde8663da301e6b708b06eb44824df), before/base [a24ec905c8](https://git.cafe/pingdotgg/t3code/commit/a24ec905c8157f1ef6114639ff614617d98f10e9).
This is a specification for future captures, not captured evidence. All required rows are **MISSING**.

## Findings first

1. **Evidence gate remains open.** The PR description has no `## Evidence` section, uploaded driver, or raw before/after transcripts. Its table names older before/after revisions, not the two revisions above. A prose timing table and a green test suite do not establish the seven separate runtime claims. Require reproducible artifacts on these exact revisions, or explicitly explain any different baseline.
2. **The new regression test is narrower than the description.** It races `waitForExit` against a real background child's delayed stdout and checks that the real leader stopped. Its `Stream.tap` observes later stream consumption; it never calls public `output`, `release`, or `disposeAll`, never fills the live-terminal limit, and never exercises output arriving within the 250 ms drain. Thus it is useful regression coverage, but cannot substitute for R02, R07–R11 below.
3. **Cleanup fixtures must escape the leader's process group.** The installed real Node spawner kills a POSIX process group during scope close. With an ordinary `&` child, release/dispose can themselves end the child's pipes and make the BEFORE run complete. R08/R09 instead use a genuinely detached child. The proof must distinguish completion before child cleanup from completion caused by child cleanup.
4. **Do not claim a universal 250 ms response bound.** The 250 ms starts when `child.exitCode` resolves, not at `create`, and bounds waiting for both pumps. Command runtime and process-group termination during kill/release/disposal are additional. R01 deliberately includes the reporter's one-second foreground delay; R07 distinguishes a drain window from an unconditional fixed delay or immediate publication.
5. **Platform limitation:** the added test uses POSIX `$T3_TEST_NODE`, `&`, and `$!` without a Windows guard. This is visible in source and agrees with an existing review comment; native Windows was not executed here. The manual Windows workflow includes this package. Linux runtime proof must not be described as Windows/macOS verification.

## Code-derived path inventory

Changed production code is only the exit watcher in [clientTerminals.ts](https://git.cafe/pingdotgg/t3code/blob/390fce27e6fde8663da301e6b708b06eb44824df/packages/provider-acp/src/server/clientTerminals.ts#L291): it observes a successful numeric exit or a signal/error, joins BOTH detached output-pump fibers, and now times out that join after 250 ms before setting `record.exitStatus` and satisfying `record.exit`.

| Consumer / entry path | Data and conditional branches relevant to this diff | Fixture coverage |
|---|---|---|
| `makeAcpClientTerminals.create` / command builder | `shellCommands:true` plus absent/empty args enables shell; nonempty args or false/absent shell option uses executable/args. `cwd`, base/session/request env remain unchanged. Parent can stay alive, exit 0, exit nonzero, or die by signal. | R01–R06 shell/direct branches, R10 alive/exit transition, R11 signal, R12 numeric/empty controls. |
| stdout and stderr pumps | Neither stream ends; only stdout ends; only stderr ends; both end before deadline; both remain open beyond deadline; data after parent exit both before and after deadline. Individual stream errors are ignored as before. | R01/R02 both held, R04/R05 one held, R06/R12 no inherited pipes, R07 before-deadline delivery, R02 after-deadline writes. |
| `terminal/output` -> `output` | `output` string, `truncated` boolean, `exitStatus` property ABSENT before publication, present afterward. stdout/stderr are merged into one buffer without a guaranteed ordering across streams. | R01–R07, R10–R12. R02 proves truncated tail after publication. |
| `terminal/wait_for_exit` -> `waitForExit` | Deferred pending versus answered; numeric exit versus signal; repeat wait is immediately answered after publication. | R01–R07, R10–R12. |
| `terminal/kill` -> `kill` | Kills real leader/process group; an escaped descendant can still hold pipes. Kill completion alone does not prove exit Deferred completion. | R11; ordinary active-terminal cleanup also in R09/R10. |
| `terminal/release` -> `release` | Marks released, closes process scope, then awaits SAME exit Deferred. Public handle rejected afterward; retained snapshot remains until pruning. | R08. |
| session-scope finalizer -> `disposeAll` | Parallel cleanup of all records, including active, exited-but-pipes-held, and already released records. Empty collection is unchanged. | R09 includes mixed states and calls disposeAll again. |
| create capacity check | `!released && exitStatus===undefined` counts as live; 16 live cap. Unreleased cap 64 is separate and unchanged. | R10, using 15 live roots plus one controlled root, never releasing that target before the replacement attempt. |
| `readOutputSnapshot` / `resolveEmbeddedTerminalContent` | Snapshots contain output/truncation/status and survive release. `tool_call`/`tool_call_update` terminal entries become snapshot text; unrelated content and unknown-ID fallback remain unchanged. Mapping does NOT attach exitStatus or proactively push late output. | R08 calls both notification forms using the real retained snapshot; no UI refresh claim. |
| ACP adapter construction / registry | Only production constructor caller is shared ACP adapter; registry enables client terminals for Devin with `shellCommands:true`. Other ACP flavours lack these client terminals. Runtime handlers directly forward create/output/wait/kill/release; create also has existing policy guard. | All ordinary rows call the exact real service with Devin's shell option. Not a live Devin session or an ACP wire capture. |

Call-site anchors: [adapter construction and finalizer](https://git.cafe/pingdotgg/t3code/blob/390fce27e6fde8663da301e6b708b06eb44824df/packages/provider-acp/src/server/adapter.ts#L1532), [handler wiring](https://git.cafe/pingdotgg/t3code/blob/390fce27e6fde8663da301e6b708b06eb44824df/packages/provider-acp/src/server/adapter.ts#L5984), [snapshot-to-tool-content consumer](https://git.cafe/pingdotgg/t3code/blob/390fce27e6fde8663da301e6b708b06eb44824df/packages/provider-acp/src/server/adapter.ts#L5903), [Devin registry configuration](https://git.cafe/pingdotgg/t3code/blob/390fce27e6fde8663da301e6b708b06eb44824df/packages/provider-acp-registry/src/server/adapter.ts#L296).

Unchanged branches do not warrant multiplying captures: cross-session/unknown/released-ID guards, create spawn error, environment merge, per-terminal default 4 MiB/max 8 MiB limits, global 16 MiB trimming, unreleased cap 64, released-snapshot cap 32, command-line lookup, nonterminal notification passthrough, and unknown embedded-terminal fallback. Existing tests cover many of these. R02 uses a small explicit byte limit to prove that output arriving after the new deadline still takes the existing bounded-buffer path. This pass does not claim a new full memory/resource stress audit.

There is no changed component render function, prop, layout, route, setting, command-palette action, keybinding, theme, viewport, or client-side wire schema. Web/local-hosted web, desktop, mobile, local/relay/tunnel all use this environment-owned server path; this does not establish live turns on every client/provider or recovery of already wedged Devin sessions.

## Capture type, seeding, and minimum set

Every row below is **recording: timestamped runtime transcript**, because method completion, event order, delayed output, and child survival are time-dependent. A terminal screenshot, still, or final JSON snapshot alone is insufficient. A screen video is unnecessary; the raw chronological transcript is the recording. Viewport = N/A and theme = N/A for all rows because there is no UI change. Minimum set is BEFORE × AFTER × each required row/subvariant, on Linux. There is no viewport/theme cross product.

Seeding for R01–R06/R08–R12: an isolated scratch driver imports the real `makeAcpClientTerminals` from the selected checkout, provides the real `NodeServices.layer`, and creates real subprocesses. Use `{defaultCwd: rowDir, shellCommands:true}` unless a row states otherwise; use sessionId `proof-28078`. Do not replace the terminal service with a mock. This is a labelled **real-service harness transcript**, not live Devin or full ACP end-to-end proof. No T3 database or app server is necessary. R07 additionally uses a clearly labelled delivery gate around a real spawner's two streams; it is a **controlled stream-delivery harness**, not a claim that the OS naturally reproduced that scheduling window. No Devin binary is available per the brief; no stub ACP agent or UI capture is required to prove this changed service boundary.

Use one identical uploaded driver and helper sources on both revisions. Each run gets a new timestamped directory below `/tmp/lead-28078-r1/`; never overwrite old captures. Run from the owning package or resolve Effect/NodeServices from that package, not from `/tmp`. Full source/fixture helpers and invocation must be uploaded with raw output. Instrumentation may observe real handles/exit events and stream chunks; it must not substitute an exit, close a pipe early, or modify production code.

## Exact command/request vocabulary

All aliases here are literal fixture definitions, not permission to choose a different workload. The capturer should materialize their final executable sources, upload them, and print the expanded request JSON. `N` is the absolute `process.execPath`, `B` is the absolute uploaded `background.mjs` helper, `P` the uploaded `parent.mjs` helper, and `D` the unique row directory. Put `PROOF_NODE=N`, `PROOF_BG=B`, `PROOF_ROW_DIR=D` in request.env, in addition to the helper-specific variables below. `req(t)` means `{sessionId:'proof-28078',terminalId:t.terminalId}`. Each fresh service uses the unmodified live-terminal limit **16**.

The background helper B is a single Node process with no children. It writes `D/background-ready.json` containing its real PID before idling with `setInterval`. On `SIGUSR1`, it writes the configured `PROOF_STDOUT_TEXT` to stdout and `PROOF_STDERR_TEXT` to stderr, independently records write callback results/error events (including EPIPE) to `D/background-writes.json`, and remains alive. No text is emitted to either pipe until the signal. Readiness and write acknowledgements are explicit file/IPC events, not a guessed sleep. Empty configured text means do not write that stream. The observer must then sample PUBLIC terminal output to prove buffering; a tap seeing a chunk or a write ACK alone is insufficient.

The parent helper P uses `spawn(N,[B],{detached:true,stdio:['ignore',OUT,ERR],env:process.env})`, calls `child.unref()`, records that spawned child's PID, waits for B's explicit readiness, prints `BG_PID=<pid>\n`, then either exits 0 (`PROOF_PARENT_MODE=exit`) or prints `ROOT_READY\n` and remains alive (`PROOF_PARENT_MODE=hold`). OUT/ERR are `inherit` or `ignore`, as specified by `PROOF_STDIO`. In hold mode it watches an initially absent file `D/exit-go`; creation of that file makes it exit 0. File watchers must check once after registration to avoid a missed event. An observer of the real spawner records each root PID and real exit independently of the terminal Deferred.

For the literal reporter command S-REPORT, additionally provide `BASH_ENV` pointing at this uploaded startup hook. This leaves the request command bytes unchanged and observes the real `sleep` spawn without finding PIDs by process-name matching. Disclose the startup instrumentation; it adds a waiting Bash parent so both descendant PIDs and the sleep's actual reap can be recorded. The outer `/bin/sh` one-second sleep does not read BASH_ENV.

```bash
printf '{"pid":%s}\n' "$BASHPID" > "$PROOF_ROW_DIR/reporter-bash-ready.json"
sleep() {
  command sleep "$@" &
  local proof_sleep_pid=$!
  printf '{"pid":%s}\n' "$proof_sleep_pid" > "$PROOF_ROW_DIR/reporter-sleep-ready.json"
  wait "$proof_sleep_pid"
  local proof_wait_status=$?
  printf '{"pid":%s,"waitStatus":%s}\n' "$proof_sleep_pid" "$proof_wait_status" > "$PROOF_ROW_DIR/reporter-sleep-reaped.json"
  return "$proof_wait_status"
}
```

| Alias | Exact `create` command and arguments / extra data |
|---|---|
| S-REPORT | `command: 'sh -c "setsid bash -c \'sleep 3600\' & sleep 1; echo done"'`; args absent; request.env includes the BASH_ENV hook above. This is the literal command in the report, with disclosed shell-startup PID/reap instrumentation. No B helper is used. |
| S-ESC | `command: 'setsid bash -c \'exec "$PROOF_NODE" "$PROOF_BG"\' & bg=$!; printf "BG_PID=%s\\n" "$bg"; sleep 1; echo done'`; args absent. Both pipes inherited; B reports ready. This preserves the reporter's shell/setsid/background/sleep/done structure with disclosed PID/helper instrumentation. Do not label it byte-for-byte the reporter's literal `sleep 3600` command. |
| S-PLAIN | `command: '"$PROOF_NODE" "$PROOF_BG" & bg=$!; printf "BG_PID=%s\\n" "$bg"; echo done'`; args absent. No setsid: child remains in shell process group. |
| X-BOTH | `command:N, args:[P]`, `PROOF_STDIO=both`, `PROOF_PARENT_MODE=exit`. P sets OUT=inherit, ERR=inherit. Nonempty args bypass shell even with shellCommands:true. |
| X-OUT | Same as X-BOTH with `PROOF_STDIO=stdout`: OUT=inherit, ERR=ignore. |
| X-ERR | Same as X-BOTH with `PROOF_STDIO=stderr`: OUT=ignore, ERR=inherit. |
| S-REDIR | Same exact command as S-ESC except `>/dev/null 2>&1` immediately before `&`. B still reports ready out of band. |
| X-HOLD | X-BOTH with `PROOF_PARENT_MODE=hold`; do not create exit-go until the row tells you to. |
| LIVE | `command:N, args:['-e', 'console.log("ROOT_READY",process.pid); setInterval(() => {},1000)']`. No descendant. |
| FINITE | `command:N, args:['-e', 'process.stdout.write("OUT\\n"); process.stderr.write("ERR\\n"); process.exitCode=3']`. No descendant. |
| EMPTY | `command:N, args:['-e','']`. No descendant or output. |
| DRAIN | `command:N, args:['-e', 'process.stdout.write("DRAIN_OUT\\n"); process.stderr.write("DRAIN_ERR\\n")']`. R07 gates delivery of the real chunks to the real terminal pumps as specified below. |

## Required fixture matrix

`deadline` below is a capture observation at 10 s after real leader exit, not a timeout added to the product. Before/after columns name their full SHAs. Every row has live limit 16; only R10 fills it. All expected results are hypotheses to verify, not results obtained in this pass.

| Row / named fixture | Affected? / capture status | Exact terminal method sequence and data | BEFORE: a24ec905c8157f1ef6114639ff614617d98f10e9 | AFTER: 390fce27e6fde8663da301e6b708b06eb44824df | Required transcript proof and PID cleanup |
|---|---|---|---|---|---|
| R01 Literal reporter command, escaped child, both pipes inherited | Yes; **MISSING**, recording, Linux | `create(S-REPORT)`; observe real leader exit; `output(req)`; start `waitForExit(req)`; record result or deadline; `output(req)`; repeat wait on AFTER; cleanup captured sleep PID and reap its Bash parent; `release(req)`; `disposeAll`. | `done\n` buffered; no exitStatus; wait still pending at deadline while descendant Bash/sleep are alive. It answers exit 0 only after descendant cleanup closes pipes. | Wait answers `{exitCode:0,signal:null}` around 250 ms after root exit; output includes done and same exitStatus. End-to-end duration includes the foreground sleep 1. Repeat wait answers without another drain. Descendants are still alive BEFORE cleanup. | Expanded exact request and disclosed BASH_ENV hook; root/Bash/sleep captured PIDs; root exit time/status and isRunning=false; wait start/end/deadline; full output JSON with `hasOwnProperty('exitStatus')`; descendant non-zombie liveness before cleanup. TERM the captured sleep PID, await hook's reap record, reap captured Bash PID through subreaper; escalate only those captured PIDs if needed. Print release/dispose completion separately. |
| R02 Plain background child, late stdout+stderr, bounded retained tail | Yes for publication; buffer behavior preserved; **MISSING**, recording | `create(S-PLAIN,outputByteLimit:32)` with stdout text `A`.repeat(80)+`LATE_OUT\n`, stderr text `B`.repeat(80)+`LATE_ERR\n`; observe root exit; start wait; snapshot at result/deadline; only THEN signal B `SIGUSR1`; await both write ACKs and observed delivered chunks; `output(req)`; cleanup B; `release`; `disposeAll`. | Wait pending at deadline; output has no exitStatus. Later writes are still readable, truncated=true, at most 32 UTF-8 bytes; no EPIPE. After cleanup wait can finish. | Wait completes before SIGUSR1 is sent; B alive at that moment. Later writes successfully reach PUBLIC output after the deadline; output remains at most 32 bytes and truncated=true; exitStatus remains exit 0. | Print emit time after wait/deadline observation, stream-tagged chunks/ACKs, output before and after emit, encoded byte length, truncated and exitStatus. Cross-stream order is nondeterministic: assert buffer equals last 32 bytes of actual observed append order, not a fixed stdout/stderr ordering. Prove a late marker survives, both streams were consumed, and both write callbacks succeeded. Kill/reap B before release (plain process-group cleanup could otherwise kill it). |
| R03 Executable/args, escaped child, both pipes held | Yes; **MISSING**, recording; TWO subvariants | (a) fresh service `{shellCommands:true}`; `create(X-BOTH)`; (b) fresh service `{shellCommands:false}`; same request. For each observe root exit; wait/result-or-deadline; output; cleanup B; release; disposeAll. | In BOTH subvariants wait pending at deadline, output lacks exitStatus although root exited 0 and B lives. | In BOTH subvariants wait answers exit 0 around root exit+250 ms and output has matching status while B lives. | Print option value and nonempty args so shell bypass is proven, real exit time, public wait/output JSON, background liveness, exact PID cleanup and reaping for each subvariant. This validates direct-command capability without implying an additional production provider uses it. |
| R04 Only stdout held, stderr EOF | Yes; **MISSING**, recording | `create(X-OUT)`; observe root exit and stderr EOF; wait/result-or-deadline; output; SIGUSR1 B with stdout=`LATE_STDOUT\n`, stderr empty; await ACK/delivery; output; cleanup B; release; disposeAll. | Wait pending; no exitStatus until stdout finally closes during cleanup. | Wait answers exit 0 despite held stdout; late stdout reaches buffer afterward; no EPIPE. | Log which pipe reached EOF, both method snapshots, timings from root exit, live B at answer, stdout marker/ACK, and exact B PID kill/reap. |
| R05 Only stderr held, stdout EOF | Yes; **MISSING**, recording | `create(X-ERR)`; observe root exit and stdout EOF; wait/result-or-deadline; output; SIGUSR1 B with stdout empty, stderr=`LATE_STDERR\n`; await ACK/delivery; output; cleanup B; release; disposeAll. | Wait pending; no exitStatus until stderr closes during cleanup. | Wait answers exit 0 despite held stderr; late stderr reaches buffer afterward; no EPIPE. | Symmetric to R04 but must show stdout EOF and actual stderr marker. R04 alone cannot establish BOTH joins are bounded. Kill/reap exact B PID. |
| R06 Redirected child, both terminal pipes end | No behavioral change expected; essential control; **MISSING**, recording | `create(S-REDIR)`; wait; output; B liveness; cleanup B; release; disposeAll. | Wait returns exit 0 promptly after root exit, while B remains alive; output done/status present. | Same; no unconditional extra 250 ms if both streams already drained. | Full request includes redirection; root-exit-to-answer timings on both revisions; done text/status; no child output in terminal; B non-zombie liveness; exact kill/reap. Compare these timings to R01 without asserting a hardware-specific millisecond cutoff. |
| R07 Real command output delivered 100 ms after exit, within drain | Drain preservation; **MISSING**, recording; CONTROLLED DELIVERY HARNESS | Real `create(DRAIN)` with a decorator around the REAL spawner: hold stdout/stderr chunks before they reach terminal pumps. Observe actual leader exit; log PUBLIC output with status absent; open both gates at real exit+100 ms using a driver timer; let both finite streams finish; `waitForExit`; output; release; disposeAll. Real process handle/status unmodified. | Wait holds until both gates/streams finish; captures both DRAIN markers and exit 0. | Same; wait MUST NOT resolve before both gated chunks are admitted, and must resolve once drained without waiting out a fixed 250 ms. Public output at wait completion contains both markers. | Log real root PID/exit, gate close/open/last-delivery/EOF times, actual delay less than 250 ms, any pre-gate answer, public output at answer. If a busy VM opens gates after 250 ms, mark run invalid and capture a NEW run, not success. This distinguishes zero-drain publication from the intended grace window. No orphan exists; confirm root reaped by spawner. Never present as natural OS scheduling or a live Devin capture. |
| R08 Release BEFORE status publication; retained content after release | Yes; **MISSING**, recording | `create(X-BOTH)`; on real root exit (do NOT await waitForExit), immediately sample output with exitStatus ABSENT, then `release(req)` in an observed fiber; record result/deadline before child cleanup. AFTER completion read `readOutputSnapshot(owner,id)`; public `output(req)` must reject released handle; invoke `resolveEmbeddedTerminalContent` on separate tool_call and tool_call_update notifications containing that terminal ID; cleanup B; join any pending release; disposeAll. | Release stays pending through deadline while escaped B holds pipes; may mark handle released already. Retained snapshot exists but status absent until B cleanup ends pipes. | Release returns `{}` without cleanup of escaped B, and retained snapshot contains exit 0. Public output rejects handle; both notification forms resolve to retained text. | Record pre-release status absence and release start/finish or pending BEFORE cleanup, root exited and B still alive; snapshot output/status; released-handle error; normalized embedded content for BOTH forms. For BEFORE, print same snapshot/error/mapping at deadline, then cleanup and record late completion. Never count completion after killing B as a successful bounded release. Exact B PID kill/reap. |
| R09 disposeAll on mixed active/exited/released records | Yes; **MISSING**, recording | Fresh service: create one FINITE, wait and release it; create TWO X-HOLD targets with distinct directories/PIDs and one LIVE; await all readiness events. Create BOTH exit-go files, observe target roots exit and immediately sample output with exitStatus ABSENT; invoke `disposeAll` directly without waiting on target Deferreds; record result/deadline before killing their B children. Read snapshots, verify all public handles rejected; cleanup exact B PIDs; join cleanup; call disposeAll AGAIN. | First disposeAll remains pending on escaped children at deadline even if active root was killed; already released record causes no new unknown-ID error. Completes only after escaped B cleanup. | First disposeAll completes after normal active-root termination and bounded drain, while BOTH escaped B processes remain alive. All terminal snapshots have exitStatus and handles are released. Second disposeAll completes. | List all terminal/root/B PIDs, per-root exit and per-child liveness, pre-dispose status absence, dispose start/end/deadline, per-terminal snapshots and released errors; active root stopped, escaped children alive prior to explicit cleanup. Print actual disposal duration separately from 250 ms drain (process termination can take longer). Cleanup/reap each captured B PID; root processes reaped by spawner. |
| R10 Live limit 16, slot recovered on exit without release | Yes; **MISSING**, recording | Fresh service limit16: create FIFTEEN LIVE roots plus ONE X-HOLD target (16 running). Attempt create(LIVE): expect concurrent-limit error. Create target's exit-go; observe its root exit; retain its handle, do NOT release it. At real exit+1 s call output and attempt replacement create(LIVE). Record target wait result/deadline independently. Cleanup all by captured PID/terminal methods, then disposeAll. Keep total unreleased count below64. | First and second replacement attempts reject limit16; target still has no exitStatus despite root exit and live escaped B. Target wait pending. | First replacement attempt rejects limit16; after target root exit+drain, second replacement SUCCEEDS. Target output has exit0 and wait answers while B lives; target was never released. | Print 16 ready root IDs/PIDs, first failure text `limit of 16 concurrent terminals`, target root exit time, second attempt time/result/new terminalId, target output/status, unreleased count and explicit `targetReleased=false`. Prove slot recovery is due to publication, not releasing/killing the target's B. Kill/reap B by PID; kill other captured roots via terminal methods and wait/release them, record dispose completion. |
| R11 Explicit kill, signal status, escaped pipe holder survives | Yes; **MISSING**, recording | `create(X-HOLD)`; await root and B readiness; record running output without exitStatus; start wait in an observed fiber; `kill(req)`; observe real root exit; record kill result separately; wait/result-or-deadline; output; prove B alive; cleanup B; release; disposeAll. Default forceKillAfter5s remains unchanged. | Kill may return after leader termination but wait remains pending on escaped B; public output lacks exitStatus at deadline. | Wait answers `{exitCode:null,signal:'SIGTERM'}` on Linux after real root exit+bounded drain; output contains SAME status despite live escaped B. | Print real root exit failure/signal, kill start/end, wait start/end/deadline and exact response, public output before/after, B non-zombie alive before cleanup. Different termination semantics on Windows must be separately labelled, not forced into POSIX expectation. Kill/reap B by exact PID. |
| R12 Finite nonzero and empty-success controls | No change expected; numeric/error and empty branches; **MISSING**, recording; TWO subvariants | Fresh terminals per subvariant: (a) create(FINITE), wait, output, release; (b) create(EMPTY), wait, output, release; disposeAll. | FINITE: both OUT/ERR lines, truncated=false, exit3/signal null. EMPTY: output empty, truncated=false, exit0/signal null. Both finish once their pipes drain. | Same. No new fixed 250 ms penalty when pipes finish sooner. | Expanded command/args, root PID/exit, root-exit-to-wait duration, exact public output JSON/status and byte length. Do not assert stdout/stderr relative ordering. Confirm root exited and was reaped by spawner; no background PID exists. |

## Transcript protocol and cleanup requirements

For each subvariant print a metadata header with full git SHA, checkout path, clean/dirty status, OS/kernel/arch, Node version, Effect/NodeServices package versions, driver SHA256, fixture ID, options (shellCommands, forceKillAfter), complete nonsecret request, sessionId, terminalId, outputByteLimit, and constants (live16/unreleased64). All times should be monotonic elapsed milliseconds relative to create plus an absolute UTC run-start. Runtime transcripts may measure elapsed time; these are not repository tests.

Print one chronological JSON line per event: create_start/result, root_spawn(pid), background_spawn(pid), ready, root_exit(status), method_start/result/deadline, stream_data(stream,text), stream_eof, emit_signal, write_ack/error, output_snapshot (including an explicit status-property-present flag), alive(pid,state), cleanup_signal(pid,signal), reaped(pid,status), row_summary. Human-friendly summary may accompany raw logs, never replace them. Print both real root-exit and method timing so sleep/startup/kill cost is not attributed to the drain.

For BEFORE hangs, keep a pending method observed and print `pending:true` at deadline BEFORE cleanup. `release`/`disposeAll` are uninterruptible; wrapping them in Effect.timeout alone can itself hang. Use observed detached calls and an external driver watchdog/cleanup path. First record the deadline, then terminate only captured background PIDs to close their pipes, then join pending calls and close scopes. Log those late completions as `completed_after_cleanup:true`, never as proof of the fix. A hard emergency timeout must preserve logs and clean up registered PIDs before ending the driver. No unbounded finalizer may be the only cleanup plan.

PID cleanup must be affirmative. Obtain every child PID from spawn results, `$!`, or the child's own ready record, cross-check identity, and print `kill(pid,0)` plus a Linux process state other than Z before claiming alive. Kill only those captured PIDs, never by name/path or process-pattern search. Send TERM to B, escalate KILL only if needed, and print the actual result. To actually reap escaped orphan fixtures on Linux, run the capture driver under a small uploaded subreaper wrapper (`prctl(PR_SET_CHILD_SUBREAPER,1)` before starting Node); it receives the captured PID ledger and `waitpid`s those specific adopted children, printing the resulting PID/status. Root handles are reaped by the real Node spawner. If such a reaper is unavailable, report that limitation and do not call a zombie or a kill request a successful reap. Neither cleanup nor reaping may precede the observations proving background-child survival. Never signal an unrelated PID or the user's dev server.

The gate/ready/write events must not be fabricated by mutating the real service. For ordinary rows, public output must be sampled after actual pump delivery, not merely after the writer reports success; use an observing stream decorator/Deferred marking consumption and a subsequent driver turn. If buffered ordering is relevant to R02, record the byte sequence actually admitted by both pumps. Do not infer that a write callback proves the size-limited buffer contains its text.

The evidence section eventually needs this shape (links filled only with actual uploaded artifacts):

```markdown
## Evidence

Driver: [source and helpers](UPLOAD_URL), SHA256 <driver hash>; invocation <exact command>.
Real makeAcpClientTerminals with NodeServices, shellCommands:true; not a live Devin turn.

| Fixture | Before | After |
|---|---|---|
| Revision | [a24ec905c8](https://git.cafe/pingdotgg/t3code/commit/a24ec905c8157f1ef6114639ff614617d98f10e9) | [390fce27e6](https://git.cafe/pingdotgg/t3code/commit/390fce27e6fde8663da301e6b708b06eb44824df) |
| R01 ... | [raw transcript](BEFORE_URL) | [raw transcript](AFTER_URL) |
```

Keep the raw logs, helper sources, and exact invocation behind the uploaded links. Label R07 as controlled stream delivery and disclose R01's BASH_ENV startup instrumentation. S-ESC/S-REDIR use instrumented reporter-shaped helper commands and must not be described as identical command bytes or identical environment. No evidence in this matrix is a claim to have run the captures.

## Pass-1 coverage and friction

Covered: complete two-file diff; all changed terminal-service consumers; relevant shared ACP registration/registry and embedded-output paths; new and adjacent tests; real installed Effect timeout/join and Node spawner cleanup behavior; current PR description, issue, reviews/comments and head-bound checks. No tests or captures run, no production edits, no server/browser started, no PR writes. Current CI reported 40 successful checks, which does not close the capture gate. The local `main` ref differs from the supplied observed base; review consistently used the supplied base, confirmed as an ancestor of head, without changing refs.

FRICTION: none

VERDICT: INSUFFICIENT R01, R02, R03a, R03b, R04, R05, R06, R07, R08, R09, R10, R11, R12a, R12b missing before/after runtime transcripts and uploaded reproducible driver
