review-anvil — Iterative Multi-Agent Fix/Review Loop
Wrap a code change in requested rounds of parallel reviewer subagents + orchestrator-applied fixes. Each round = (parallel review by M agents, each with a distinct lens) → (you synthesize and verify findings) → (you apply fixes, run the build/test gate, commit) → next round. In productive per_fix runs, the orchestrator may continue adaptively after the requested rounds until convergence or max_rounds.
Preset skills
This is the engine. Three preset skills in the same plugin pin common configurations; each is a separate skills/<name>/SKILL.md whose description triggers activation cross-agent.
| Preset |
Pins |
Intent |
review-anvil (engine) |
nothing |
Default fix/commit loop, or any custom param combination. |
review-anvil-readonly |
commit_mode=none; default rounds=1 |
Read-only review — no edits, no commits. |
review-anvil-pr [<locator>] |
commit_mode=none, target=<locator>, report_path=<file>, trusted run_ordinal=<observed> |
Review a GitHub PR and post the report back as a PR comment (orchestrates scripts/pr-helper.sh). Locator auto-detected from the current branch when omitted. |
review-anvil-improve-pr [<locator>] |
commit_mode=per_fix, target=<base>...HEAD, report_path=<file>, trusted run_ordinal=<observed> |
Improve a checked-out PR: fix commits across requested rounds plus any adaptive continuation, then push. Targets the branch (not a PR locator), deliberately routing around the PR-target/per_fix rule below. |
Parameters
Parse the user's free-form args string into:
| Param |
Default |
Plain-English forms |
rounds |
3 |
"5 rounds", "three rounds", "do 4 passes" |
max_rounds |
per_fix: min(max(6, rounds), rounds + adaptive budget) — budget 1/2/3 for small/medium/large diffs (see Parsing); rounds for commit_mode=none |
"max 4 rounds", "allow one extra round", "3 rounds, continue if needed"; "exactly 3 rounds", "only 3 rounds", or "no extra rounds" keeps max_rounds=rounds |
agents |
3 |
"3 agents", "2 reviewers", or a mix like "2 codex + 1 claude" |
focus |
the four pillars (correctness, maintainability, simplicity, production blast-radius) |
"focus on async correctness"; an only: prefix replaces the defaults instead of appending |
target |
auto-detect |
"PR #42", "branch", "uncommitted", "src/auth/", "last 3 commits" |
allow_new_deps |
false |
"allow new deps" — auto-apply fixes that introduce new imports/subsystems instead of deferring them |
min_fix_severity |
medium |
"auto-fix high and above", "fix only critical" — minimum severity for auto-fix; lower findings are listed, not applied |
commit_mode |
per_fix |
per_fix (one commit per fix-group) or none ("review only", "don't commit", "no fixes") |
approve |
allowed |
"never approve", "comment only", approve: never — always write {"event": "COMMENT"} to .approval.json. Presets additionally export REVIEW_ANVIL_NO_APPROVE=1 so the helper enforces it mechanically. Only meaningful for review-only PR runs |
reproduction |
auto |
auto, on, or off — default-on batched reproduction of uncertain medium+ findings before auto-fix/reporting; "skip reproduction" disables it and marks single-reviewer material findings as unconfirmed |
proof_runner |
unavailable |
proof_runner: /absolute/trusted/path — opt-in isolated executable proof runner; generated probes remain unexecuted when unset |
adversarial |
off |
off, auto, challenge, targeted, full, or strict — read-only post-synthesis review that attacks candidate findings and would-apply plans before they become final guidance |
adversarial_rounds |
1 |
one adversarial pass by default; max 2, and a second pass runs only when the first pass materially changes medium+ guidance |
disagreement_policy |
defer |
defer moves unresolved material disputes to Deferred; comment keeps the finding actionable but forces review-only PR approvals to COMMENT |
verify_cmd |
auto-detect |
"verify with npm test", verify_cmd: none to skip — build/test command run after each round's fixes (see "Build/test gate"; per_fix only) |
reviewer_timeout |
600 (420 for small diffs) |
"timeout 10 minutes" — hard per-reviewer wall-clock cap in seconds for Bash-dispatched reviewers (see run-reviewer.sh). Default is 3× the slowest legitimate reviewer observed in real runs (98–213s); when unset and the diff is under ~500 changed lines (added+removed — the same measure as the adaptive budget tiers), requested rounds use 420 (2× that observed max) so a hung reviewer pins the wave 3 minutes less. Adaptive rounds always use the full base value — 600, or 1200 after the >5000-line doubling (doubling transforms the base; the small-diff reduction never applies to adaptive rounds). Explicit user values are never scaled or doubled |
report_path |
unset |
File path; when set, the engine writes the final report there (creating parent dirs) and prints exactly that path as its last output line so downstream consumers can pick it up |
run_ordinal is trusted invocation context, not a free-form user parameter.
Use run_ordinal only when it is a positive trusted value. Treat an absent
value or unavailable as local/degraded context and omit RUN from item IDs.
Keep UUID markers as the collision-resistant run identity. The ordinal adds
human-readable provenance; it does not replace the marker.
Parsing
- Split the args on top-level commas; canonicalize each segment to a
(param, value) pair — explicit key: value maps directly, plain-English forms per the table. Unrecognized segments are noted and ignored.
- First occurrence wins per param; later duplicates are dropped with
warning: user-supplied <param>=<value> ignored — earlier value wins. Unset params take defaults.
- Presets assemble
<pins>, <user-args>, <overridable defaults>: pins come first so first-occurrence-wins makes them authoritative; defaults come last so user args beat them.
- Pin-rejection (presets; defense in depth against the prose parser being talked into overrides): before assembling, segment-split
$ARGUMENTS as above, lowercase each segment's key (the text before its first :), and abort with error: <param> is pinned by <preset-name> and cannot be overridden in args if any key equals a pinned param. Match segment keys, never raw substrings — focus: "target: PR safety" has key focus and must pass. A host that cannot segment-split must abort (error: pin-rejection unavailable in this environment; refusing to invoke engine without pin enforcement), not degrade to substring scanning.
agents: a count (use the mix table below) or an explicit mix naming codex/codex-exec / claude/claude-exec — honor a mix exactly.
target auto-detect order: currently checked-out PR (e.g. gh pr view --json number,headRefName, a GitHub MCP query, or REST) → branch-vs-main diff (git diff main...HEAD) → uncommitted changes (git diff + git diff --cached). Empty args = all defaults.
rounds is the requested count. Resolve max_rounds after rounds, the final commit_mode (including the PR-locator rule below), and the target: the per_fix default is min(max(6, rounds), rounds + budget), where the adaptive budget scales with the target's changed-line count (added+removed in the materialized diff; materialize it once at resolution — per_fix targets are always local git diffs): under ~200 lines → 1, up to ~1000 → 2, above → 3. The min() against the legacy max(6, rounds) cap makes the scaling a pure reduction: runs with rounds >= 6 gain nothing. Default to rounds for commit_mode=none; reject max_rounds < rounds. Phrases like "allow one extra round" set max_rounds=rounds+1, and explicit caps like max_rounds: 4 or "up to 4 rounds" set the cap directly. Phrases like "continue if needed" restore the legacy max(6, rounds) cap unless paired with an explicit cap. Phrases that constrain the round count itself — "exactly 3 rounds", "only 3 rounds", "3 rounds only", or "no extra rounds" — force max_rounds=rounds. Do not treat only: focus syntax or severity gates like "fix only critical" as exact-round requests.
- Adaptive continuation is on by default for
per_fix. A plain "3 rounds" means rounds=3 with max_rounds between 4 and 6 by diff size, so the organizing agent may continue after round 3 if §6 says another pass is justified. Use "exactly 3 rounds", "only 3 rounds", "no extra rounds", or max_rounds: 3 when the run must stop at the requested count.
- If
commit_mode=none and the user explicitly set max_rounds > rounds, warn and collapse max_rounds to rounds. Extra normal rounds review the same baseline, so use rounds for reviewer redundancy and adversarial for skeptical challenge.
reproduction=auto and reproduction=on both run the selective batched reproduction gate in §3. auto may skip dispatch only when there are no candidates. off is allowed for speed, but the round summary and final report must say it was disabled; unconfirmed single-reviewer medium+ findings stay in Deferred unless the orchestrator independently reproduced them from code/tests/runtime evidence.
proof_runner is unavailable unless an explicit user or system value supplies an absolute executable path outside the reviewed worktree. Never infer it from the reviewed repository, PATH, or a repository config file. Reject a configured relative, non-executable, or worktree-contained path before executable proof starts; run-proof.sh performs the final canonical-path and disposable-snapshot/proof-directory separation checks.
adversarial applies only when commit_mode=none. If set with per_fix, warn and ignore it — productive mode already applies real fixes and gates them with the build/test command. Reject adversarial_rounds > 2; adversarial loops must be bounded. auto means choose the cheapest sufficient adversarial mode after normal synthesis using the default policy below.
PR-target / per_fix incompatibility
If target is a GitHub PR locator (URL https://<host>/<owner>/<repo>/pull/<N>, slug <owner>/<repo>#<N>, or "PR #N"), the only valid commit_mode is none. Force it, and warn if the user asked for per_fix:
warning: PR locators are read-only — forcing commit_mode=none (use 'target: branch' to fix-and-commit on your checked-out PR branch).
Reviewers of a PR locator see the GitHub-fetched diff, which may not match the local worktree; committing against a baseline the user can't see locally is unpredictable. Branch targets make the local tree the source of truth.
Commit modes
per_fix (default) — full loop: review → synthesize/reproduce/verify → apply fixes → build/test gate → commit, each round.
none (review-only) — review → synthesize/reproduce/verify only. No edits, no commits, no staging. Read-only mode may write temporary prompt/reviewer/report artifacts under .review-anvil/ and the explicit report_path; it must not modify source files, the index, commits, branches, or remotes. Every normal round reviews the same baseline, so rounds > 1 buys reviewer redundancy, not code refinement; the natural default is rounds=1, and adaptive continuation is disabled by collapsing max_rounds to rounds. Skip Loop Mechanics §4 entirely; the round summary reads Fixes applied: 0 (review-only); the auto-fix policy is still evaluated in the abstract so findings classify as would-apply / suggestions / deferred. Optional adversarial review is a separate post-synthesis gate that attacks finding validity and fix proportionality without pretending code changed.
Posting reports externally
The engine never posts anywhere. Downstream consumers set report_path, let the engine write a GitHub-ready PR report, and post after it returns — review-anvil-pr + its pr-helper.sh is the reference implementation.
When report_path is set, optimize the report for a PR timeline reader, not for archival completeness. The per-round console output and reviewer artifacts are the transcript; the posted report is the decision summary plus the few findings that need action.
Adaptive continuation details belong in Run Details unless they change the review decision; do not paste per-round continuation reasoning into PR reports.
Examples
Skill review-anvil → 3 requested rounds, adaptive up to 4–6 total rounds by diff size, 2 codex + 1 claude, four-pillar focus, auto-detected target.
Skill review-anvil "5 rounds, 2 codex + 1 claude, focus: async correctness, target: PR #42"
Skill review-anvil "3 rounds, max_rounds: 4" → 3 requested rounds, then at most 1 adaptive round if the continuation policy allows it.
Skill review-anvil "1 round, only: security, target: src/auth/"
Skill review-anvil "fix only critical" → severity gate raised to critical; everything else surfaces as suggestions.
Skill review-anvil "target: PR #42, adversarial: auto" → normal review first, then adversarial review only if the synthesized findings/fix plans need a validity or proportionality challenge.
Default Mix Policy
When the user gives a count but no mix:
agents |
Mix |
| 1 |
1 codex-exec |
| 2 |
1 codex-exec + 1 claude-exec |
| 3 |
2 codex-exec + 1 claude-exec |
| 4 |
2 codex-exec + 2 claude-exec |
| 5 |
3 codex-exec + 2 claude-exec |
| N |
~60/40 codex/claude split, codex gets the odd one |
Rationale: codex-exec surfaces more issues per call in our usage, so it gets the larger share (and the agents=1 slot).
Loop Mechanics
Run the loop for the requested rounds, then continue adaptively up to
max_rounds only when §6 allows adaptive continuation. Within a round:
1. Snapshot the target
Capture the target's state at round start so all reviewers see the same input:
- Non-PR targets (branch, uncommitted, path): materialize the diff with the appropriate
git diff ….
- PR targets (always
commit_mode=none): fetch the PR's diff via gh pr diff <N> -R <owner>/<repo> (or equivalent MCP/REST). The local worktree is irrelevant — reviewers see the PR as it exists on GitHub.
- Whenever PR context is available — a PR-locator target, or a preset that supplies it (
review-anvil-improve-pr does, after verify-checkout) — fetch the PR title/body/base branch/file list too, then infer the PR's intended scope in one sentence (e.g. "performance optimization in annotation seeding", "left-sidebar UX reorganization"). Put that scope in every reviewer prompt. A finding is actionable only if the PR introduces/regresses it or if it directly undermines the PR's stated purpose. Obvious, high-confidence pre-existing defects may be mentioned, but only under a separate "Out-of-scope follow-ups" section — never as blockers or inline actionable review comments for the current PR.
- Likewise gather the complete PR review history before dispatch: when a preset supplied the ledger (improve-pr captures it at verify-checkout time), use that; for PR-locator targets fetch it via
pr-helper.sh history <host> <owner> <repo> <n> (ships with review-anvil-pr; threads, review bodies, and fallback comments are paginated and retried once). Include the status-tagged ledger in every reviewer prompt (PR REVIEW HISTORY block): open threads, resolved threads, outdated anchors, summary-only reported findings, prior deferred/outside/review-dismissed items, and explicit local suppressed findings. Pending reviews are not shown to the author and are excluded. Before dispatch, semantically coalesce entries with the same root cause (summary wording changes do not create a second item) while retaining every source URL and strongest status. If history lookup fails after one retry, do not dispatch or post/approve a review that could ignore or duplicate prior feedback; for the read-only preset, REVIEW_ANVIL_SKIP_DISMISSED=1 is the explicit degraded escape hatch and mechanically forces COMMENT.
- Note
git rev-parse HEAD so the round summary can reference the exact baseline (informational-only for PR targets).
2. Dispatch reviewers in parallel
Before any Codex-backed dispatch in the run — reviewer, reproduction,
adversarial, clarity, or action-lock — resolve codex lazily if CODEX_BIN is
not already set. Resolve it from the trusted host environment to an absolute
canonical executable path. Reject a missing, relative, non-executable, or
reviewed-worktree-contained result before that dispatch. Never pin a
package-manager-specific path or resolve the binary from the reviewed
repository.
In Claude Code (the primary host)
Use the Agent tool for claude-exec reviewers. Do NOT use claude -p via Bash — that path is for non-Claude hosts only.
claude-exec: Agent tool, subagent_type: "general-purpose", the assembled Reviewer Prompt as prompt, run_in_background: true. The Agent tool streams natively, has no --max-turns ceiling, and inherits the session environment.
codex-exec: Bash through the wrapper: REVIEW_ANVIL_REQUIRE_FINDINGS=1 bash <wrapper> .review-anvil/round<N>-<label>.md <reviewer_timeout> -- "$CODEX_BIN" exec -m gpt-5.6-luna -c 'model_reasoning_effort="max"' -c 'shell_environment_policy.inherit="all"' -c 'mcp_servers.webexapis.enabled=false' --ephemeral --sandbox read-only -C <project-dir> '<prompt>' < /dev/null, with run_in_background: true. The validation flag makes the wrapper reject confirmation-only, plan-only, or otherwise incomplete responses that do not end with the required fenced findings block. --ephemeral prevents reviewer sessions from leaking into later dispatches. The < /dev/null is load-bearing: codex takes its prompt as argv and must not inherit an open stdin — the wrapper passes its stdin through (<&0, which the claude fallback needs), and codex blocking on a never-closing fd 0 is a known hang class from real runs.
- Send all M reviewers in a single message with multiple tool calls. The harness notifies you on completion; do not poll.
In Codex CLI or other hosts without the Agent tool
claude-exec: write the assembled prompt to a file, then:
REVIEW_ANVIL_REQUIRE_FINDINGS=1 bash <wrapper> .review-anvil/round<N>-<label>.md <reviewer_timeout> -- \
claude -p --max-turns 100 --no-session-persistence \
--permission-mode dontAsk --output-format text \
--tools "Bash,Read,Glob,Grep" \
--allowedTools "Bash(git:*)" "Read" "Glob" "Grep" \
< .review-anvil/round<N>-<label>.prompt.md
--tools restricts the built-in tool set; --allowedTools auto-approves the listed safe tool uses and is variadic, so the prompt MUST arrive via stdin (the wrapper passes its stdin through). --permission-mode dontAsk keeps the fallback non-interactive by denying anything outside the allowed/read-only path. Do not size --max-turns to the task — task-sized caps keep biting (20 was hit in production), and a reviewer that hits the cap loses its entire output. The wrapper's wall-clock timeout is the real bound; 100 is a runaway backstop that should never bind.
codex-exec: same validation-enabled wrapper around "$CODEX_BIN" exec -m gpt-5.6-luna -c 'model_reasoning_effort="max"' -c 'shell_environment_policy.inherit="all"' -c 'mcp_servers.webexapis.enabled=false' --ephemeral --sandbox read-only -C <project-dir> '<prompt>' < /dev/null — stdin from /dev/null here too.
Launch all M wrapper invocations as background shell processes and wait.
Bash-dispatched reviewers MUST go through run-reviewer.sh
Every shell-dispatched reviewer (codex-exec everywhere; claude-exec outside Claude Code) runs under scripts/run-reviewer.sh (next to this SKILL.md). Never background a bare claude -p ... > out.md 2>&1 and wait on the file — in text mode nothing prints until the final answer, so a hung reviewer and a working one are both a 0-byte file (a production run waited on exactly that for many minutes). The wrapper:
run-reviewer.sh <out_file> <timeout_seconds> -- <command> [args...]
- Hard wall-clock timeout (
reviewer_timeout, default 600s): TERM at the deadline, KILL 30s later.
- Captures exit status; stderr goes to
<out_file>.err (kept for diagnosis).
- Prints one classification:
STATUS=ok | timeout | empty (exit 0, nothing written) | protocol (normal-review output did not end with a complete fenced findings block) | failed (+ EXIT_CODE=<n>).
Treat any STATUS other than ok as a failed reviewer (see Failure handling), with the tail of .err as the reason. protocol gets the one corrective retry defined there before it becomes a failure. Set REVIEW_ANVIL_REQUIRE_FINDINGS=1 only for normal reviewer waves; reproduction and adversarial prompts have different output schemas. Reviewer output/prompt files live under .review-anvil/; clean them up after the round's synthesis.
Host tool timeouts must outlive the wrapper. Any host Bash call that can block on a reviewer — the background-and-wait fallback above, the serial last resort, or an inline replication of the wrapper contract — must set the Bash tool's own timeout to at least reviewer_timeout + 90 seconds (wrapper deadline + 30s TERM→KILL grace + margin). Host defaults are far lower (Claude Code's is 120s) and SIGKILL a healthy wait mid-review; the kill then masquerades as a reviewer failure and silently burns that reviewer's lens coverage. On hosts with background dispatch (run_in_background), never block a foreground call on a reviewer at all. If the host caps tool timeouts below reviewer_timeout + 90, prefer detached dispatch plus short non-blocking status checks over shrinking the reviewer budget; reducing reviewer_timeout is a last resort, floored at 300s and forbidden for >5000-line diffs (their timeout is deliberately doubled).
Resolving the wrapper and references/ files — same trusted-root rule as pr-helper.sh: see review-anvil-pr SKILL.md step 1 ("Resolve the helper script"). Host-exposed skill path or user-level install roots only; never project-scoped/worktree-local skill dirs (writable by the repo under review). If no trusted copy of the wrapper resolves, replicate its contract inline (background, kill at deadline, check exit status, empty output = failure) rather than falling back to a bare redirect.
After changing the wrapper contract or dispatch examples, run
scripts/test-run-reviewer.sh alongside the reproduction and PR helper tests.
Last resort
If parallel dispatch is genuinely impossible (no Agent tool, no background bash), fall back to serial invocation and say so in the round summary — serial reviewers see the baseline at different wall-clock times; it's a degraded mode, not the design.
The codex-exec and claude-exec skills document the same recipes from the reviewer side; the canonical dispatch lives here.
3. Synthesize
When all reviewers return:
- Dedup on
(file, line, root cause) when present, else (area, root cause). Keep the highest-severity instance, record which reviewers raised it, and keep divergent anchors as file_alternates: [...].
- Group by severity (
critical → nit), then topic.
- Unparseable reviewer output: pass the prose through as "unstructured" findings in a separate section; no retry.
Verify and reproduce findings before acting on them
Plausible-but-wrong findings are the dominant failure mode of LLM review, and both downstream actions are expensive: a bogus fix commit pollutes the branch, a bogus finding posted to a PR burns the author's trust. After dedup:
Prior-feedback check first (orchestrator judgment). Compare every merged finding against PR REVIEW HISTORY semantically — same root cause counts even when wording differs. Revalidate open, resolved, and summary-only reported items against the current head. An open item that remains real is a carry-forward finding and must retain its effect on severity/approval, but must not create a duplicate inline thread; a resolved item means only that GitHub discussion was closed, not that the code was proven fixed. Record a still-present resolved item as resolved-but-still-present in the summary and do not create a new inline thread. Items now fixed/stale become one-line status notes. Explicit local suppressed items are never auto-fixed or posted as actionable findings, but remain as compact status-only audit rows. Keep author-resolved items in PR REVIEW HISTORY for reviewer context. After synthesis and dedup, drop semantic matches to author-resolved items before building reproduction candidates. Exception: retain a finding when the reviewer explicitly set prior_feedback: reintroduced for a distinct new instance with new evidence. Do not report, post, auto-fix, or let ordinary author-resolved matches affect approval. A reintroduced finding remains actionable; it affects approval only at critical or high severity. The post-time helper catches near-verbatim repeats (exact path + text similarity ≥ 0.9) as a deterministic duplicate-thread backstop.
Scope/artifact filter next (orchestrator judgment). Drop or move to out-of-scope follow-ups before reproduction when the claim is about archived design notes, changelogs, old migration examples, generated fixtures, vendored files, or historical docs that are not the review's live product surface. Do not spend verifier budget proving historical provenance is stale. Conversely, live docs that users rely on — README usage, CLI help, API docs, config reference, plugin metadata, or marketplace copy — are product surface and may become reproduction candidates when they drift from code/runtime behavior.
Assign provenance IDs. Assign IDs after semantic deduplication, prior-feedback classification, and scope filtering, and before reproduction or adversarial dispatch. Use this output recipe:
PR finding: `RAV-RUN<run>-R<origin-round>-F<ordinal>`
PR plan: `RAV-RUN<run>-R<origin-round>-P<ordinal>`
Local: `RAV-R<origin-round>-F<ordinal>` / `RAV-R<origin-round>-P<ordinal>`
Canonical grammar: RAV-(RUN<run>-)?R<origin-round>-(F|P)<ordinal>.
run and origin-round are unpadded positive base-10 integers. Encode
ordinal from an unpadded positive base-10 integer by left-padding only to a
minimum width of three digits: encode 1 as 001, 10 as 010, 100 as
100, and 1000 as 1000; do not add any other leading zero. Valid
multi-digit examples are
RAV-RUN12-R10-F010 and RAV-R12-P1000. Invalid forms include
RAV-RUN03-R2-F001, RAV-RUN3-R02-F001, RAV-RUN3-R2-F0001,
RAV-RUN0-R2-F001, RAV-RUN3-R0-F001, and RAV-RUN3-R2-F000.
ID legend: RUN is the observed PR review run, R is the immutable origin round, F is a finding, and P is a plan. Examples are RAV-RUN3-R2-F001, RAV-RUN3-R2-P001, RAV-R2-F001, and RAV-R2-P001.
Cross-round allocation example: in trusted PR run 3, a history finding with
id=RAV-RUN1-R2-F007 keeps that ID. A new round-1 finding receives
RAV-RUN3-R1-F001 and keeps it when re-raised in round 2. The next new
round-2 finding receives RAV-RUN3-R2-F002. A round-2 plan covering those
two current-run findings receives RAV-RUN3-R2-P001. The matching
local/degraded allocation for the same newly allocated items has no history
carry-forward: RAV-R1-F001, RAV-R2-F002, and RAV-R2-P001.
The carried RAV-RUN1-R2-F007 has no new inline comment. It remains
available in history, the report, reproduction, and adversarial review.
Finding and plan ordinals are independent, run-wide counters named
next_finding_ordinal and next_plan_ordinal. Both start at 001.
Counters never reset each round and never reuse gaps. Before assignment,
order findings by priority, normalized path, line, and topic; order plans by
covered-finding priority, area, and subject.
A finding's origin round is the first normal review round that raises it. A
plan's origin round is the normal review round in which its concrete fix
group is first assembled.
The origin round never changes after confirmation, refutation, priority change, deferral, fix, or re-raise.
Reproduction and adversarial passes are not rounds.
Carried findings consume the exact id= value supplied in PR REVIEW HISTORY;
use that complete ID unchanged without advancing a current-run
counter. For an actionable history entry that has only legacy=,
use the legacy= value as a source alias and assign the next canonical ID. Do not
assign a new ID to a non-actionable legacy-only entry. Historical RAVF###, RAVW###, F-###, and W-### forms are migration/read-boundary aliases only. New findings and plans always use the recipe above.
Once assigned, keep the complete ID unchanged in reproduction candidates,
adversarial targets, report rows, plan coverage, and later round references.
ID reuse flows to an inline body only when the finding is otherwise eligible
for a new inline. Ordinary open, resolved, and reported carry-forwards
retain their IDs in history, reports, reproduction, and adversarial targets,
but do not create a new inline thread.
Already-assigned in-scope low/nit and set-aside findings keep their complete canonical F IDs.
Truly unassigned out-of-scope follow-ups stay distinct and do not receive an F ID.
Build REPRODUCTION CANDIDATES after prior-feedback classification and ID assignment:
- every
medium+ finding raised by exactly one reviewer,
- every
medium+ deletion/dead-code/unused/redundant-code/simplification finding, and any deletion/simplification that would remove runtime code, public docs/API, compatibility behavior, or another high-blast-radius surface, regardless of reviewer count,
- every
critical/high finding whose evidence is mostly inferred from a hunk rather than confirmed from code/runtime context,
- every finding the orchestrator is materially uncertain about after reading the cited files.
When reproduction=auto or on and candidates exist, run the two batched verifier passes defined in references/reproduction-prompt.md. Do not spawn one verifier per finding unless the batch is too large to fit in one prompt. Both passes run backgrounded under the Concurrency section's deadline rule — never an unbounded foreground wait.
- Dispatch
MODE=AUTHOR. Validate its final proofs block and referenced proof-file blocks exactly as the reference requires. Retain rejected material instead of repairing model-supplied proof code.
- For each valid executable manifest, materialize the bundle in the host-created private proof root and create the exact disposable snapshot. If
proof_runner is unavailable, retain the bundle without execution. Otherwise invoke the trusted engine-root run-proof.sh once for that manifest; never execute the probe directly or use a runner from the reviewed repository. Remove the disposable snapshot after the runner completes.
- Dispatch
MODE=VERDICT with the validated static evidence and each retained bundle's runner output or explicit unavailable/failed state. This pass returns confirmed, refuted, unclear, narrowed, or downgraded for the supplied complete canonical finding IDs only and returns each ID unchanged.
Apply reproduction verdicts before auto-fix/reporting:
confirmed and narrowed findings may remain actionable, with narrowed wording when supplied.
downgraded findings re-enter the normal severity gates after changing severity.
refuted findings are dropped from final Findings (or, if useful for transparency, one-line Deferred notes).
unclear findings move to Deferred with We set this aside because <plain-language description of the missing proof>. Rewrite the verifier's reason; do not copy it.
Findings raised independently by 2+ reviewers and not listed as reproduction candidates may skip batched reproduction; consensus is the signal (this is why dedup records who raised what). Still open enough code/context before destructive action to ensure the fix path is coherent.
Deletions ("delete this"/dead/unused/redundant) require reproduction plus execution when per_fix applies the cut — the highest-blast-radius, highest-false-positive class. In per_fix, after reproduction confirms the cut, apply it and run the full test suite: a red gate means keep it. A green gate is necessary but not sufficient (it only proves test-covered behavior), so the reproduction/skeptic pass must also look for a concrete reason the code must stay, visible in the diff (trust boundary, aliasing copy, ordering, back-compat, dedup, edge semantics — or another specific contract). The two cover different blind spots: the gate catches callers the skeptic can't see; the skeptic catches behavior no test exercises. Block only on a red gate or a specific skeptic refutation — not on generic "there might be an unseen caller" (that's what the gate tests). Read-only mode has only the skeptic. Blocked → Deferred (We set this aside because the code is still needed — <what>).
If reproduction=off, say so in the round summary and final report. Required reproduction candidates — including single-reviewer medium+ findings, deletion/high-risk findings, and orchestrator-uncertain findings — cannot become actionable unless the orchestrator independently reproduces them from code/tests/runtime evidence; otherwise move them to Deferred with We set this aside because the needed check was not run.
low/nit findings skip verification: they're below the auto-fix gate and surface as suggestions either way.
Canonical examples for where reproduction helps and where it must stay out of
the way live in references/reproduction-examples.md. After changing this
policy, the reproduction prompt, or the proof-runner boundary, run
scripts/test-reproduction-policy.sh and scripts/test-run-proof.sh alongside
the PR helper tests.
Approving out-of-scope follow-ups
A pre-existing issue outside the PR's scope can still be worth noting, but it must not become an inline/blocking PR finding. Classify each out-of-scope follow-up:
- Auto-approved follow-up — create/queue separate work when all are true: severity is
critical/high (or clearly reproducible medium), the bug is confirmed from code/tests/runtime evidence, it is not a product decision/style preference, it is not already tracked in prior PR feedback or explicitly suppressed, and the fix is plausibly separable from the current PR.
- Needs human triage — mention only as a non-blocking follow-up when the issue is real but severity/ownership/product intent is ambiguous.
- Do not surface — drop if speculative, low/nit, a product decision, already dismissed/tracked, or only discoverable by reviewing unrelated code paths deeply.
When report_path is set, write follow-ups once, after the final round, to <report_path>.followups.json — schema in references/report-artifacts.md. Automation may file issues only for auto_approved entries after duplicate search; presets read the file before posting (the helper deletes it afterwards).
Optional adversarial review (commit_mode=none only)
When adversarial is not off, run a bounded post-synthesis gate after
dedup/reproduction and before writing the final report artifacts. Read
references/adversarial-prompt.md before dispatching adversarial reviewers.
Dispatch every adversary of the selected mode in parallel — one message,
multiple background tool calls, exactly like §2 reviewers, under the
Concurrency section's deadline rule — and synthesize verdicts when all return
or the deadline fires. Never await one adversary before launching the next.
Adversarial review is not another broad review pass and not a simulated patch
application. It attacks the candidate synthesis:
- Finding validity — false-positive claims, wrong anchors, dismissed
findings, out-of-scope issues, over-severity, and missing reachability
evidence.
- Fix proportionality — suggested fixes that would technically address a
problem but create more trouble than they solve: harmful blast radius,
unnecessary dependencies, bloated abstractions, future tech debt, non-local
churn, unsafe deletions, brittle tests, or symptom fixes that miss root cause.
- Report safety — unsafe one-click GitHub suggestions, unclear fix paths,
overconfident approvals, and actionable comments that should be deferred.
Modes:
| Mode |
Dispatch |
Intent |
auto |
Chosen after synthesis |
Selects off, challenge, targeted, or strict using the default policy below. |
challenge |
1 adversary |
Cheap local check over all medium+ findings and would-apply plans. |
targeted |
2 adversaries |
Recommended PR mode: false-positive/scope auditor + fix-plan breaker. Force a deletion skeptic when any would-apply item removes code. |
full |
3 adversaries |
Adds second-order bug hunting across interacting plans, config, migrations, and tests. |
strict |
Same as full |
Approval-sensitive: any required adversary failure or unresolved high+ dispute forces COMMENT. |
Role mapping:
challenge: one combined adversary using the core prompt plus both the
false-positive-scope-auditor and fix-plan-breaker role additions.
targeted: two adversaries, one false-positive-scope-auditor and one
fix-plan-breaker; add/replace with the deletion skeptic behavior from
fix-plan-breaker when any would-apply plan removes code.
full/strict: false-positive-scope-auditor, fix-plan-breaker, and
second-order-bug-hunter; add report-auditor only if the report/approval
artifact itself is the risky surface.
Default policy:
- Local
review-anvil-readonly defaults to off. If the user asks for careful,
skeptical, high-confidence, low-noise, or thorough read-only review, the
orchestrator should append adversarial: auto unless the user explicitly
asked for a fast/rough pass.
review-anvil-pr defaults to adversarial: auto because GitHub output is
public reviewer speech and may include inline comments, one-click
suggestions, or an approval event.
- Explicit user input wins:
adversarial: off disables the gate; explicit
challenge/targeted/full/strict uses that mode. In review-only PR runs,
explicit adversarial: off also forces .approval.json to {"event": "COMMENT"}; unchallenged LLM review should not satisfy branch protection by
accident.
auto selection after normal synthesis:
- First estimate meaningful changed size from the reviewed snapshot. Exclude
generated/vendor/build artifacts, lockfiles, and snapshot/fixture churn unless
those files are the review's product surface. Treat
>1000 meaningful changed
lines or >20 meaningful files as large, and >5000 meaningful changed
lines, >50 meaningful files, or several interacting subsystems as very
large. Size is an escalation floor, not the only signal: a small risky auth or
migration diff can still choose targeted, while a huge mechanical rename
may stay below full after exclusions.
- Use
off only when approval is disabled/impossible and the result is clean
or low/nit-only, has no medium+ inline comments, no GitHub suggestion
blocks, no critical/high actionable or deferred author-action items, and
no would-apply plan with deletion, dependency, non-local behavior, or
abstraction/churn risk. For local non-PR runs, ignore the approval condition.
- Use
challenge for small or self-authored comment-only reviews with material
feedback but no suggestion blocks, no high-risk fix plans, and approve: never / REVIEW_ANVIL_NO_APPROVE=1.
- Use
targeted when candidate output includes any medium+ inline comment,
any Git
…(truncated)
1---2name: review-anvil3description: Use when a user requests repeated code-review and fix rounds, an iterative review loop, multiple reviewer passes, or hardening a change through codex/claude review.4---56# review-anvil — Iterative Multi-Agent Fix/Review Loop78Wrap a code change in **requested rounds of parallel reviewer subagents + orchestrator-applied fixes**. Each round = (parallel review by M agents, each with a distinct lens) → (you synthesize and **verify** findings) → (you apply fixes, run the build/test gate, commit) → next round. In productive `per_fix` runs, the orchestrator may continue adaptively after the requested rounds until convergence or `max_rounds`.910## Preset skills1112This is the **engine**. Three preset skills in the same plugin pin common configurations; each is a separate `skills/<name>/SKILL.md` whose `description` triggers activation cross-agent.1314| Preset | Pins | Intent |15|---|---|---|16| `review-anvil` (engine) | nothing | Default fix/commit loop, or any custom param combination. |17| `review-anvil-readonly` | `commit_mode=none`; default `rounds=1` | Read-only review — no edits, no commits. |18| `review-anvil-pr [<locator>]` | `commit_mode=none`, `target=<locator>`, `report_path=<file>`, trusted `run_ordinal=<observed>` | Review a GitHub PR and post the report back as a PR comment (orchestrates `scripts/pr-helper.sh`). Locator auto-detected from the current branch when omitted. |19| `review-anvil-improve-pr [<locator>]` | `commit_mode=per_fix`, `target=<base>...HEAD`, `report_path=<file>`, trusted `run_ordinal=<observed>` | Improve a checked-out PR: fix commits across requested rounds plus any adaptive continuation, then push. Targets the branch (not a PR locator), deliberately routing around the PR-target/per_fix rule below. |2021## Parameters2223Parse the user's free-form args string into:2425| Param | Default | Plain-English forms |26|---|---|---|27| `rounds` | `3` | "5 rounds", "three rounds", "do 4 passes" |28| `max_rounds` | `per_fix`: `min(max(6, rounds), rounds + adaptive budget)` — budget 1/2/3 for small/medium/large diffs (see Parsing); `rounds` for `commit_mode=none` | "max 4 rounds", "allow one extra round", "3 rounds, continue if needed"; "exactly 3 rounds", "only 3 rounds", or "no extra rounds" keeps `max_rounds=rounds` |29| `agents` | `3` | "3 agents", "2 reviewers", or a mix like `"2 codex + 1 claude"` |30| `focus` | the four pillars (correctness, maintainability, simplicity, production blast-radius) | "focus on async correctness"; an `only:` prefix replaces the defaults instead of appending |31| `target` | auto-detect | "PR #42", "branch", "uncommitted", "src/auth/", "last 3 commits" |32| `allow_new_deps` | `false` | "allow new deps" — auto-apply fixes that introduce new imports/subsystems instead of deferring them |33| `min_fix_severity` | `medium` | "auto-fix high and above", "fix only critical" — minimum severity for auto-fix; lower findings are listed, not applied |34| `commit_mode` | `per_fix` | `per_fix` (one commit per fix-group) or `none` ("review only", "don't commit", "no fixes") |35| `approve` | `allowed` | "never approve", "comment only", `approve: never` — always write `{"event": "COMMENT"}` to `.approval.json`. Presets additionally export `REVIEW_ANVIL_NO_APPROVE=1` so the helper enforces it mechanically. Only meaningful for review-only PR runs |36| `reproduction` | `auto` | `auto`, `on`, or `off` — default-on batched reproduction of uncertain `medium`+ findings before auto-fix/reporting; "skip reproduction" disables it and marks single-reviewer material findings as unconfirmed |37| `proof_runner` | unavailable | `proof_runner: /absolute/trusted/path` — opt-in isolated executable proof runner; generated probes remain unexecuted when unset |38| `adversarial` | `off` | `off`, `auto`, `challenge`, `targeted`, `full`, or `strict` — read-only post-synthesis review that attacks candidate findings and would-apply plans before they become final guidance |39| `adversarial_rounds` | `1` | one adversarial pass by default; max 2, and a second pass runs only when the first pass materially changes `medium`+ guidance |40| `disagreement_policy` | `defer` | `defer` moves unresolved material disputes to Deferred; `comment` keeps the finding actionable but forces review-only PR approvals to COMMENT |41| `verify_cmd` | auto-detect | "verify with `npm test`", `verify_cmd: none` to skip — build/test command run after each round's fixes (see "Build/test gate"; per_fix only) |42| `reviewer_timeout` | `600` (`420` for small diffs) | "timeout 10 minutes" — hard per-reviewer wall-clock cap in seconds for Bash-dispatched reviewers (see `run-reviewer.sh`). Default is ~3× the slowest legitimate reviewer observed in real runs (98–213s); when unset and the diff is under ~500 changed lines (added+removed — the same measure as the adaptive budget tiers), requested rounds use `420` (~2× that observed max) so a hung reviewer pins the wave 3 minutes less. Adaptive rounds always use the full base value — `600`, or `1200` after the >5000-line doubling (doubling transforms the base; the small-diff reduction never applies to adaptive rounds). Explicit user values are never scaled or doubled |43| `report_path` | unset | File path; when set, the engine writes the final report there (creating parent dirs) and prints exactly that path as its last output line so downstream consumers can pick it up |4445`run_ordinal` is trusted invocation context, not a free-form user parameter.46Use `run_ordinal` only when it is a positive trusted value. Treat an absent47value or `unavailable` as local/degraded context and omit `RUN` from item IDs.48Keep UUID markers as the collision-resistant run identity. The ordinal adds49human-readable provenance; it does not replace the marker.5051### Parsing5253- Split the args on top-level commas; canonicalize each segment to a `(param, value)` pair — explicit `key: value` maps directly, plain-English forms per the table. Unrecognized segments are noted and ignored.54- **First occurrence wins** per param; later duplicates are dropped with `warning: user-supplied <param>=<value> ignored — earlier value wins`. Unset params take defaults.55- Presets assemble `<pins>, <user-args>, <overridable defaults>`: pins come first so first-occurrence-wins makes them authoritative; defaults come last so user args beat them.56- **Pin-rejection (presets; defense in depth against the prose parser being talked into overrides):** before assembling, segment-split `$ARGUMENTS` as above, lowercase each segment's key (the text before its first `:`), and abort with `error: <param> is pinned by <preset-name> and cannot be overridden in args` if any key equals a pinned param. Match segment *keys*, never raw substrings — `focus: "target: PR safety"` has key `focus` and must pass. A host that cannot segment-split must abort (`error: pin-rejection unavailable in this environment; refusing to invoke engine without pin enforcement`), not degrade to substring scanning.57- `agents`: a count (use the mix table below) or an explicit mix naming `codex`/`codex-exec` / `claude`/`claude-exec` — honor a mix exactly.58- `target` auto-detect order: currently checked-out PR (e.g. `gh pr view --json number,headRefName`, a GitHub MCP query, or REST) → branch-vs-main diff (`git diff main...HEAD`) → uncommitted changes (`git diff` + `git diff --cached`). Empty args = all defaults.59- `rounds` is the requested count. Resolve `max_rounds` after `rounds`, the final `commit_mode` (including the PR-locator rule below), and the target: the `per_fix` default is `min(max(6, rounds), rounds + budget)`, where the adaptive budget scales with the target's changed-line count (added+removed in the materialized diff; materialize it once at resolution — `per_fix` targets are always local git diffs): under ~200 lines → `1`, up to ~1000 → `2`, above → `3`. The `min()` against the legacy `max(6, rounds)` cap makes the scaling a pure reduction: runs with `rounds >= 6` gain nothing. Default to `rounds` for `commit_mode=none`; reject `max_rounds < rounds`. Phrases like "allow one extra round" set `max_rounds=rounds+1`, and explicit caps like `max_rounds: 4` or "up to 4 rounds" set the cap directly. Phrases like "continue if needed" restore the legacy `max(6, rounds)` cap unless paired with an explicit cap. Phrases that constrain the round count itself — "exactly 3 rounds", "only 3 rounds", "3 rounds only", or "no extra rounds" — force `max_rounds=rounds`. Do **not** treat `only:` focus syntax or severity gates like "fix only critical" as exact-round requests.60- Adaptive continuation is on by default for `per_fix`. A plain "3 rounds" means `rounds=3` with `max_rounds` between `4` and `6` by diff size, so the organizing agent may continue after round 3 if §6 says another pass is justified. Use "exactly 3 rounds", "only 3 rounds", "no extra rounds", or `max_rounds: 3` when the run must stop at the requested count.61- If `commit_mode=none` and the user explicitly set `max_rounds > rounds`, warn and collapse `max_rounds` to `rounds`. Extra normal rounds review the same baseline, so use `rounds` for reviewer redundancy and `adversarial` for skeptical challenge.62- `reproduction=auto` and `reproduction=on` both run the selective batched reproduction gate in §3. `auto` may skip dispatch only when there are no candidates. `off` is allowed for speed, but the round summary and final report must say it was disabled; unconfirmed single-reviewer `medium`+ findings stay in Deferred unless the orchestrator independently reproduced them from code/tests/runtime evidence.63- `proof_runner` is unavailable unless an explicit user or system value supplies an absolute executable path outside the reviewed worktree. Never infer it from the reviewed repository, `PATH`, or a repository config file. Reject a configured relative, non-executable, or worktree-contained path before executable proof starts; `run-proof.sh` performs the final canonical-path and disposable-snapshot/proof-directory separation checks.64- `adversarial` applies only when `commit_mode=none`. If set with `per_fix`, warn and ignore it — productive mode already applies real fixes and gates them with the build/test command. Reject `adversarial_rounds > 2`; adversarial loops must be bounded. `auto` means choose the cheapest sufficient adversarial mode after normal synthesis using the default policy below.6566### PR-target / per_fix incompatibility6768If `target` is a GitHub PR locator (URL `https://<host>/<owner>/<repo>/pull/<N>`, slug `<owner>/<repo>#<N>`, or "PR #N"), the only valid `commit_mode` is `none`. Force it, and warn if the user asked for `per_fix`:6970> `warning: PR locators are read-only — forcing commit_mode=none (use 'target: branch' to fix-and-commit on your checked-out PR branch).`7172Reviewers of a PR locator see the GitHub-fetched diff, which may not match the local worktree; committing against a baseline the user can't see locally is unpredictable. Branch targets make the local tree the source of truth.7374### Commit modes7576- **`per_fix` (default)** — full loop: review → synthesize/reproduce/verify → apply fixes → build/test gate → commit, each round.77- **`none` (review-only)** — review → synthesize/reproduce/verify only. **No edits, no commits, no staging.** Read-only mode may write temporary prompt/reviewer/report artifacts under `.review-anvil/` and the explicit `report_path`; it must not modify source files, the index, commits, branches, or remotes. Every normal round reviews the same baseline, so `rounds > 1` buys reviewer redundancy, not code refinement; the natural default is `rounds=1`, and adaptive continuation is disabled by collapsing `max_rounds` to `rounds`. Skip Loop Mechanics §4 entirely; the round summary reads `Fixes applied: 0 (review-only)`; the auto-fix policy is still evaluated in the abstract so findings classify as would-apply / suggestions / deferred. Optional adversarial review is a separate post-synthesis gate that attacks finding validity and fix proportionality without pretending code changed.7879### Posting reports externally8081The engine never posts anywhere. Downstream consumers set `report_path`, let the engine write a GitHub-ready PR report, and post after it returns — `review-anvil-pr` + its `pr-helper.sh` is the reference implementation.8283When `report_path` is set, optimize the report for a PR timeline reader, not for archival completeness. The per-round console output and reviewer artifacts are the transcript; the posted report is the decision summary plus the few findings that need action.84Adaptive continuation details belong in Run Details unless they change the review decision; do not paste per-round continuation reasoning into PR reports.8586### Examples8788- `Skill review-anvil` → 3 requested rounds, adaptive up to 4–6 total rounds by diff size, 2 codex + 1 claude, four-pillar focus, auto-detected target.89- `Skill review-anvil "5 rounds, 2 codex + 1 claude, focus: async correctness, target: PR #42"`90- `Skill review-anvil "3 rounds, max_rounds: 4"` → 3 requested rounds, then at most 1 adaptive round if the continuation policy allows it.91- `Skill review-anvil "1 round, only: security, target: src/auth/"`92- `Skill review-anvil "fix only critical"` → severity gate raised to `critical`; everything else surfaces as suggestions.93- `Skill review-anvil "target: PR #42, adversarial: auto"` → normal review first, then adversarial review only if the synthesized findings/fix plans need a validity or proportionality challenge.9495## Default Mix Policy9697When the user gives a count but no mix:9899| `agents` | Mix |100|---|---|101| 1 | 1 codex-exec |102| 2 | 1 codex-exec + 1 claude-exec |103| 3 | 2 codex-exec + 1 claude-exec |104| 4 | 2 codex-exec + 2 claude-exec |105| 5 | 3 codex-exec + 2 claude-exec |106| N | ~60/40 codex/claude split, codex gets the odd one |107108Rationale: codex-exec surfaces more issues per call in our usage, so it gets the larger share (and the `agents=1` slot).109110## Loop Mechanics111112Run the loop for the requested `rounds`, then continue adaptively up to113`max_rounds` only when §6 allows adaptive continuation. Within a round:114115### 1. Snapshot the target116117Capture the target's state at round start so all reviewers see the same input:118119- Non-PR targets (branch, uncommitted, path): materialize the diff with the appropriate `git diff …`.120- PR targets (always `commit_mode=none`): fetch the PR's diff via `gh pr diff <N> -R <owner>/<repo>` (or equivalent MCP/REST). The local worktree is irrelevant — reviewers see the PR as it exists on GitHub.121- Whenever PR context is available — a PR-locator target, or a preset that supplies it (`review-anvil-improve-pr` does, after `verify-checkout`) — fetch the PR title/body/base branch/file list too, then infer the PR's intended scope in one sentence (e.g. "performance optimization in annotation seeding", "left-sidebar UX reorganization"). Put that scope in every reviewer prompt. A finding is actionable only if the PR introduces/regresses it or if it directly undermines the PR's stated purpose. Obvious, high-confidence pre-existing defects may be mentioned, but only under a separate "Out-of-scope follow-ups" section — never as blockers or inline actionable review comments for the current PR.122- Likewise gather the complete **PR review history** before dispatch: when a preset supplied the ledger (improve-pr captures it at verify-checkout time), use that; for PR-locator targets fetch it via `pr-helper.sh history <host> <owner> <repo> <n>` (ships with `review-anvil-pr`; threads, review bodies, and fallback comments are paginated and retried once). Include the status-tagged ledger in every reviewer prompt (PR REVIEW HISTORY block): `open` threads, `resolved` threads, `outdated` anchors, summary-only `reported` findings, prior `deferred`/`outside`/`review-dismissed` items, and explicit local `suppressed` findings. Pending reviews are not shown to the author and are excluded. Before dispatch, semantically coalesce entries with the same root cause (summary wording changes do not create a second item) while retaining every source URL and strongest status. If history lookup fails after one retry, do not dispatch or post/approve a review that could ignore or duplicate prior feedback; for the read-only preset, `REVIEW_ANVIL_SKIP_DISMISSED=1` is the explicit degraded escape hatch and mechanically forces COMMENT.123- Note `git rev-parse HEAD` so the round summary can reference the exact baseline (informational-only for PR targets).124125### 2. Dispatch reviewers in parallel126127Before any Codex-backed dispatch in the run — reviewer, reproduction,128adversarial, clarity, or action-lock — resolve `codex` lazily if `CODEX_BIN` is129not already set. Resolve it from the trusted host environment to an absolute130canonical executable path. Reject a missing, relative, non-executable, or131reviewed-worktree-contained result before that dispatch. Never pin a132package-manager-specific path or resolve the binary from the reviewed133repository.134135#### In Claude Code (the primary host)136137**Use the Agent tool for `claude-exec` reviewers. Do NOT use `claude -p` via Bash — that path is for non-Claude hosts only.**138139- **`claude-exec`**: Agent tool, `subagent_type: "general-purpose"`, the assembled Reviewer Prompt as `prompt`, `run_in_background: true`. The Agent tool streams natively, has no `--max-turns` ceiling, and inherits the session environment.140- **`codex-exec`**: Bash through the wrapper: `REVIEW_ANVIL_REQUIRE_FINDINGS=1 bash <wrapper> .review-anvil/round<N>-<label>.md <reviewer_timeout> -- "$CODEX_BIN" exec -m gpt-5.6-luna -c 'model_reasoning_effort="max"' -c 'shell_environment_policy.inherit="all"' -c 'mcp_servers.webexapis.enabled=false' --ephemeral --sandbox read-only -C <project-dir> '<prompt>' < /dev/null`, with `run_in_background: true`. The validation flag makes the wrapper reject confirmation-only, plan-only, or otherwise incomplete responses that do not end with the required fenced findings block. `--ephemeral` prevents reviewer sessions from leaking into later dispatches. The `< /dev/null` is load-bearing: codex takes its prompt as argv and must not inherit an open stdin — the wrapper passes its stdin through (`<&0`, which the claude fallback needs), and codex blocking on a never-closing fd 0 is a known hang class from real runs.141- Send all M reviewers in a *single message* with multiple tool calls. The harness notifies you on completion; do not poll.142143#### In Codex CLI or other hosts without the Agent tool144145- **`claude-exec`**: write the assembled prompt to a file, then:146147 ```bash148 REVIEW_ANVIL_REQUIRE_FINDINGS=1 bash <wrapper> .review-anvil/round<N>-<label>.md <reviewer_timeout> -- \149 claude -p --max-turns 100 --no-session-persistence \150 --permission-mode dontAsk --output-format text \151 --tools "Bash,Read,Glob,Grep" \152 --allowedTools "Bash(git:*)" "Read" "Glob" "Grep" \153 < .review-anvil/round<N>-<label>.prompt.md154 ```155156 `--tools` restricts the built-in tool set; `--allowedTools` auto-approves the listed safe tool uses and is variadic, so the prompt MUST arrive via stdin (the wrapper passes its stdin through). `--permission-mode dontAsk` keeps the fallback non-interactive by denying anything outside the allowed/read-only path. **Do not size `--max-turns` to the task** — task-sized caps keep biting (20 was hit in production), and a reviewer that hits the cap loses its entire output. The wrapper's wall-clock timeout is the real bound; `100` is a runaway backstop that should never bind.157158- **`codex-exec`**: same validation-enabled wrapper around `"$CODEX_BIN" exec -m gpt-5.6-luna -c 'model_reasoning_effort="max"' -c 'shell_environment_policy.inherit="all"' -c 'mcp_servers.webexapis.enabled=false' --ephemeral --sandbox read-only -C <project-dir> '<prompt>' < /dev/null` — stdin from `/dev/null` here too.159- Launch all M wrapper invocations as background shell processes and `wait`.160161#### Bash-dispatched reviewers MUST go through `run-reviewer.sh`162163Every shell-dispatched reviewer (codex-exec everywhere; claude-exec outside Claude Code) runs under `scripts/run-reviewer.sh` (next to this SKILL.md). **Never background a bare `claude -p ... > out.md 2>&1` and wait on the file** — in text mode nothing prints until the final answer, so a hung reviewer and a working one are both a 0-byte file (a production run waited on exactly that for many minutes). The wrapper:164165```166run-reviewer.sh <out_file> <timeout_seconds> -- <command> [args...]167```168169- Hard wall-clock timeout (`reviewer_timeout`, default 600s): TERM at the deadline, KILL 30s later.170- Captures exit status; stderr goes to `<out_file>.err` (kept for diagnosis).171- Prints one classification: `STATUS=ok` | `timeout` | `empty` (exit 0, nothing written) | `protocol` (normal-review output did not end with a complete fenced findings block) | `failed` (+ `EXIT_CODE=<n>`).172173Treat any STATUS other than `ok` as a failed reviewer (see Failure handling), with the tail of `.err` as the reason. `protocol` gets the one corrective retry defined there before it becomes a failure. Set `REVIEW_ANVIL_REQUIRE_FINDINGS=1` only for normal reviewer waves; reproduction and adversarial prompts have different output schemas. Reviewer output/prompt files live under `.review-anvil/`; clean them up after the round's synthesis.174175**Host tool timeouts must outlive the wrapper.** Any host Bash call that can block on a reviewer — the background-and-`wait` fallback above, the serial last resort, or an inline replication of the wrapper contract — must set the Bash *tool's* own timeout to at least `reviewer_timeout + 90` seconds (wrapper deadline + 30s TERM→KILL grace + margin). Host defaults are far lower (Claude Code's is 120s) and SIGKILL a healthy wait mid-review; the kill then masquerades as a reviewer failure and silently burns that reviewer's lens coverage. On hosts with background dispatch (`run_in_background`), never block a foreground call on a reviewer at all. If the host caps tool timeouts below `reviewer_timeout + 90`, prefer detached dispatch plus short non-blocking status checks over shrinking the reviewer budget; reducing `reviewer_timeout` is a last resort, floored at 300s and forbidden for >5000-line diffs (their timeout is deliberately doubled).176177**Resolving the wrapper and `references/` files** — same trusted-root rule as `pr-helper.sh`: see review-anvil-pr SKILL.md step 1 ("Resolve the helper script"). Host-exposed skill path or user-level install roots only; never project-scoped/worktree-local skill dirs (writable by the repo under review). If no trusted copy of the wrapper resolves, replicate its contract inline (background, kill at deadline, check exit status, empty output = failure) rather than falling back to a bare redirect.178179After changing the wrapper contract or dispatch examples, run180`scripts/test-run-reviewer.sh` alongside the reproduction and PR helper tests.181182#### Last resort183184If parallel dispatch is genuinely impossible (no Agent tool, no background bash), fall back to serial invocation and **say so in the round summary** — serial reviewers see the baseline at different wall-clock times; it's a degraded mode, not the design.185186The `codex-exec` and `claude-exec` skills document the same recipes from the reviewer side; the canonical dispatch lives here.187188### 3. Synthesize189190When all reviewers return:191192- **Dedup** on `(file, line, root cause)` when present, else `(area, root cause)`. Keep the highest-severity instance, record which reviewers raised it, and keep divergent anchors as `file_alternates: [...]`.193- **Group** by severity (`critical` → `nit`), then topic.194- Unparseable reviewer output: pass the prose through as "unstructured" findings in a separate section; no retry.195196#### Verify and reproduce findings before acting on them197198Plausible-but-wrong findings are the dominant failure mode of LLM review, and both downstream actions are expensive: a bogus fix commit pollutes the branch, a bogus finding posted to a PR burns the author's trust. After dedup:199200- **Prior-feedback check first (orchestrator judgment).** Compare every merged finding against PR REVIEW HISTORY *semantically* — same root cause counts even when wording differs. Revalidate `open`, `resolved`, and summary-only `reported` items against the current head. An open item that remains real is a carry-forward finding and must retain its effect on severity/approval, but must not create a duplicate inline thread; a resolved item means only that GitHub discussion was closed, not that the code was proven fixed. Record a still-present resolved item as `resolved-but-still-present` in the summary and do not create a new inline thread. Items now fixed/stale become one-line status notes. Explicit local `suppressed` items are never auto-fixed or posted as actionable findings, but remain as compact status-only audit rows. Keep `author-resolved` items in PR REVIEW HISTORY for reviewer context. After synthesis and dedup, drop semantic matches to `author-resolved` items before building reproduction candidates. Exception: retain a finding when the reviewer explicitly set `prior_feedback: reintroduced` for a distinct new instance with new evidence. Do not report, post, auto-fix, or let ordinary `author-resolved` matches affect approval. A reintroduced finding remains actionable; it affects approval only at `critical` or `high` severity. The post-time helper catches near-verbatim repeats (exact path + text similarity ≥ 0.9) as a deterministic duplicate-thread backstop.201- **Scope/artifact filter next (orchestrator judgment).** Drop or move to out-of-scope follow-ups before reproduction when the claim is about archived design notes, changelogs, old migration examples, generated fixtures, vendored files, or historical docs that are not the review's live product surface. Do not spend verifier budget proving historical provenance is stale. Conversely, live docs that users rely on — README usage, CLI help, API docs, config reference, plugin metadata, or marketplace copy — are product surface and may become reproduction candidates when they drift from code/runtime behavior.202- **Assign provenance IDs.** Assign IDs after semantic deduplication, prior-feedback classification, and scope filtering, and before reproduction or adversarial dispatch. Use this output recipe:203204 ```text205 PR finding: `RAV-RUN<run>-R<origin-round>-F<ordinal>`206 PR plan: `RAV-RUN<run>-R<origin-round>-P<ordinal>`207 Local: `RAV-R<origin-round>-F<ordinal>` / `RAV-R<origin-round>-P<ordinal>`208 ```209210 Canonical grammar: `RAV-(RUN<run>-)?R<origin-round>-(F|P)<ordinal>`.211 `run` and `origin-round` are unpadded positive base-10 integers. Encode212 `ordinal` from an unpadded positive base-10 integer by left-padding only to a213 minimum width of three digits: encode 1 as `001`, 10 as `010`, 100 as214 `100`, and 1000 as `1000`; do not add any other leading zero. Valid215 multi-digit examples are216 `RAV-RUN12-R10-F010` and `RAV-R12-P1000`. Invalid forms include217 `RAV-RUN03-R2-F001`, `RAV-RUN3-R02-F001`, `RAV-RUN3-R2-F0001`,218 `RAV-RUN0-R2-F001`, `RAV-RUN3-R0-F001`, and `RAV-RUN3-R2-F000`.219220 ID legend: `RUN` is the observed PR review run, `R` is the immutable origin round, `F` is a finding, and `P` is a plan. Examples are `RAV-RUN3-R2-F001`, `RAV-RUN3-R2-P001`, `RAV-R2-F001`, and `RAV-R2-P001`.221 Cross-round allocation example: in trusted PR run 3, a history finding with222 `id=RAV-RUN1-R2-F007` keeps that ID. A new round-1 finding receives223 `RAV-RUN3-R1-F001` and keeps it when re-raised in round 2. The next new224 round-2 finding receives `RAV-RUN3-R2-F002`. A round-2 plan covering those225 two current-run findings receives `RAV-RUN3-R2-P001`. The matching226 local/degraded allocation for the same newly allocated items has no history227 carry-forward: `RAV-R1-F001`, `RAV-R2-F002`, and `RAV-R2-P001`.228 The carried `RAV-RUN1-R2-F007` has no new inline comment. It remains229 available in history, the report, reproduction, and adversarial review.230231 Finding and plan ordinals are independent, run-wide counters named232 `next_finding_ordinal` and `next_plan_ordinal`. Both start at `001`.233 Counters never reset each round and never reuse gaps. Before assignment,234 order findings by priority, normalized path, line, and topic; order plans by235 covered-finding priority, area, and subject.236237 A finding's origin round is the first normal review round that raises it. A238 plan's origin round is the normal review round in which its concrete fix239 group is first assembled.240 The origin round never changes after confirmation, refutation, priority change, deferral, fix, or re-raise.241 Reproduction and adversarial passes are not rounds.242243 Carried findings consume the exact `id=` value supplied in `PR REVIEW HISTORY`;244 use that complete ID unchanged without advancing a current-run245 counter. For an actionable history entry that has only `legacy=`,246 use the `legacy=` value as a source alias and assign the next canonical ID. Do not247 assign a new ID to a non-actionable legacy-only entry. Historical `RAVF###`, `RAVW###`, `F-###`, and `W-###` forms are migration/read-boundary aliases only. New findings and plans always use the recipe above.248249 Once assigned, keep the complete ID unchanged in reproduction candidates,250 adversarial targets, report rows, plan coverage, and later round references.251 ID reuse flows to an inline body only when the finding is otherwise eligible252 for a new inline. Ordinary `open`, `resolved`, and `reported` carry-forwards253 retain their IDs in history, reports, reproduction, and adversarial targets,254 but do not create a new inline thread.255 Already-assigned in-scope low/nit and set-aside findings keep their complete canonical F IDs.256 Truly unassigned out-of-scope follow-ups stay distinct and do not receive an F ID.257- Build `REPRODUCTION CANDIDATES` after prior-feedback classification and ID assignment:258 - every `medium`+ finding raised by exactly one reviewer,259 - every `medium`+ deletion/dead-code/unused/redundant-code/simplification finding, and any deletion/simplification that would remove runtime code, public docs/API, compatibility behavior, or another high-blast-radius surface, regardless of reviewer count,260 - every `critical`/`high` finding whose evidence is mostly inferred from a hunk rather than confirmed from code/runtime context,261 - every finding the orchestrator is materially uncertain about after reading the cited files.262- When `reproduction=auto` or `on` and candidates exist, run the two batched verifier passes defined in `references/reproduction-prompt.md`. Do not spawn one verifier per finding unless the batch is too large to fit in one prompt. Both passes run backgrounded under the Concurrency section's deadline rule — never an unbounded foreground wait.263 1. Dispatch `MODE=AUTHOR`. Validate its final `proofs` block and referenced `proof-file` blocks exactly as the reference requires. Retain rejected material instead of repairing model-supplied proof code.264 2. For each valid executable manifest, materialize the bundle in the host-created private proof root and create the exact disposable snapshot. If `proof_runner` is unavailable, retain the bundle without execution. Otherwise invoke the trusted engine-root `run-proof.sh` once for that manifest; never execute the probe directly or use a runner from the reviewed repository. Remove the disposable snapshot after the runner completes.265 3. Dispatch `MODE=VERDICT` with the validated static evidence and each retained bundle's runner output or explicit unavailable/failed state. This pass returns `confirmed`, `refuted`, `unclear`, `narrowed`, or `downgraded` for the supplied complete canonical finding IDs only and returns each ID unchanged.266- Apply reproduction verdicts before auto-fix/reporting:267 - `confirmed` and `narrowed` findings may remain actionable, with narrowed wording when supplied.268 - `downgraded` findings re-enter the normal severity gates after changing severity.269 - `refuted` findings are dropped from final Findings (or, if useful for transparency, one-line Deferred notes).270 - `unclear` findings move to Deferred with `We set this aside because <plain-language description of the missing proof>.` Rewrite the verifier's reason; do not copy it.271- Findings raised independently by **2+ reviewers** and not listed as reproduction candidates may skip batched reproduction; consensus is the signal (this is why dedup records who raised what). Still open enough code/context before destructive action to ensure the fix path is coherent.272- **Deletions ("delete this"/dead/unused/redundant) require reproduction plus execution when `per_fix` applies the cut** — the highest-blast-radius, highest-false-positive class. In `per_fix`, after reproduction confirms the cut, apply it and run the full test suite: a **red gate means keep it**. A green gate is necessary but not sufficient (it only proves *test-covered* behavior), so the reproduction/skeptic pass must also look for a concrete reason the code must stay, visible in the diff (trust boundary, aliasing copy, ordering, back-compat, dedup, edge semantics — or another specific contract). The two cover different blind spots: the gate catches callers the skeptic can't see; the skeptic catches behavior no test exercises. Block **only** on a red gate or a specific skeptic refutation — not on generic "there might be an unseen caller" (that's what the gate tests). Read-only mode has only the skeptic. Blocked → **Deferred** (`We set this aside because the code is still needed — <what>`).273- If `reproduction=off`, say so in the round summary and final report. Required reproduction candidates — including single-reviewer `medium`+ findings, deletion/high-risk findings, and orchestrator-uncertain findings — cannot become actionable unless the orchestrator independently reproduces them from code/tests/runtime evidence; otherwise move them to Deferred with `We set this aside because the needed check was not run.`274- `low`/`nit` findings skip verification: they're below the auto-fix gate and surface as suggestions either way.275276Canonical examples for where reproduction helps and where it must stay out of277the way live in `references/reproduction-examples.md`. After changing this278policy, the reproduction prompt, or the proof-runner boundary, run279`scripts/test-reproduction-policy.sh` and `scripts/test-run-proof.sh` alongside280the PR helper tests.281282#### Approving out-of-scope follow-ups283284A pre-existing issue outside the PR's scope can still be worth noting, but it must not become an inline/blocking PR finding. Classify each out-of-scope follow-up:285286- **Auto-approved follow-up** — create/queue separate work when all are true: severity is `critical`/`high` (or clearly reproducible `medium`), the bug is confirmed from code/tests/runtime evidence, it is not a product decision/style preference, it is not already tracked in prior PR feedback or explicitly suppressed, and the fix is plausibly separable from the current PR.287- **Needs human triage** — mention only as a non-blocking follow-up when the issue is real but severity/ownership/product intent is ambiguous.288- **Do not surface** — drop if speculative, low/nit, a product decision, already dismissed/tracked, or only discoverable by reviewing unrelated code paths deeply.289290When `report_path` is set, write follow-ups once, after the final round, to `<report_path>.followups.json` — schema in `references/report-artifacts.md`. Automation may file issues only for `auto_approved` entries after duplicate search; presets read the file before posting (the helper deletes it afterwards).291292#### Optional adversarial review (`commit_mode=none` only)293294When `adversarial` is not `off`, run a bounded post-synthesis gate after295dedup/reproduction and before writing the final report artifacts. Read296`references/adversarial-prompt.md` before dispatching adversarial reviewers.297Dispatch every adversary of the selected mode in parallel — one message,298multiple background tool calls, exactly like §2 reviewers, under the299Concurrency section's deadline rule — and synthesize verdicts when all return300or the deadline fires. Never await one adversary before launching the next.301302Adversarial review is not another broad review pass and not a simulated patch303application. It attacks the candidate synthesis:304305- **Finding validity** — false-positive claims, wrong anchors, dismissed306 findings, out-of-scope issues, over-severity, and missing reachability307 evidence.308- **Fix proportionality** — suggested fixes that would technically address a309 problem but create more trouble than they solve: harmful blast radius,310 unnecessary dependencies, bloated abstractions, future tech debt, non-local311 churn, unsafe deletions, brittle tests, or symptom fixes that miss root cause.312- **Report safety** — unsafe one-click GitHub suggestions, unclear fix paths,313 overconfident approvals, and actionable comments that should be deferred.314315Modes:316317| Mode | Dispatch | Intent |318|---|---|---|319| `auto` | Chosen after synthesis | Selects `off`, `challenge`, `targeted`, or `strict` using the default policy below. |320| `challenge` | 1 adversary | Cheap local check over all `medium`+ findings and would-apply plans. |321| `targeted` | 2 adversaries | Recommended PR mode: false-positive/scope auditor + fix-plan breaker. Force a deletion skeptic when any would-apply item removes code. |322| `full` | 3 adversaries | Adds second-order bug hunting across interacting plans, config, migrations, and tests. |323| `strict` | Same as `full` | Approval-sensitive: any required adversary failure or unresolved `high`+ dispute forces COMMENT. |324325Role mapping:326327- `challenge`: one combined adversary using the core prompt plus both the328 `false-positive-scope-auditor` and `fix-plan-breaker` role additions.329- `targeted`: two adversaries, one `false-positive-scope-auditor` and one330 `fix-plan-breaker`; add/replace with the deletion skeptic behavior from331 `fix-plan-breaker` when any would-apply plan removes code.332- `full`/`strict`: `false-positive-scope-auditor`, `fix-plan-breaker`, and333 `second-order-bug-hunter`; add `report-auditor` only if the report/approval334 artifact itself is the risky surface.335336Default policy:337338- Local `review-anvil-readonly` defaults to `off`. If the user asks for careful,339 skeptical, high-confidence, low-noise, or thorough read-only review, the340 orchestrator should append `adversarial: auto` unless the user explicitly341 asked for a fast/rough pass.342- `review-anvil-pr` defaults to `adversarial: auto` because GitHub output is343 public reviewer speech and may include inline comments, one-click344 suggestions, or an approval event.345- Explicit user input wins: `adversarial: off` disables the gate; explicit346 `challenge`/`targeted`/`full`/`strict` uses that mode. In review-only PR runs,347 explicit `adversarial: off` also forces `.approval.json` to `{"event":348 "COMMENT"}`; unchallenged LLM review should not satisfy branch protection by349 accident.350351`auto` selection after normal synthesis:352353- First estimate **meaningful changed size** from the reviewed snapshot. Exclude354 generated/vendor/build artifacts, lockfiles, and snapshot/fixture churn unless355 those files are the review's product surface. Treat `>1000` meaningful changed356 lines or `>20` meaningful files as large, and `>5000` meaningful changed357 lines, `>50` meaningful files, or several interacting subsystems as very358 large. Size is an escalation floor, not the only signal: a small risky auth or359 migration diff can still choose `targeted`, while a huge mechanical rename360 may stay below `full` after exclusions.361- Use `off` only when approval is disabled/impossible and the result is clean362 or low/nit-only, has no `medium`+ inline comments, no GitHub suggestion363 blocks, no `critical`/`high` actionable or deferred author-action items, and364 no would-apply plan with deletion, dependency, non-local behavior, or365 abstraction/churn risk. For local non-PR runs, ignore the approval condition.366- Use `challenge` for small or self-authored comment-only reviews with material367 feedback but no suggestion blocks, no high-risk fix plans, and `approve:368 never` / `REVIEW_ANVIL_NO_APPROVE=1`.369- Use `targeted` when candidate output includes any `medium`+ inline comment,370 any Git371372…(truncated)