# Feature: Cross-round review delta (Phase 4 Steps 2.1, 2.2, 3.8)

**Pattern**: reviewers re-read the whole diff every round with no memory of the round before, so they rediscover last round's findings in new words and nothing can tell "still broken" from "new". A finding therefore needs an identity that survives the fix: `finding-fingerprint.mjs` computes `F:xxxxxxxx` from the file and either the cited `ruleId` or the normalised issue text (lowercase, quotes stripped, paths reduced to basenames, digit runs collapsed). The line, the severity, the fix text and the reviewer take no part, because all of them change between rounds without the finding changing. `review-delta.mjs` then compares round N with round N-1 and reports `stillPresent`, `resolved`, `downgraded` (re-reported but no longer accepted by triage) and `new`, plus `recurrence`: how many consecutive rework cycles each survivor has lasted. That count is the autopilot circuit-breaker's trigger 2.

Gated by `prefs.global.autopilotCircuitBreaker` (`enabled` default true, `identicalFindingCycles` default 2). Computed by scripts, never by the model: an older triage JSON gets its fingerprints on the fly, and a reviewer that echoes one is respected but not relied on.

## Files per round

Phase 4 Step 3.2.1 writes `$WORKTREE/.pipeline/triage-round-<N>.json` (N = `state.reviewIterations | length`), validates it, annotates it and copies it to `$WORKTREE/triage-output.json`, the name Phase 7, `worktree-finalize.sh`, `render-work-summary.sh` and `diff-explain.mjs` read. A copy rather than a symlink because the salvage is `cp -R`, and `.pipeline/` is on the salvage list, so every round survives into `artifactsPath`. Step 3.7 rewrites the round file; the copy is repeated after it.

## Step 2.1 block: previous-round findings (iteration >= 2)

```bash
ITERATION=$(jq '.reviewIterations | length' "$STATE_FILE")
PREV_ROUND="$WORKTREE/.pipeline/triage-round-$((ITERATION-1)).json"
PREV_BLOCK=""
[ "$ITERATION" -ge 2 ] && [ -f "$PREV_ROUND" ] && PREV_BLOCK=$(node $HOME/.claude/scripts/finding-fingerprint.mjs annotate "$PREV_ROUND" 2>/dev/null \
  | jq -r '[.accepted[] | select(.severity=="blocking" or .severity=="important")] | sort_by(.severity != "blocking") | .[:40][] | "- \(.fingerprint) [\(.severity)] \(.file): \(.issue)"')
```

Rendered at the end of the shared prefix (Step 1.9), identical for every reviewer and for the triage call of that iteration:

```
<previous-round-findings>
Each entry below was accepted last round and sent for rework.
- If the issue is still present, report it again with the SAME fingerprint value and the current line.
- If it is fixed, omit it. Omission is how you report resolution; never emit a "resolved" finding.
- Any finding not listed here is new: leave fingerprint unset.
{PREV_BLOCK}
</previous-round-findings>
```

The cap of 40 entries drops `important` before `blocking` so the prefix stays bounded. The block never enters the repo-stable prefix `prompt-assembly.md` describes: the diff it follows is already per-run. After each reviewer's validator gate, `finding-fingerprint.mjs annotate --in-place` fills in any fingerprint the reviewer left unset; `anonymize-findings.mjs` strips only identity keys, so the fingerprint reaches triage, and the triage prompt tells the model to preserve it verbatim.

## Step 2.2 block: scope self-check (every iteration)

Phase 3 Step 3.7 wrote `$WORKTREE/.pipeline/scope-check.json` (contract: `features/scope-check.md`). Render it so reviewers judge the diff against the dev's stated scope and do not re-propose what was rejected:

```bash
SCOPE_JSON="$WORKTREE/.pipeline/scope-check.json"
SCOPE_GATE=$(git -C "$WORKTREE" diff --name-only "origin/$BASE_BRANCH"...HEAD \
  | node $HOME/.claude/scripts/scope-check-gate.mjs --check "$SCOPE_JSON" --diff-files - --advisory 2>/dev/null)
```

```
<scope-self-check>
Files and the reason the dev gave for touching each:
{jq -r '.files[] | "- \(.path): \(.reason)"' "$SCOPE_JSON"}
Files in the diff with no stated reason (flag as scope drift if the change is not obviously required):
{jq -r '.unjustified[]' <<< "$SCOPE_GATE"}
Deliberately not done (do not raise these as findings; they are known):
{jq -r '.notDone[] | "- \(.what) (\(.why))"' "$SCOPE_JSON"}
</scope-self-check>
```

