/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 (perTHRESHOLDS.reviewLoop.maxIterations)--cost-ceiling-tokens <n>— combined input+output token budget; default 300000--scope <git-diff|head-1>— defaultgit-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>— defaultcode. Controls which reviewer lens set fires in Step 4. The Stop hook auto-selects in this precedence order:- 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; - else any reviewable source file →
--mode code; - else any changed file matching the deliverable globs (
.md/.mdxunderreports/,research/,content/orsrc/content/, plus*-report.md,*-brief.md,*-summary.mdat 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 deliverableby hand for those.--mode claimreviews 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 deliverablereviews 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.
- any changed file matching the plan-artifact globs (
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:
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:
0independent → proceed to Step 1 normally. Nothing else changes.3self-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.3self-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 thereview-cleantoken, so the auto-merge gate keeps holding and a human decides. The reason is quoted verbatim on the PR.3self-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.4unverified → proceed, but the verdict saysindependence: unverifiedand 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:
{
"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— usefs.openSync(path, 'wx')(O_EXCL). - Per-repo lock
.local-state/repo-<sha1(toplevel)>.lock— same pattern. Hash ofgit 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-repoto.local-state/<session-id>.logand 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 filesgit diff HEAD— full diff textgit 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.mdagents/design-coherence.mdagents/adversarial.mdagents/ship-readiness.mdagents/statistical-rigor.md(skip if project has.claude/review-loop.disabled-roleslistingstatistical-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-standardif 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 achecklist-unavailablefinding, 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:
- Drift-guard. Compute cosine similarity between each finding's
claimtext 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. Exemptchecklist-unavailableentirely — its claim is about a standard that failed to load, shares almost no vocabulary with the deliverable, and would always drift-drop. - Cross-iteration dedup. For iteration ≥ 2: drop findings whose
claimis cosine-similar (≥0.85) to any finding already instate.findings_history[]that was addressed. - Confidence filter. For iteration ≥ 2: drop LLM findings with
confidence < 70(execution-grounded always kept). - Classify.
actionable= severity ≥ medium ANDload_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_iterationsAND cost-spent < ceiling AND ≥1 actionable):- Apply fixes for
actionablefindings usingEdit/Writetools (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 tostate.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--modevalue across iterations; the mode does not flip mid-loop.
- Apply fixes for
- Exhausted (iter cap reached OR cost ceiling crossed):
- Write
completion: 'review-exhausted'. - Emit
<promise>review-exhausted</promise>followed by an exit checklist listing unresolvedactionablefindings + the fullspeculativeset. - Release locks; exit.
- Write
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.
- Detect the PR (read-only):
gh pr view --json number,headRefOid,headRefName,statefor the current branch. If it errors, returns no PR, orstate != "OPEN"→ skip silently (no PR = nothing to do). - 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. - Be honest about staleness: if the reviewed diff is uncommitted WIP, or local
HEADdiffers from the PR'sheadRefOid, 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. - Idempotent — never spam: if the PR's most recent comment already carries the
<!-- review-loop:<sha> -->marker, update it withgh pr comment <n> --edit-last --body-file <tmp>; otherwise add one withgh pr comment <n> --body-file <tmp>(temp file — the body has markdown/newlines). - 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.
- 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.
- Detect the shape (read-only) from
git status --porcelain,git rev-parse --abbrev-ref HEAD,git log @{u}.. 2>/dev/null(unpushed commits), and thegh pr viewresult already gathered:- Uncommitted — the reviewed changes are still in the working tree (dirty
git status). - Committed, no PR —
HEADcarries the reviewed change butgh pr viewfound 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).
- Uncommitted — the reviewed changes are still in the working tree (dirty
- 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.
- 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.
- Attribute by POSITIVE authorship — never by recency. Anchor to the primary checkout —
git worktree list --porcelain | head -1 | sed 's/^worktree //', which is wheresession-endwrites so the handoff survives worktree cleanup — and operate ONLY on<primary-checkout>/.claude/handoffs/. Do NOT anchor togit 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 ongit statuseither — the handoff lives in the primary checkout, so agit statusin 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. - 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. - 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 as unresolved (see PR trail) — confirm before ." 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.
- 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 (
- 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.
- 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-exhaustedbranch will be taken in Step 7.
When invoked manually (no --auto flag)
Print a one-line preamble: "Running /review-loop on , iter /, scope=." before Step 4. Otherwise identical.
Notes
- Per memory entry
feedback-confidence-per-step: each reviewer's prompt instructs it to labelload_bearinghonestly — 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=1is 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.logeither 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[]).