# SPEC-P4 — Autonomy: deterministic doneness (LIVE engine)

audience: AI coding agents first.
Target: `modules/harness/v2`. DO NOT touch `modules/harness/src` (dead WIP). Depends on SPEC-P1 + SPEC-P2 landed. Closes SEAM-AUDIT `P4` (major) and `P5` (major).

Principle: **code validates agent output**. Doneness MUST derive from executed evidence, never from agent prose.

Order: N1 verify contract → N2 digest completeness → N3 reply contract → N4 classification → N5 review inputs. N4 depends on N1.

## Measured evidence this spec answers

Supervised run `pi-envelope-fixture`, 2026-08-04, engine `0.1.108`:

- Task `t1` committed `6ba6c2f4` with `=== codex.sh exit=0 ===`, work correct in substance.
- Engine verdict: `task t1 blocked: dispatch-invalid-reply (coder reply missing, malformed, or child exit nonzero)`. Correct committed work discarded.
- Same agent reported `Acceptance passed`. Acceptance was RED — `na: rate-limit` failed `actual '' expected '2026-08-04T12:00:00Z'`.

Both facts hold at once: engine over-trusts prose (accepted a false pass claim) and over-punishes prose (discarded good work over a missing marker). One fix resolves both — execute acceptance, then stop treating the marker as the only signal.

## N1 — Executable verify contract

Plan task schema gains `verify`: array of shell command strings, each run in the task worktree.

- `plan.js normalizeTask` MUST require non-empty `verify` for every code-producing task (any task with `files_modify`, or any task reaching a dispatch seat). Reject at load with the task id in the message. Prose `acceptance` stays required and stays advisory.
- New `modules/harness/v2/verify.js` exports `runVerify({ task, worktree, budgetMs, logPath })`. Executes each `verify` entry through `child.js runChild`, cwd = task worktree, in order, stopping at first nonzero exit.
- `run.js runTrustedTask` MUST call `runVerify` after the claim-escape check and BEFORE `runQualityPhase`.
- Nonzero exit → `lastFailure = { failureClass: 'acceptance-failed', cause: '<command> exited <code>' }`, break. Journal kind `verify.failed` carrying command, exit code, and captured tail.
- All green → journal kind `verify.passed` carrying every command and its exit code.

```json
{"type":"task","id":"t1","desc":"…","files_modify":["modules/harness/v2/test/wrapper-terminal-contract.test.js"],
 "verify":["node --test modules/harness/v2/test/wrapper-terminal-contract.test.js"],
 "acceptance":["the Pi stub emits session.started and agent.finished lines"]}
```

DO NOT satisfy this by widening gate0. `gates.sh gate0` is repo-wide and yields `no check commands found` in this repo — it cannot express per-task doneness.

Migration, same task: every existing `docs/plans/*.jsonl` code-producing task MUST gain a `verify` entry, or it stops loading. Derive each from the task's existing `acceptance` prose. No plan left unmigrated.

## N2 — Digest completeness

`plan.js taskDigest` currently hashes `id`, normalized `desc`, sorted deps, seat, tier. It MUST also hash `verify`, `acceptance`, `files_modify`, and risk.

Rationale: `doneness.js completionForTask` matches a commit's `Task-Digest` trailer to decide `done`. A task whose verify command changed but whose digest did not is replayed as already-done against the old contract.

Expected consequence, not a regression: every pre-P4 `Task-Digest` trailer stops matching, so every previously-completed task reads `pending`. A resume test asserting replay-as-done against a pre-P4 trailer MUST be updated, never worked around.

## N3 — Phase-aware prompt stating the reply contract

`seats.js buildPrompt(task)` is phase-blind: `seatDispatch` builds one prompt from `task.desc` and hands the identical text to the coder, the reviewer, and the fixer. It MUST take the phase and emit the contract that phase's parser enforces.

Coder: MUST state the required terminal reply shape. Today it does not, while `quality.js validateDispatchReply` hard-requires a `REPLY_MARKERS` key — the engine punishes an agent for omitting something it was never asked for.

Reviewer: MUST state the exact verdict schema `quality.js normalizeReviewVerdict` enforces — object with exactly `verdict` (`"PASS"` or `"FAIL"`) and `findings` (array of exactly `severity`, `path`, `message`). Today the reviewer is asked for none of this and `parseReviewVerdict` throws `review verdict must be valid JSON` on whatever prose comes back.

Delete the comment claiming `desc` is the whole specification and that the wrapper contract carries everything else. The run above disproves it.

Prompt MUST end with the marker contract verbatim, naming `committed` for a coder seat:

```
End your reply with exactly one JSON object on its own line: {"committed": true}
```

Keep `REPLY_MARKERS` as the single source of truth in `quality.js:9`; `verdict-status.py:15` MUST stay byte-identical.

## N4 — Non-destructive classification (depends on N1)

`run.js dispatchInvalidReply` MUST stop being terminal on its own.

Decision ladder, stop at first rung that holds:

1. Child exit nonzero, or killed by budget → terminal, unchanged.
2. Verify red → `acceptance-failed`, terminal (N1).
3. Verify green, tree has changes, reply marker absent or unparsable → journal kind `reply.unmarked`, PROCEED to `runQualityPhase`. NEVER block.
4. Verify green, reply present → proceed, unchanged.

Scope: the trusted path (`run.js runTrustedTask`) only. `gate.mode: "stub"` dispatches no agent and is untouched.

Rung 3 is safe only because rung 2 executed real commands AND `quality.js reviewRequired` returns `true` unconditionally — independent review runs for every task and still gates the commit. DO NOT lean on gate0 here: `gates.sh gate0` reports `no check commands found` in this repo, so after N1 the deterministic bar is `verify` plus independent review, nothing else. DO NOT implement rung 3 without N1 landed — without executed verify it is a pure loosening and lets red work integrate.

## N5 — Review reads engine evidence, never the coder's claim

The reviewer prompt MUST carry the engine's own `verify.passed` / `verify.failed` journal record. It MUST NOT carry the coder's self-reported acceptance prose.

Rationale: the measured run's coder claimed a pass on a red tree. A reviewer fed that claim inherits the lie.

## Exit criteria

- A plan task lacking `verify` is rejected at load, with a test asserting the message names the task id.
- A coder that commits correct work and emits NO reply marker completes the task (rung 3), proven by integration test.
- A coder that commits work failing its `verify` command is blocked `acceptance-failed` and creates no commit, no checkpoint, no integration intent — proven by integration test.
- Changing a task's `verify` changes its `taskDigest`; a resume test proves the task re-runs rather than replaying as done.
- `verify.passed` / `verify.failed` appear in `/plans` for a real run.
- A reviewer dispatch returns a verdict `parseReviewVerdict` accepts, proven by a test asserting the reviewer prompt carries the `{verdict, findings}` schema.
- Engine bundle bumped, released, and the `VERSION` change landed — engine code on main is NOT armed until `modules/harness/bin/harness-release.sh bump && release`.