A missing record renders the block with `no scope-check.json written` and a `review.scope_check=missing` metric; the review proceeds, and the absence is itself information for the reviewer.

## Step 3.8: delta + trigger 2

```bash
TRIP=$(jq -r '.global.autopilotCircuitBreaker.identicalFindingCycles // 2' "$PREFS_FILE")
DELTA_JSON=$(node $HOME/.claude/scripts/review-delta.mjs --rounds-dir "$WORKTREE/.pipeline" --iteration "$ITERATION" --trip-cycles "$TRIP"); DELTA_RC=$?
jq -c --argjson d "$DELTA_JSON" --argjson i "$((ITERATION-1))" \
  '{reviewIterations: (.reviewIterations | .[$i] += {delta: ($d + {computedAt: (now | todate)})})}' "$STATE_FILE" \
  | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE"
$HOME/.claude/scripts/log-metric.sh "$TASK_ID" 4 review.delta iteration=$ITERATION \
  new=$(jq '.counts.new // 0' <<< "$DELTA_JSON") still_present=$(jq '.counts.stillPresent // 0' <<< "$DELTA_JSON") \
  resolved=$(jq '.counts.resolved // 0' <<< "$DELTA_JSON") plateau=$(jq '.plateau // false' <<< "$DELTA_JSON") tripped=$([ "$DELTA_RC" -eq 3 ] && echo true || echo false)
```

| Exit | Meaning | Action |
|---|---|---|
| 0 | Progress, or nothing to compare (iteration 1, missing previous round) | Continue to Step 4. |
| 3 | A blocking/important finding survived `identicalFindingCycles` consecutive rework cycles | **Autopilot** with `enabled`: trip the breaker. `state.circuitBreaker = {tripped: true, trigger: 2, detail: "finding <fingerprint> (<file>: <issue>) survived <n> consecutive rework cycles", checkpoint: {phase: 4, step: "3.8", iteration: N}, trippedAt, counters: {identicalFindingCycles: <max recurrence>, reworkCycles: N-1}}`, then the halt-visibility protocol from `phases/operations.md` (`status=paused`, `haltReason="4:circuit-breaker:identical-finding"`, tracker meta, the `>&2 HALT` line, the usage report). **Interactive modes**: do not trip; show `delta.stillPresent` and ask (picker-contract) `Continue rework` / `Escalate to me` / `Accept as deferred`, the last moving those findings to `deferred[]` with reason `circuit-breaker: accepted by user after <n> cycles`. |
| 1 | Unreadable round file | Log `review.delta_skipped reason=invalid` and continue; the delta is advisory and never blocks on its own failure. |

`plateau` (the still-present set unchanged from the previous delta) is logged, not acted on: at the default threshold it coincides with trigger 2 and with the Phase 3 `retryCount` hard-kill. The rework-storm cap itself is trigger 3, recorded by the Phase 3 re-entry. The Phase 3 reflection prompt names each accepted finding by `fingerprint` and quotes `delta.stillPresent` first, marked `STILL PRESENT after round N-1's fix`.

## Why fingerprints are computed, not stored

Same argument `shortId()` in `_retrieval.mjs` makes for corpus rows: derived from the finding's own content, so existing triage files get ids without a migration and a re-annotated file keeps the ids it had. The known trade-off is over-merging: two findings in one file that differ only by a number share a fingerprint. The delta output prints `file: issue` beside every id and the trip needs two consecutive recurrences, so a merge is visible and cannot halt a run by itself. Under-merging (a reviewer that rewrites rather than echoes) only ever suppresses a trip; it never causes one.

## Telemetry

`review.delta` once per iteration >= 2: `iteration`, `new`, `still_present`, `resolved`, `plateau`, `tripped`. `run-metrics.mjs` reports `reviewDelta.stillPresentFinal`, `resolvedTotal` and `tripped`; Phase 7 renders the three counts per round.

## Reference

Scripts: `$HOME/.claude/scripts/_fingerprint.mjs`, `finding-fingerprint.mjs`, `review-delta.mjs`. Schemas: `reviewer-output` 1.2.0, `triage-output` 3.4.0, `dev-critic-output` (optional `fingerprint`), `agent-state` (`reviewIterations[].delta`, `circuitBreaker`). Breaker: `features/autopilot-circuit-breaker.md`. Smokes: `smoke-review-delta.sh`, `smoke-autopilot-circuit-breaker.sh`.
