diff --git a/bin/fm-supervision-instructions.sh b/bin/fm-supervision-instructions.sh index a503bd9d35e..6130192ce53 100755 --- a/bin/fm-supervision-instructions.sh +++ b/bin/fm-supervision-instructions.sh @@ -158,6 +158,13 @@ repair_line() { esac } +drain_ack_stanza() { + # shellcheck disable=SC2016 # Backticked script and token names are literal instruction text. + printf '%s\n' '- Drain first with `bin/fm-wake-drain.sh`.' + # shellcheck disable=SC2016 # Backticked flag and token names are literal instruction text. + printf '%s\n' ' After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption.' +} + ordinary_wake_line() { case "$HARNESS" in claude) @@ -209,6 +216,7 @@ if [ "$X_MODE" -eq 1 ]; then else printf '%s\n' '- X mode: inactive; use the default watcher cadence.' fi +drain_ack_stanza ordinary_wake_line printf '\n' render_snippet diff --git a/docs/supervision-protocols/claude.md b/docs/supervision-protocols/claude.md index 1e5033a55ed..91c51c70777 100644 --- a/docs/supervision-protocols/claude.md +++ b/docs/supervision-protocols/claude.md @@ -1,26 +1,24 @@ Mode: Claude Stop-hook-owned supervision. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. Routine watcher arm and re-arm are owned by the Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`), never by you. +1. Routine watcher arm and re-arm are owned by the Stop `asyncRewake` hook (`bin/fm-claude-stop-autoarm.sh`), never by you. Every turn end while supervision is needed launches or attaches one home-scoped watcher cycle with no model command and no model tokens. An actionable close wakes you through the hook's exit-2 rewake, delivered as a `Stop hook feedback` message. -3. On a `Stop hook feedback` wake (`signal:`, `stale:`, `check:`, or `heartbeat`), run `bin/fm-wake-drain.sh` first and handle the wake. +2. On a `Stop hook feedback` wake (`signal:`, `stale:`, `check:`, or `heartbeat`), run `bin/fm-wake-drain.sh` first and handle the wake. Do not run `bin/fm-watch-arm.sh` after an ordinary wake; the next turn end re-arms automatically when supervision is still needed. Do not invent a wake from an attach-status line alone; drain and act only on real wake records, the drain's `OPEN DECISIONS` and `UNREAD STATUS` entries, or a real watcher reason line. -4. On the one `Stop hook feedback` automatic-mechanism failure notice (`firstmate watcher auto-arm FAILED ...`), drain, inspect the automatic mechanism failure, and do not turn the notice into a repeating manual-arm loop. -5. If the Stop hook does not claim the home or reports an exhausted failure, inspect its registration and watcher startup path before ending blind. +3. On the one `Stop hook feedback` automatic-mechanism failure notice (`firstmate watcher auto-arm FAILED ...`), drain, inspect the automatic mechanism failure, and do not turn the notice into a repeating manual-arm loop. +4. If the Stop hook does not claim the home or reports an exhausted failure, inspect its registration and watcher startup path before ending blind. Keep the Stop-owned automatic mechanism as the only Claude arm owner. -6. Treat `watcher: started ...` and `watcher: attached ...` inside automatic arm output as proof that one live cycle exists. +5. Treat `watcher: started ...` and `watcher: attached ...` inside automatic arm output as proof that one live cycle exists. On attach, the arm follows verified identity-matched successors instead of exiting when the first cycle ends. -7. The durable wake queue preserves actionable events between a rewake and the next Stop-launched arm, while the bounded turn-end guard prevents a blind Stop when recovery did not start. +6. The durable wake queue preserves actionable events between a rewake and the next Stop-launched arm, while the bounded turn-end guard prevents a blind Stop when recovery did not start. No PreToolUse hook denies fleet commands based on watcher status. [`watcher-continuity.md`](../watcher-continuity.md) owns the exact session-lock recovery boundary. -8. The turn-end guard (`bin/fm-turnend-guard.sh --claude`) remains the final backstop. +7. The turn-end guard (`bin/fm-turnend-guard.sh --claude`) remains the final backstop. It requires the PID-strict live-watcher and fresh-beacon predicate at the Stop boundary, while the mid-turn pull guard accepts a fresh beacon without a live process under Claude's between-turns auto-arm model. It allows the stop when a watcher is healthy or the role-verified auto-arm owns recovery, while fresh failure epochs advance the bounded one-time attended fail-open progression described in [`turnend-guard.md`](../turnend-guard.md). -9. Waiting on the hook-owned cycle is silent: do not send idle progress while the watcher is parked. +8. Waiting on the hook-owned cycle is silent: do not send idle progress while the watcher is parked. The watcher itself remains `bin/fm-watch.sh`, and `bin/fm-watch-arm.sh` remains the verified arm wrapper that the Stop hook foregrounds. Re-arm attaches to an existing healthy cycle when one is already present and follows its verified successor chain. diff --git a/docs/supervision-protocols/codex.md b/docs/supervision-protocols/codex.md index a7552d5391d..a9c8fbd1893 100644 --- a/docs/supervision-protocols/codex.md +++ b/docs/supervision-protocols/codex.md @@ -1,16 +1,14 @@ Mode: Codex foreground checkpoint. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. Source `__FM_X_MODE_ENV__` first when Relay is active. -3. First cycle: run one foreground watcher checkpoint with `bin/fm-watch-checkpoint.sh --seconds "${FM_CODEX_WATCH_CHECKPOINT:-180}"`. -4. Ordinary wake: if the command prints `signal:`, `stale:`, `check:`, or `heartbeat`, drain queued wakes, handle that wake, then start the next checkpoint. -5. If the command prints `checkpoint:` or exits 124 with no wake, drain queued wakes anyway, process any queued user message now visible to Codex, then start the next checkpoint. -6. Never use shell `&` or Codex background tasks for firstmate watcher supervision. -7. Do not run `bin/fm-watch-arm.sh` as Codex's normal supervision command. +1. Source `__FM_X_MODE_ENV__` first when Relay is active. +2. First cycle: run one foreground watcher checkpoint with `bin/fm-watch-checkpoint.sh --seconds "${FM_CODEX_WATCH_CHECKPOINT:-180}"`. +3. Ordinary wake: if the command prints `signal:`, `stale:`, `check:`, or `heartbeat`, drain queued wakes, handle that wake, then start the next checkpoint. +4. If the command prints `checkpoint:` or exits 124 with no wake, drain queued wakes anyway, process any queued user message now visible to Codex, then start the next checkpoint. +5. Never use shell `&` or Codex background tasks for firstmate watcher supervision. +6. Do not run `bin/fm-watch-arm.sh` as Codex's normal supervision command. If it is ever shelled anyway, a backgrounded, piped, or bundled anti-pattern is denied automatically by the PreToolUse seatbelt (`bin/fm-arm-pretool-check.sh`) registered in `.codex/hooks.json`. -8. Failure or missing cycle only: drain queued wakes, inspect the failure, then start a fresh foreground checkpoint. +7. Failure or missing cycle only: drain queued wakes, inspect the failure, then start a fresh foreground checkpoint. Codex cannot reason while a foreground tool call is running. The bounded checkpoint returns control regularly so user messages and queued wakes can be handled without relying on background-task wake semantics. diff --git a/docs/supervision-protocols/cursor.md b/docs/supervision-protocols/cursor.md index f0e496641c3..3817848431b 100644 --- a/docs/supervision-protocols/cursor.md +++ b/docs/supervision-protocols/cursor.md @@ -1,26 +1,24 @@ Mode: Cursor stop-hook-owned park. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. Routine watcher arm and re-arm are owned by the `stop` hook (`bin/fm-turnend-guard-cursor.sh`), never by you. +1. Routine watcher arm and re-arm are owned by the `stop` hook (`bin/fm-turnend-guard-cursor.sh`), never by you. Cursor runs that hook synchronously and awaits it, so every turn end while supervision is needed parks the turn boundary open on one home-scoped watcher cycle, with no model command and no model tokens spent while parked. -3. An actionable close wakes you as a follow-up turn carrying the `watcher` operational kind. +2. An actionable close wakes you as a follow-up turn carrying the `watcher` operational kind. On that wake, run `bin/fm-wake-drain.sh` first and handle it. Do not run `bin/fm-watch-arm.sh` after an ordinary wake; the next turn end parks again automatically when supervision is still needed. - Do not invent a wake from an attach-status line alone; drain and act only on real wake records, the drain's `OPEN DECISIONS` entries, or a real watcher reason line. -4. The captain keeps control while the hook is parked. + Do not invent a wake from an attach-status line alone; drain and act only on real wake records, the drain's `OPEN DECISIONS` and `UNREAD STATUS` entries, or a real watcher reason line. +3. The captain keeps control while the hook is parked. A message typed into a parked Cursor pane is accepted and runs its turn immediately, but the older park remains the recorded owner until that turn ends and the next `stop` hook claims the baton. An actionable watcher close in that window can still be delivered by the older park as one follow-up. This is bounded and safe: only one park exists in that window, so the event is a real wake rather than a stale duplicate of another park's wake, the durable wake queue makes handling idempotent, and the next `stop` claim makes an older park that is still running stand down without emitting. The private supersession records are `state/.cursor-park-owner` and its short publication and commit lock `state/.cursor-park-owner.lock`. -5. On a `turn-end-guard` follow-up, the park could not establish a live cycle. +4. On a `turn-end-guard` follow-up, the park could not establish a live cycle. Inspect the watcher startup path rather than turning the notice into a repeating manual-arm loop; the nag is bounded by `FM_CURSOR_TURNEND_BLOCK_BUDGET` (default 3) and then stops on its own. -6. Treat `watcher: started ...` and `watcher: attached ...` inside park output as proof that one live cycle exists. +5. Treat `watcher: started ...` and `watcher: attached ...` inside park output as proof that one live cycle exists. On attach, the arm follows verified identity-matched successors instead of exiting when the first cycle ends. -7. The durable wake queue preserves actionable events between a follow-up and the next park. +6. The durable wake queue preserves actionable events between a follow-up and the next park. [`watcher-continuity.md`](../watcher-continuity.md) owns the exact session-lock recovery boundary. -8. Waiting on the hook-owned park is silent: do not send idle progress while the watcher is parked. +7. Waiting on the hook-owned park is silent: do not send idle progress while the watcher is parked. The watcher itself remains `bin/fm-watch.sh`, and `bin/fm-watch-arm.sh` remains the verified arm wrapper that the `stop` hook runs as its own tracked child. Re-arm attaches to an existing healthy cycle when one is already present and follows its verified successor chain. diff --git a/docs/supervision-protocols/grok.md b/docs/supervision-protocols/grok.md index f27ae302e13..82e04d40b98 100644 --- a/docs/supervision-protocols/grok.md +++ b/docs/supervision-protocols/grok.md @@ -1,24 +1,22 @@ Mode: Grok background-notify supervision. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. Source `__FM_X_MODE_ENV__` first when Relay is active. -3. First cycle: arm with Grok's tracked background tool, as its own call: +1. Source `__FM_X_MODE_ENV__` first when Relay is active. +2. First cycle: arm with Grok's tracked background tool, as its own call: `run_terminal_command` with `background: true` on: `[ -f __FM_X_MODE_ENV_SH__ ] && . __FM_X_MODE_ENV_SH__; exec bin/fm-watch-arm.sh` -4. Trust only the arm's one-line status. -5. `watcher: started ...` or `watcher: attached ...` means a live cycle exists. +3. Trust only the arm's one-line status. +4. `watcher: started ...` or `watcher: attached ...` means a live cycle exists. On attach, the background task follows verified identity-matched successors instead of exiting when the first cycle ends. -6. Failure or missing cycle only: `watcher: FAILED ...` means supervision is down; fix and re-arm. -7. After a successful start or attach status, end the turn. +5. Failure or missing cycle only: `watcher: FAILED ...` means supervision is down; fix and re-arm. +6. After a successful start or attach status, end the turn. The background arm remains the live wait until it returns an actionable wake or failure. -8. Waiting is silent. -9. Never use shell `&` for firstmate supervision. -10. Never bundle the arm onto another command. - A shell `&`, a truncating pipe, or bundling is denied automatically by the PreToolUse seatbelt (`bin/fm-arm-pretool-check.sh`) whenever this project's Grok hooks are trusted. +7. Waiting is silent. +8. Never use shell `&` for firstmate supervision. +9. Never bundle the arm onto another command. + A shell `&`, a truncating pipe, or bundling is denied automatically by the PreToolUse seatbelt (`bin/fm-arm-pretool-check.sh`) whenever this project's Grok hooks are trusted. Grok injects a synthetic user message with `synthetic_reason: task_completed` when the background arm completes. When you see a background-task-completed system reminder for the arm: diff --git a/docs/supervision-protocols/opencode.md b/docs/supervision-protocols/opencode.md index 928daf96a70..91f3efbd4aa 100644 --- a/docs/supervision-protocols/opencode.md +++ b/docs/supervision-protocols/opencode.md @@ -1,17 +1,15 @@ Mode: OpenCode TUI plugin background wake. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. First cycle: let `.opencode/plugins/fm-primary-watch-arm.js` arm supervision after the OpenCode session goes idle. -3. The plugin listens for `session.idle`, spawns `bin/fm-watch-arm.sh --restart` without awaiting it in the idle handler, and owns every later successor launch. -4. After an actionable child close, the plugin rechecks session-lock ownership and verifies one singleton successor before it calls `client.session.promptAsync`; its bounded fallback is defined in `docs/watcher-continuity.md`. -5. Ordinary wake: do not ask the model to re-arm because continuity is plugin-owned. -6. An unexpected child close enters bounded exponential retry, and an exhausted retry or lost session lock is surfaced as a watcher failure instead of disappearing. -7. Failure or missing cycle only: if the plugin reports a watcher failure, drain queued wakes, inspect the failure text, and use `bin/fm-watch-arm.sh` manually only as a short recovery probe. -8. Never use shell `&` for watcher supervision. +1. First cycle: let `.opencode/plugins/fm-primary-watch-arm.js` arm supervision after the OpenCode session goes idle. +2. The plugin listens for `session.idle`, spawns `bin/fm-watch-arm.sh --restart` without awaiting it in the idle handler, and owns every later successor launch. +3. After an actionable child close, the plugin rechecks session-lock ownership and verifies one singleton successor before it calls `client.session.promptAsync`; its bounded fallback is defined in `docs/watcher-continuity.md`. +4. Ordinary wake: do not ask the model to re-arm because continuity is plugin-owned. +5. An unexpected child close enters bounded exponential retry, and an exhausted retry or lost session lock is surfaced as a watcher failure instead of disappearing. +6. Failure or missing cycle only: if the plugin reports a watcher failure, drain queued wakes, inspect the failure text, and use `bin/fm-watch-arm.sh` manually only as a short recovery probe. +7. Never use shell `&` for watcher supervision. The arm mechanism above is plugin-owned, not a model tool call, but a manual recovery probe that backgrounds, pipes, or bundles the arm is denied automatically by the PreToolUse seatbelt (`.opencode/plugins/fm-primary-pretool-check.js`, `bin/fm-arm-pretool-check.sh`). -9. Do not rely on this plugin in headless `opencode run`; firstmate primary supervision targets persistent OpenCode TUI sessions. +8. Do not rely on this plugin in headless `opencode run`; firstmate primary supervision targets persistent OpenCode TUI sessions. OpenCode's persistent TUI plugin runtime is the wake mechanism. The plugin applies in the main primary checkout and a secondmate's own home, and stays silent only in child crewmate and scout worktrees. diff --git a/docs/supervision-protocols/pi.md b/docs/supervision-protocols/pi.md index 5cdcaed7b08..2e1ccb73cfa 100644 --- a/docs/supervision-protocols/pi.md +++ b/docs/supervision-protocols/pi.md @@ -1,22 +1,20 @@ Mode: Pi extension background wake. When this session owns supervision and away mode is not active: -1. Drain first with `bin/fm-wake-drain.sh`. - After handling all emitted wakes and reconciling open decisions and unread status lines, run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`; until then the work remains durable for idempotent re-handling after interruption. -2. Confirm the Pi primary auto-loaded both project extensions (plain `pi` or `pi-signed`, after approving project trust once per clone); if not, restart the selected executable with `-e __FM_PI_TURNEND_EXT__ -e __FM_PI_EXT__` as a trust-free fallback. -3. First cycle only: make the one required `fm_watch_arm_pi` call. +1. Confirm the Pi primary auto-loaded both project extensions (plain `pi` or `pi-signed`, after approving project trust once per clone); if not, restart the selected executable with `-e __FM_PI_TURNEND_EXT__ -e __FM_PI_EXT__` as a trust-free fallback. +2. First cycle only: make the one required `fm_watch_arm_pi` call. Use `/fm-watch-arm-pi` only as a human-entered fallback. Never run `bin/fm-watch-arm.sh` through Pi's bash tool because that foreground arm can wedge the agent and bypasses extension-owned cleanup. -4. If the extension says no live session holds the lock, run `bin/fm-session-start.sh` to reclaim the session lock, then call `fm_watch_arm_pi` again. -5. The extension starts `bin/fm-watch-arm.sh --restart`, keeps the child attached to the live Pi process, and owns every later successor launch. -6. Ordinary same-process session replacement (`/new`, `/resume`, `/fork`, reload) retires only the prior generation; call `fm_watch_arm_pi` once for the first cycle of the replacement session without restarting Pi. +3. If the extension says no live session holds the lock, run `bin/fm-session-start.sh` to reclaim the session lock, then call `fm_watch_arm_pi` again. +4. The extension starts `bin/fm-watch-arm.sh --restart`, keeps the child attached to the live Pi process, and owns every later successor launch. +5. Ordinary same-process session replacement (`/new`, `/resume`, `/fork`, reload) retires only the prior generation; call `fm_watch_arm_pi` once for the first cycle of the replacement session without restarting Pi. The generation-owner contract lives in `.pi/extensions/fm-primary-pi-watch.ts`. -7. After an actionable child close, the extension rechecks session-lock ownership and verifies one successor before it delivers the follow-up wake; its bounded fallback is defined in `docs/watcher-continuity.md`. -8. Ordinary work, turn completion, and ordinary signal, stale, check, heartbeat, or other wake handling: do not call `fm_watch_arm_pi` again because continuity is extension-owned rather than model-memory-owned. -9. An unexpected child close enters bounded exponential retry, and an exhausted retry or lost session lock is surfaced as a watcher failure instead of disappearing. -10. Missing, failed, or unhealthy cycle only: if a later notification explicitly reports one of those repair conditions, drain queued wakes, inspect the failure text, call `fm_watch_arm_pi`, and restart the selected Pi-family executable with both extensions loaded if needed. +6. After an actionable child close, the extension rechecks session-lock ownership and verifies one successor before it delivers the follow-up wake; its bounded fallback is defined in `docs/watcher-continuity.md`. +7. Ordinary work, turn completion, and ordinary signal, stale, check, heartbeat, or other wake handling: do not call `fm_watch_arm_pi` again because continuity is extension-owned rather than model-memory-owned. +8. An unexpected child close enters bounded exponential retry, and an exhausted retry or lost session lock is surfaced as a watcher failure instead of disappearing. +9. Missing, failed, or unhealthy cycle only: if a later notification explicitly reports one of those repair conditions, drain queued wakes, inspect the failure text, call `fm_watch_arm_pi`, and restart the selected Pi-family executable with both extensions loaded if needed. A redundant call while the extension owns an arm child or scheduled retry is an ownership-based `watcher: unchanged` no-op, not an independent health claim. -11. Never use shell `&` for watcher supervision. +10. Never use shell `&` for watcher supervision. The arm mechanism above is extension-owned, not a model tool call, but a manual recovery probe that backgrounds, pipes, or bundles the arm is denied automatically by the PreToolUse seatbelt (`bin/fm-arm-pretool-check.sh`, wired into the turn-end guard extension at `__FM_PI_TURNEND_EXT__`). The turn-end guard extension lives at `__FM_PI_TURNEND_EXT__`. diff --git a/docs/supervision-protocols/unknown.md b/docs/supervision-protocols/unknown.md index 0615cf6a2f3..f6fa1d2d61e 100644 --- a/docs/supervision-protocols/unknown.md +++ b/docs/supervision-protocols/unknown.md @@ -2,9 +2,8 @@ Mode: Unknown harness fallback. This primary harness does not have a verified watcher wake adapter. Follow the generic supervision contract in `AGENTS.md`. -First cycle: drain queued wakes, then choose a supervision wait that the harness can actually wake from. -Ordinary wake: drain, handle all emitted wakes, reconcile open decisions and unread status lines, and run the exact `--ack-through` command printed as `WAKE_ACK_REQUIRED`, then repeat that verified wait while supervision is still required. -Before that acknowledgement, interruption leaves the work durable for idempotent re-handling. +First cycle: choose a supervision wait that the harness can actually wake from. +Ordinary wake: after handling the wake, repeat that verified wait while supervision is still required. Use `bin/fm-watch-arm.sh` only when the harness has a tracked background mechanism that survives the tool call and notifies the model on process exit. Use a bounded foreground wait over `bin/fm-watch.sh` when that wake mechanism is not verified. Never use shell `&` for watcher supervision. diff --git a/docs/turnend-guard.md b/docs/turnend-guard.md index de9b5ed922e..78439a337f4 100644 --- a/docs/turnend-guard.md +++ b/docs/turnend-guard.md @@ -159,6 +159,6 @@ It also covers true-reason banner wording and reason-keyed episode dedup survivi `tests/fm-cursor-primary.test.sh` covers the Cursor park end to end over real processes with no harness installed: each tracked Claude-shaped entrypoint standing down on a Cursor payload, both follow-up sources, the bounded repair nag and its reset, the nested loop bounds, supersession, away-mode and lock-ownership inertness, child-worktree exclusion, and that the adapter never exits 2. `FM_CURSOR_PRIMARY_LIVE_E2E=1 tests/fm-cursor-primary-live-e2e.test.sh` is the opt-in guard that proves the same behavior against the installed cursor-agent and fails naming the harness and version. `tests/fm-kimi-harness.test.sh` covers the separate Kimi crew hook's format preservation, idempotence, refusal cases, token guard, spawn registration, and teardown cleanup. -`tests/fm-supervision-instructions.test.sh` covers recovery-line ownership and pi-signed's identity-preserving reuse of Pi's protocol. +`tests/fm-supervision-instructions.test.sh` covers recovery-line ownership, pi-signed's identity-preserving reuse of Pi's protocol, and the neutral drain/ack stanza rendering exactly once for every harness including the unknown fallback. `FM_PI_LIVE_E2E=1 tests/fm-pi-primary-live-e2e.test.sh` is the opt-in isolated Pi path. [`verification/supervision.md`](verification/supervision.md#turn-end-guard) records the active cross-harness empirical evidence, including the 2026-07-24 Claude `asyncRewake` revalidation. diff --git a/tests/fm-supervision-instructions.test.sh b/tests/fm-supervision-instructions.test.sh index 377e95d152a..20cb20b0f16 100755 --- a/tests/fm-supervision-instructions.test.sh +++ b/tests/fm-supervision-instructions.test.sh @@ -176,8 +176,21 @@ test_pi_snippet_uses_effective_extension_path() { pass "pi supervision snippet renders the effective extension path" } +test_drain_ack_rendered_exactly_once() { + local harness out count + for harness in claude codex opencode pi pi-signed grok cursor not-real; do + out=$("$RENDER" --harness "$harness") + count=$(printf '%s\n' "$out" | grep -F -o 'WAKE_ACK_REQUIRED' | grep -c .) + if [ "$count" -ne 1 ]; then + fail "harness $harness rendered WAKE_ACK_REQUIRED $count times, expected exactly 1" + fi + done + pass "renderer emits the neutral drain/ack contract exactly once for every harness including unknown" +} + test_selected_harness_block_only test_unknown_fallback +test_drain_ack_rendered_exactly_once test_conditional_stanzas test_repair_lines test_cross_harness_ordinary_continuation_and_repair_matrix