# Review Loop

> Dispatches a multi-agent review team over this session's work, runs an execution-grounded lint/test/build check, falsifies each finding against the diff, fixes load-bearing issues, and re-reviews until clean or the budget hits. Always records a commit-pinned verdict on the branch's open PR, so the review leaves a trail. Invoked manually or by the Stop hook after real work outside plan mode. `--mode claim` reviews an analytical conclusion against its primary sources instead of a diff; `--mode deliverable` reviews finished reader-facing work (a report, a content page, a decision briefing) for whether the human it is handed to can actually use it.

- Skill: `bryceewatson/review-loop` (Agent Skill, multi-file: 22 files)
- Install (CLI): `npx skillmds@latest add bryceewatson/review-loop`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bryceewatson/review-loop/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: BryceEWatson (https://skillmd.com/u/bryceewatson)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bryceewatson/review-loop

---


# /review-loop

You are running the auto-review-loop skill. The Stop hook installed at
`~/.claude/skills/review-loop/stop-hook.cjs` invoked you after Claude
finished a session with real code changes outside plan mode. (Or the user
invoked you manually.)

## Argument parsing

Parse from the slash-command arguments:

- `--session-id <id>` — required; uniquely identifies this session
- `--iteration <n>` — 0-indexed iteration count; defaults to 0
- `--max-iter <n>` — defaults to 3 (per `THRESHOLDS.reviewLoop.maxIterations`)
- `--cost-ceiling-tokens <n>` — combined input+output token budget; default 300000
- `--scope <git-diff|head-1>` — default `git-diff` (uncommitted changes)
- `--auto` — set when hook-invoked; suppresses preamble, writes structured exit
- `--independence <independent|self-authored|unverified>` — set by the Stop hook from the
  measured authorship check. Absent on a manual invocation, where Step 0 measures it itself.
- `--self-review-ack "<reason>"` — the documented escape hatch. Runs the review in the
  authoring session anyway, and permanently records WHY on the verdict. See Step 0.
- `--mode <code|plan|claim|deliverable>` — default `code`. Controls which reviewer
  lens set fires in Step 4. The Stop hook auto-selects in this precedence order:
  1. any changed file matching the plan-artifact globs (`**/*-plan.md`,
     `**/*-proposal.md`, `**/*-spec.md`, `**/*-retrospective.md`,
     `**/*-research/**/*.md`, or a project override at
     `.claude/review-loop.plan-paths`) → `--mode plan`;
  2. else any reviewable source file → `--mode code`;
  3. else any changed file matching the deliverable globs (`.md`/`.mdx` under
     `reports/`, `research/`, `content/` or `src/content/`, plus `*-report.md`,
     `*-brief.md`, `*-summary.md` at any depth, or a project override at
     `.claude/review-loop.deliverable-paths`) → `--mode deliverable`.

  Code outranks deliverable on purpose: the deliverable lens set does not review
  source, so letting a stray content file demote a code diff would silently drop
  the code review. The honest cost is that a diff mixing code and a deliverable
  gets code lenses only — invoke `--mode deliverable` by hand for those.

  `--mode claim` reviews an **analytical conclusion / research finding** (not a
  diff): the "artifact" is a set of load-bearing claims plus the primary sources
  they cite, and reviewers are sent back to those sources to try to break the
  conclusion. Manual-invocation only (the hook never auto-selects it); pass the
  claims under review — and where their evidence lives — as the slash-command
  arguments.

  `--mode deliverable` reviews **finished reader-facing work**: a report, a
  published content or project page, a briefing, a PR/issue body, a draft
  message, a generated artifact, a CLI's output. The hook auto-selects it for a
  deliverable-only diff, and it is also the right manual mode for a deliverable
  that never lands in a file — a decision presented in chat, a rendered page —
  in which case pass the artifact (or a path/URL to it) as the arguments.

  Manual invocation may override the mode.

Treat missing args defensively: any arg can be omitted, all have defaults.

## Workflow

### Step 0 — Independence gate (BEFORE any review work)

A session cannot independently review work it wrote: it carries the authoring context, and
therefore the same blind spots that produced the defect. This step runs **first**, so that
nobody has spent reviewer effort by the time it stops them.

Run the guard — this is a command, not a judgement call:

```bash
node ~/.claude/skills/review-loop/authorship-guard.mjs --session-id <session-id> --repo "$(git rev-parse --show-toplevel)" --json
```

It reports which files in the diff **this session (or a subagent it dispatched) edited**, and
exits `0` independent · `3` self-authored · `4` unverified.

Act on the exit code:

- **`0` independent** → proceed to Step 1 normally. Nothing else changes.
- **`3` self-authored, invoked manually** → **STOP. Do no review work.** Print the guard's
  message verbatim: it names the fix (dispatch a fresh session, `gh pr checkout <n>`, review
  there), not just the fault. Do not post a verdict. Do not fix anything. Exit.
- **`3` self-authored, with `--self-review-ack "<reason>"`** → proceed, but the run is
  permanently marked: every verdict it produces carries the SELF-REVIEW banner below and
  **withholds the `review-clean` token**, so the auto-merge gate keeps holding and a human
  decides. The reason is quoted verbatim on the PR.
- **`3` self-authored, hook-invoked (`--auto`)** → the Stop event fires *in* the authoring
  session, so this is the normal case there. Do **not** suppress the review: its findings still
  have value and an unattended run has nobody to dispatch a replacement. Run it, fix
  load-bearing findings as usual, and mark the verdict SELF-REVIEW as above.
- **`4` unverified** → proceed, but the verdict says `independence: unverified` and names why.
  Could-not-determine is never upgraded to independent.

If `--independence` was already passed by the hook, trust it and skip re-running the guard.

**One review per session.** Before starting, check `.local-state/<session-id>.json` for a prior
completed run. A second `/review-loop` in a session that already ran one shares the first
review's context — the same contamination one step later. Warn plainly, name the earlier run,
and prefer a fresh session.

### Step 1 — State load

State file: `~/.claude/skills/review-loop/.local-state/<session-id>.json`.

Shape:

```json
{
  "session_id": "...",
  "started_at": "<unix-ms>",
  "iteration": 0,
  "max_iterations": 3,
  "cost_spent_input_tokens": 0,
  "cost_spent_output_tokens": 0,
  "cost_ceiling_input_tokens": 300000,
  "last_diff_sha": null,
  "findings_history": [],
  "completion": null,
  "scope": { "branch": "...", "files": [] }
}
```

Read the file if it exists; otherwise initialize.

### Step 2 — Concurrent-run locks

Two locks needed:

- **Per-session lock** `.local-state/<session-id>.lock` — use `fs.openSync(path, 'wx')` (O_EXCL).
- **Per-repo lock** `.local-state/repo-<sha1(toplevel)>.lock` — same pattern. Hash of `git rev-parse --show-toplevel`.

Lock files store `<pid>` + `<unix-ts>`. If either fails to acquire:
- Read the existing lock's PID; check liveness via `process.kill(pid, 0)`.
- If lock's mtime > 30 min ago AND PID is dead → remove stale lock, retry once.
- Otherwise → log `skip: concurrent-loop-in-repo` to `.local-state/<session-id>.log` and exit cleanly.

### Step 3 — Gather scope

**Claim mode:** skip the git commands below — there is no diff. Scope = the
verbatim claims under review (from the invocation args, or a named artifact
file) + the primary sources they cite. Use the claims' combined text as the
`diff_sha` equivalent for the stall check and as the reference text for the
Step 6 drift-guard. Skip the "Leave a trail on the PR" section unless the
analysis lives in a file tied to an open PR.

**Deliverable mode:** the artifact is the **whole finished deliverable**, not its
diff — a reader meets the entire page, not the lines that changed. Run the git
commands to identify which deliverable files changed, then read each one in full
and pass that full text to the reviewer. **Read `.claude/review-loop.deliverable-paths`
first if it exists: it REPLACES the default globs listed above, so in an
override project the built-in list is wrong by construction and matching
against it would find nothing.** If the matched set comes out empty, say so
explicitly — an empty artifact set is a routing fault to report, never a
`review-clean`. If the deliverable was passed inline
(no file — a decision presented in chat, a page reachable only by URL), skip the
git commands as claim mode does and use the artifact text as the `diff_sha`
equivalent and the Step 6 drift-guard reference. When the deliverable renders
(an HTML page, a site route, a generated image, a CLI's output), open or run it
and pass what you actually saw alongside the source.

Run via Bash:
- `git status --porcelain` — list of changed files
- `git diff HEAD` — full diff text
- `git rev-parse --abbrev-ref HEAD` — current branch
- SHA the diff via Bash `git diff HEAD | sha256sum` (or compute in Node).

If `state.last_diff_sha === current_diff_sha` AND `state.iteration > 0`:
- Stall: same diff after attempted fixes means we're not making progress. Write `completion: 'stalled'` to state; emit `<promise>review-stalled</promise>` + unresolved checklist; exit.

### Step 4 — Dispatch reviewers (parallel)

The reviewer lens set depends on `--mode`:

#### Mode `code` (default)

Read the 6 agent files:
- `agents/simplicity.md`
- `agents/design-coherence.md`
- `agents/adversarial.md`
- `agents/ship-readiness.md`
- `agents/statistical-rigor.md` (skip if project has `.claude/review-loop.disabled-roles` listing `statistical-rigor`)
- `agents/execution-grounded.md` (special — see below)

#### Mode `plan`

The code-tuned lenses misfire on planning documents (`ship-readiness` has nothing to say about a markdown spec; `execution-grounded` has no `pnpm lint` to run on a `.md` file; `simplicity` flags prose redundancy as if it were duplicated code). Use this lens set instead:

- `agents/vision-coherence.md` — does the plan realize each non-negotiable / falsifier / stated goal of the source-of-truth doc the plan derives from (VISION.md, CLAUDE.md, charter, RFC)?
- `agents/operator-empathy.md` — will the human who acts on this plan actually be able to act on it under realistic constraints (touch-time, mobile parity, anxiety surfaces, decision fatigue)?
- `agents/architecture.md` — does the plan respect declared layer separations, introduce silent state, bloat scope beyond a referenced scope-table, or imply Tier-1 file edits without surfacing them?
- `agents/completeness.md` — cross-reference the plan against every spec section it claims to cover; flag missing surfaces, untestable done-criteria, dangling Tier-1 asks.

Do NOT load `execution-grounded.md` in plan mode — there's nothing to execute.
Do NOT load `ship-readiness.md`, `simplicity.md`, or `statistical-rigor.md` in plan mode — they assume code-shape artifacts and produce category-error findings on prose.

#### Mode `claim`

The artifact is **not a diff** — it is one or more load-bearing analytical
claims (a research finding, a comparison verdict, a recommendation) plus the
primary sources they rest on. The failure modes are overstatement, confirmation
bias, missed disconfirming evidence, and method holes — not bugs. Use this lens
set:

- `agents/claim-falsification.md` — go to the cited primary sources and try to
  DISPROVE each load-bearing claim; hunt specifically for disconfirming evidence
  the analysis omitted.
- `agents/claim-method.md` — is the method sound enough to support the claim
  (do comparable data actually exist; is the sample non-degenerate; is
  attribution by an authoritative field, not inference; is an under-powered null
  being sold as a finding)?
- `agents/claim-calibration.md` — do stated confidence and the headline match
  the evidence? Flag overstatement, proxy-as-fact, absence-of-evidence-vs-
  evidence-of-absence, and caveat-then-ignore.
- `agents/claim-coverage.md` — did the analysis examine the RIGHT evidence?
  Selection/sampling bias, missed sources, over-aggressive filtering.

Do NOT load `execution-grounded.md` — there is nothing to run.
Do NOT load any code/plan lens — they category-error on an analytical claim.

#### Mode `deliverable`

The artifact is **finished reader-facing work**. Every other mode checks an
artifact against its specification; this one checks it against the person who has
to read it. Correct, complete work still fails if the reader can't find the
answer, can't act without opening three other things, or has to rebuild a
structure the author already had. Use this lens set:

- `agents/operator-empathy.md` — **Scope B**. Assign the deliverable a genre
  (`report` / `reference` / `decision-ask` / `narrative` / `machine-output`),
  then run its D1–D11 checklist: answer-first, inventories rendered as prose
  instead of tables, lists carrying reasoning, heading shape vs. genre, a
  decision without its evidence inline, a menu instead of a recommendation,
  terms used before they're introduced, code detail in the reading path, density,
  rigor apparatus in the reading path, edit-meta.

One lens, deliberately. `completeness` and the `claim-*` lenses would each have
something to say about a report, but they answer "is it right / is it whole",
which is already covered elsewhere and is not the gap this mode exists to close.
Do NOT load `execution-grounded.md` (nothing to run), `ship-readiness.md`,
`simplicity.md`, or `statistical-rigor.md` (code-shape lenses on prose).

Read its `[]` for exactly what it buys, and no more: the checklist is embedded
in the lens, so an empty result means the checks were present and found nothing.
It is **not** corroborated by a second lens, and the lens's false-negative rate
is unmeasured — an over-tightened check can return `[]` on a deliverable that
genuinely fails, which happened during this mode's own calibration. The one
failure it does rule out is a declared project standard silently failing to
load, which must surface as a `checklist-unavailable` finding instead.

#### Dispatch (all modes)

For each enabled agent file, dispatch a `Task` (subagent_type: `general-purpose`) with:
- The agent file's full body as the system instruction
- The diff + file list + branch name (plan mode: include the full content of changed plan files, not just the diff — review needs the surrounding doc context; **claim mode: include the verbatim claims under review + pointers to the primary sources/transcripts/data they cite, and instruct each reviewer to read those sources directly — its job is to break the claim against ground truth, not critique prose**; **deliverable mode: include the FULL text of each changed deliverable (never only the diff), the rendered form where one exists, and the contents of `.claude/review-loop.deliverable-standard` if present — a one-line pointer to the project's own standard — plus the document it points at, so the lens can load it as additional checks**)
- For iteration ≥ 2: the prior iteration's findings injected as a **Reflexion-style verbal reflection** (prepend: *"In the prior iteration you flagged: [list]. The developer applied fixes. Your new findings should reflect what's now true rather than re-litigate prior decisions."*)
- An instruction to return JSON findings only, no preamble
- **Plan mode only — hard PLAN-VS-CODE DISCIPLINE block**: "This document is a PLAN, not implementation. Do NOT emit findings of the form 'code at X does not implement Y' against a forward-looking spec. Valid findings: internal contradiction in the plan, missing required section, untestable done-criterion, constraint violation, Tier-1 ask missing for an implied file edit. Cap at 5 findings; quality over quantity."
- **Claim mode only — hard CLAIM DISCIPLINE block**: "You are reviewing an analytical CONCLUSION, not code. Ground every finding in the cited primary source (read it; do not trust the analysis's own summary of it). Valid findings: a load-bearing claim contradicted or unsupported by the source, disconfirming evidence the analysis omitted, a method flaw that makes the claim unsupportable, overstatement vs. stated confidence. A failed falsification (you tried and could NOT break a claim) is a valid, useful result — report it. Cap at 6 findings; quality over quantity."
- **Deliverable mode only — hard DELIVERABLE DISCIPLINE block**: "You are reviewing FINISHED work for whether its reader can use it — not whether it is correct, complete, or well-written. Assign the genre first; the checks are genre-dependent and mis-assigning it is how this lens produces noise. Every finding must carry a COUNT you measured (inventories, tables, list items, words per heading, sentence lengths) and must name the thing and the fix — which inventory becomes which table with which columns, which two sentences the answer moves into. 'Consider adding structure' is not a finding. Do not flag correctness, completeness, prose style, or word choice. A clean deliverable returns `[]`. Cap at 5 findings. The single exception to the count rule is a `checklist-unavailable` finding, which reports that a declared project standard could not be read and needs no measurement."

All LLM reviewers dispatch in parallel (one message, multiple `Task` calls). Code mode: 5 lenses. Plan mode: 4 lenses. Claim mode: 4 lenses. Deliverable mode: 1 lens.

The **execution-grounded** reviewer (code mode only): not a Task dispatch. Invoke Bash directly to run the project's `pnpm lint`, `pnpm test`, `pnpm build` (or `npm run lint/test/build` if no pnpm-lock.yaml exists). Capture exit codes and stderr. Each non-zero exit → one finding with severity `high`, confidence `100`, `load_bearing: true`, category `execution-failure`, claim describing the failure. Timeout: 180s.

Accumulate token usage from each Task's response into `state.cost_spent_*_tokens`.

### Step 5 — Falsifier stage (NEW per industry research)

For each LLM-reviewer finding (NOT execution-grounded), dispatch a short-context falsifier `Task`:

> "A reviewer flagged this issue: [finding.claim] at [finding.file]:[finding.line] (category: [finding.category], severity: [finding.severity]). The actual diff at that location is: [diff snippet ±10 lines around the cited line]. Try to disprove the claim. If the finding refers to nonexistent code, misreads the change, attacks a strawman, or relies on hallucinated context, return `{falsified: true, evidence: '<short reason>'}`. Otherwise return `{falsified: false}`."

Findings with `falsified: true` are dropped before aggregation. Token budget per falsifier: ~8000 tokens.

**Claim mode:** the falsifier receives the relevant **primary-source excerpt**
(not a diff snippet) and tries to disprove the finding against it — i.e. confirm
the finding is itself grounded in the source before it's used to overturn a
conclusion. A finding that misreads or misquotes its source is dropped.

**Deliverable mode:** the falsifier receives the **surrounding deliverable text**
(not a diff snippet) and re-counts what the finding claims to have measured.
Drop the finding if the count is wrong (it says "zero tables" and there are
four), if the cited passage doesn't exist, or if the check does not apply to the
genre the deliverable actually is (a `reference` page flagged for list density).
A miscounted finding is exactly the kind an author dismisses, so this stage is
what keeps the lens credible. **Never falsify a `checklist-unavailable`
finding** — it is a meta-signal about the run, not a claim about the artifact,
and it cites a path that by definition could not be read, so the "cited passage
doesn't exist" rule would silently delete the one signal that says the review
was degraded.

### Step 6 — Aggregate + drift-guard + dedup

After falsifier:

1. **Drift-guard.** Compute cosine similarity between each finding's `claim` text and the diff text. Drop findings with similarity < 0.3 (off-topic / drift, per CodeAgent QA-Checker pattern). Use a simple TF-IDF approximation if no embedding model available. **Deliverable mode:** compare the claim against its **cited passage plus that passage's section**, NOT the concatenated document. The 0.3 cutoff was calibrated against a short focused diff, and cosine between a short claim and a long whole document is systematically lower (the document's larger vector norm shrinks each shared term's contribution), so reusing the number against full text would silently make this a stricter filter in the one mode that has a single lens and no cross-lens corroboration. Exempt `checklist-unavailable` entirely — its claim is about a standard that failed to load, shares almost no vocabulary with the deliverable, and would always drift-drop.
2. **Cross-iteration dedup.** For iteration ≥ 2: drop findings whose `claim` is cosine-similar (≥0.85) to any finding already in `state.findings_history[]` that was addressed.
3. **Confidence filter.** For iteration ≥ 2: drop LLM findings with `confidence < 70` (execution-grounded always kept).
4. **Classify.** `actionable` = severity ≥ medium AND `load_bearing: true`. `speculative` = the rest.

### Step 7 — Decide

- **All clean** (0 actionable findings): write `completion: 'review-clean'` to state; emit `<promise>review-clean</promise>` with a one-paragraph summary; release locks; exit.
- **More iterations available** (`iter + 1 < max_iterations` AND cost-spent < ceiling AND ≥1 actionable):
  - Apply fixes for `actionable` findings using `Edit` / `Write` tools (one fix per actionable finding; if a fix is non-obvious, write a tracking-only comment in the affected file with a `// REVIEW-LOOP-iter<N>:` prefix).
  - Increment `state.iteration`; append the current iteration's full findings to `state.findings_history`.
  - Save state.
  - Release locks.
  - Exit with stdout JSON: `{"decision": "block", "reason": "/review-loop --auto --session-id <id> --iteration <next> --max-iter <max> --mode <mode>", "systemMessage": "Review-loop iter <next>/<max>"}` — preserve the `--mode` value across iterations; the mode does not flip mid-loop.
- **Exhausted** (iter cap reached OR cost ceiling crossed):
  - Write `completion: 'review-exhausted'`.
  - Emit `<promise>review-exhausted</promise>` followed by an exit checklist listing unresolved `actionable` findings + the full `speculative` set.
  - Release locks; exit.

**Claim mode:** an `actionable` finding means a load-bearing claim is wrong or
overstated. The "fix" is to correct the conclusion — if the analysis lives in a
file, `Edit` it; if it lives only in the conversation, emit the corrected claim
in your summary. Re-running reviewers on a purely deflationary correction
(claiming *less*) is usually unnecessary — prefer a single pass unless a
correction introduces a NEW load-bearing claim.

**Deliverable mode:** an `actionable` finding means the reader cannot use the
deliverable as written. The fix is a **restructure, never a rewrite** — turn the
inventory into the table, move the answer into the opening, paste the numbers in
beside the link, add the gloss on first use. Do not change what the deliverable
claims: its facts are another mode's business, and a usability pass that quietly
edits substance is a worse defect than the one it fixed. If the deliverable lives
only in the conversation, emit the restructured version in your summary rather
than describing what should change.

## Output format

Every run produces a final assistant text block ending with exactly one of:
- `<promise>review-clean</promise>`
- `<promise>review-stalled</promise>`
- `<promise>review-exhausted</promise>`
- (no promise marker if iteration continues — the hook re-invokes you)

The Stop hook reads stdout for the JSON `{decision, reason, systemMessage}` block; the user sees the assistant text.

## Leave a trail on the PR (ALWAYS, when one exists)

On a **terminal verdict** (`review-clean` / `review-exhausted` / `review-stalled`) — i.e. the same point
you'd archive state — record the verdict ON the open PR for this branch, so "was this reviewed?" is
answerable from the PR itself, not from a chat transcript. This is **not optional**: a review without a
trail is treated as incomplete. It is self-gating — most loop runs are on uncommitted WIP with no PR, and
those post nothing.

1. **Detect the PR (read-only):** `gh pr view --json number,headRefOid,headRefName,state` for the current
   branch. If it errors, returns no PR, or `state != "OPEN"` → **skip silently** (no PR = nothing to do).
2. **Compose a commit-pinned verdict** from the template below. Pin to the PR's `headRefOid`. The verdict
   is valid only for the commit it ran against.
3. **Be honest about staleness:** if the reviewed diff is uncommitted WIP, or local `HEAD` differs from the
   PR's `headRefOid`, say so in the comment — the verdict covers the reviewed state, not necessarily the
   PR head. Never let a review of one state read as covering another.
4. **Idempotent — never spam:** if the PR's most recent comment already carries the
   `<!-- review-loop:<sha> -->` marker, update it with `gh pr comment <n> --edit-last --body-file <tmp>`;
   otherwise add one with `gh pr comment <n> --body-file <tmp>` (temp file — the body has markdown/newlines).
5. Posting to GitHub is the loop's only outbound action. It writes a comment to the operator's own PR
   (reversible — editable/deletable) and is the standing instruction, so post it as part of completing the
   review; do **not** block the terminal exit waiting for a separate confirm.

```
## 🔬 review-loop verdict — pinned to `<headRefOid short>`

**Reviewed:** `<scope/range>` on `<headRefName>` · **iterations:** <n> · **method:** automated multi-agent
review-loop — <K> lens subagents (<names>) in independent contexts + an execution-grounded lint/test/build
check. Findings falsified against the diff; not a human review.

**Verdict:** <review-clean ✅ | exhausted — N unresolved | stalled> <one line>
**Independence:** <independent — the reviewing session did not author this diff | ⚠️ SELF-REVIEW — NOT INDEPENDENT | unverified — could not be determined, see below>

### Fixed in-loop
- **[load-bearing]** `path:line` — <issue> → <fix>

### Surfaced, not fixed
- `path:line` — <issue> (<why deferred>)

<!-- review-loop:<headRefOid> --> _Re-run after new commits to refresh._
```

**When independence is anything but `independent`,** the verdict line MUST NOT contain the token
`review-clean` — the auto-merge gate reads that token as "an independent review cleared this," and a
self-review has not. Write `self-review — no blocking findings` instead, and open the comment with:

```
> ⚠️ **SELF-REVIEW — NOT INDEPENDENT.** This review ran in the session that wrote the code, so it
> carries the same blind spots that produced any defect in it. Discount it accordingly.
> Reason given: <verbatim --self-review-ack reason, or "hook-dispatched: the Stop event fires in the
> authoring session">. Files this session edited in this diff: <n> (<list>).
> A fresh session must still review this before it lands.
```

This is the whole enforcement: a self-review can still find things and still leaves a trail, but it
cannot produce the token that lands the change. Withholding it needs no change to the merge gate.

## Surface the shipping gap — no-PR nudge (ALWAYS, on a terminal verdict)

"Leave a trail on the PR" only fires when a PR **already exists**. The complementary risk — the one that
bites silently — is a session that did real, reviewable work and then **stopped without shipping it**: the
change is uncommitted, or committed but never PR'd, or parked on an unrelated branch. This is especially
common for **spawned background-task ("chip") sessions**, which finish a self-contained unit of work in a
worktree and have no memory telling them to open a PR. On a terminal verdict, after the PR-trail step, run
this cheap read-only check and **surface** (never auto-act on) the gap.

1. **Self-gate.** If an open PR for the branch already exists (the trail step posted to it) → skip. If there
   are no reviewable changes → skip. This no-op is the common case.
2. **Detect the shape (read-only)** from `git status --porcelain`, `git rev-parse --abbrev-ref HEAD`,
   `git log @{u}.. 2>/dev/null` (unpushed commits), and the `gh pr view` result already gathered:
   - **Uncommitted** — the reviewed changes are still in the working tree (dirty `git status`).
   - **Committed, no PR** — `HEAD` carries the reviewed change but `gh pr view` found no open PR.
   - **Unrelated branch** — the current branch's name/topic looks unrelated to the reviewed files (a weak
     heuristic — surface it for the operator to judge, never assert it).
3. **Surface in the final summary** (one short block, not a wall): name the shape and recommend the next
   shipping step — e.g. "recommend committing + opening a PR off the default branch", or "branch has commits
   but no PR — open one?" — and **offer to do it**, confirmation-gated. Creating a branch / commit / PR is an
   outbound, judgment-laden action: **never auto-commit, auto-push, or auto-open a PR** without an explicit
   per-instance confirm. Many sessions intentionally leave WIP — this is a **nudge, not a gate**, and it never
   blocks the terminal exit.
4. **Docs-staleness one-liner.** If the reviewed diff changed behavior, a flag/CLI surface, an API, or a
   config field, add one line: "confirm the docs that describe this (README / skill / `docs/`) are updated —
   this loop reviewed the diff, not whether docs elsewhere went stale." Skip if the diff is doc-only or
   clearly doc-neutral.

Keep it to a few lines. A reviewed-clean change that never ships is not "done" — the operator should leave
the session knowing exactly what remains to ship it.

## Reconcile the session's handoff (code mode; only the handoff THIS session wrote)

`session-end` writes a handoff (`<primary-checkout>/.claude/handoffs/<ts>_<slug>_<discriminator>.md`) BEFORE this review runs, so its
review-status can be stale the instant the verdict lands ("not reviewed yet"), or a load-bearing finding can
change the risk of its stated next-action. On a **terminal verdict**, reconcile it — run this AFTER "Leave a
trail on the PR" and BEFORE "State archival", on the still-on-disk state. **Code mode only** (skip in
plan/claim/deliverable mode, as claim mode skips the PR trail). Self-gating: most runs wrote no handoff and
this no-ops.

1. **Attribute by POSITIVE authorship — never by recency.** Anchor to the **primary checkout** —
   `git worktree list --porcelain | head -1 | sed 's/^worktree //'`, which is where `session-end` writes so
   the handoff survives worktree cleanup — and operate ONLY on `<primary-checkout>/.claude/handoffs/`. Do
   NOT anchor to `git rev-parse --show-toplevel`: in a linked worktree that is the worktree's own root, and
   this step would find zero handoffs and silently stop reconciling. **Do not gate on `git status` either**
   — the handoff lives in the primary checkout, so a `git status` in the reviewed worktree will not list
   it, and treating that as "no candidates" reproduces the same silent skip. List that directory directly.
   From filenames + first lines alone (no full body reads yet), find the handoff whose FIRST line is
   `<!-- review-loop:session:<id> -->` with `<id>` equal
   to THIS run's `--session-id`. If exactly zero match (incl. `unattributed`, or session-end stamped none) OR
   more than one matches → **skip silently**; this no-op is the expected outcome for most runs. NEVER fall back
   to "newest by mtime" — concurrent sibling sessions write into the same dir, and editing an unbound handoff
   corrupts another live session's continuation prompt.
2. **Idempotency.** If the attributed handoff already carries `<!-- review-loop-reconciled:<diff_sha> -->` for
   the current reviewed state, skip (already done). Otherwise reconcile and write/replace that single marker in
   place — never append a second status block.
3. **Reconcile ONLY what the review changed — surgical, never a rewrite:**
   - **Correct a now-false review-status line — literal trigger only.** Edit a line ONLY if it literally
     asserts the work is "unreviewed" / "not yet reviewed" / "untested" / "needs /review-loop" / "review
     pending" (case-insensitive; this enumerated set, no inference). Replace with per-verdict HONEST wording:
     clean → "review-loop: clean (`<diff_sha>`)"; exhausted → "review-loop: EXHAUSTED — N unresolved actionable
     findings (see PR trail)"; stalled → "review-loop: STALLED — no progress, not clean". NEVER write
     "clean"/"reviewed-OK" on a non-clean verdict. No such literal line → make NO status edit.
   - **Elevate a load-bearing finding the handoff under-weights — only if it changes the next-action's risk.**
     Add ONE line to the continuation prompt's next-action or verification-debts, phrased as an OPEN risk, never
     as resolved: "precondition: review-loop flagged <X> as unresolved (see PR trail) — confirm before
     <next-action>." Only for a finding already classified load-bearing; non-load-bearing findings never touch
     the handoff.
   - **Done-close shape:** if the handoff has no continuation-prompt / next-action / verification-debts section,
     do NOT inject one — at most fix a literal false status line, else skip. Never fabricate a section the
     author did not write.
   - Do not paraphrase, re-voice, or add review jargon to any other line. Cap: ≤2 status edits + ≤1
     precondition. **Quote each original line you changed** in the final summary.
4. **Atomic write:** write the full reconciled content to a temp file in the same dir and rename over the
   original (never a partial in-place stream), so a crash cannot truncate the irreplaceable handoff.
5. A handoff-only edit is classified nothing-reviewable by the Stop hook's Gate A, so it will not re-arm the
   loop. (Edge: avoid handoff slugs ending in a plan-glob suffix like `-retrospective` / `-plan` / `-spec`,
   which would route to plan mode.)

## State archival

On terminal exit (clean / stalled / exhausted), move `state/<session-id>.json` to `state/archive/<session-id>-<unix-ts>.json` and the `.lock` file is removed. The hook checks for terminal `completion` field and exits 0 if present (per its escape-hatch list).

## Cost discipline

If at any point `cost_spent_input + cost_spent_output > cost_ceiling_input_tokens`:
- Skip remaining reviewer dispatches in this iteration.
- Treat current accumulated findings as the iteration's output.
- Move to Step 6 / Step 7 (decide).
- The `review-exhausted` branch will be taken in Step 7.

## When invoked manually (no `--auto` flag)

Print a one-line preamble: *"Running /review-loop on <branch>, iter <N>/<max>, scope=<scope>."* before Step 4. Otherwise identical.

## Notes

- Per memory entry `feedback-confidence-per-step`: each reviewer's prompt instructs it to **label `load_bearing` honestly** — in-iter fixes are limited to load-bearing items; speculative items surface as the exit checklist.
- Per memory entry `feedback-claude-code-not-api`: when surfacing cost to the user, label as "plan usage (API-equivalent)" not "$X spent."
- The Stop hook handles the infinite-loop guard (env var `CLAUDE_REVIEW_LOOP_ACTIVE=1` is set before each iteration's re-invocation); the skill itself does not need to check this — if you're running, you've been invited.
- The Stop hook is a cheap **always-on gate, not an always-on review**: after the safety exits it also skips when **nothing reviewable changed** (only docs / handoffs / lockfiles / scratch / generated files) or when the **exact diff was already dispatched** for review, logging the decision to `.local-state/hook.log` either way. Per-project knobs under `<repo>/.claude/`: `review-loop.disabled` (opt out entirely), `review-loop.plan-paths` (globs that count as plan artifacts), `review-loop.code-exts` (extensions that count as reviewable code; the file replaces the built-in default set), `review-loop.deliverable-paths` (globs that count as reader-facing deliverables; also replaces its default set — but anything under `.claude/` stays excluded unconditionally and cannot be re-included by an override, since that path is session machinery), `review-loop.deliverable-standard` (a one-line **pointer** holding the path of the project's own deliverable/report standard, e.g. `docs/REPORT-READABILITY-STANDARD.md` — read by the lens, not by the hook, as additional checks; if it or its target can't be read the lens must say so rather than silently return `[]`).

