Run
Multi-agent code review for Claude Code. Multiple AI models review your PR, code, or plan independently; a cross-family refutation pass and an active judge then distill the findings into a curated, severity-ranked list of what actually needs changing.From its SKILL.md
npx -y skills add deployhq/review-council --skill runAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
92.1 KB, as published. Nobody here has run it
Review Council — Multi-Agent Review
You are the Orchestrator of a review council. Your job is to run multiple AI reviewers independently, route their findings through a cross-family refutation pass, and have the judge synthesize a single curated, severity-ranked list of findings.
Step 0: Read Config & Detect Available Providers
Two independent gates decide the reviewer roster: configuration (which reviewers/lenses are enabled, from the config files) and detection (which reviewers are available on this machine). A reviewer participates only if it is both enabled and available. Do 0.1 and 0.2, then reconcile in 0.3.
0.1 Read the effective configuration
Run the bundled config reader and capture its key=value output. It reconciles .review-council/config.yml, .review-council/config.local.yml, RC_* env vars, and built-in defaults — precedence env > config.local.yml > config.yml > built-in default — and prints the effective config to stdout (diagnostics to stderr). It always exits 0: absent files or absent yq degrade to defaults. See rules/config.md for the full schema.
# Read from the TARGET repo's .review-council/ (the CWD where the review runs).
# ${CLAUDE_PLUGIN_ROOT} is the plugin's own install dir — must be double-quoted.
RC_NOTES="$(mktemp "${TMPDIR:-/tmp}/rc-config-notes.XXXXXX")"
CONFIG_OUT="$("${CLAUDE_PLUGIN_ROOT}/scripts/rc-config.sh" .review-council 2>"$RC_NOTES")"
printf '%s\n' "$CONFIG_OUT"
# Surface any reader diagnostics (malformed keys, yq-not-found, skipped files):
[ -s "$RC_NOTES" ] && { echo "--- rc-config notes ---"; cat "$RC_NOTES"; }
rm -f "$RC_NOTES"
Echo the effective, reconciled config to the user before applying it. This printed block is the observable artifact of what the run resolved — show what you resolved, never silently eyeball the YAML. Parse the key=value lines (one per line, no spaces around =; # lines are section comments). The keys are:
reviewer.<p>.enabled/reviewer.<p>.modelforpinclaude,codex,google,perplexity.lens.<l>.enabled/lens.<l>.providersforlinsecurity,correctness,cross_file,performance,design,dependency— pluslens.security.replaces_dedicated.settings.<k>forpersonas,verify,verify_max_findings,learn,min_reviewers,reviewer_timeout_seconds,run_budget_seconds,auto_retry,health_probe,health_probe_timeout_seconds,claude_max_turns.static_analysis.<k>forenabled,tools,timeout_seconds,semgrep_config— the deterministic static-scan layer consumed in Step 2.5. (An older reader that does not emit a# static_analysissection → fall back to the defaults:enabled=true,tools=gitleaks,trufflehog,osv-scanner,semgrep,ruff,shellcheck,actionlint,hadolint,timeout_seconds=60,semgrep_config=p/default.)pr_comments.<k>forenabled(defaultfalse) andbot_token_env(defaultRC_PR_BOT_TOKEN) — the optional PR-digest posting engine consumed in Step 6.6. (An older reader that does not emit a# pr_commentssection → fall back toenabled=false,bot_token_env=RC_PR_BOT_TOKEN, i.e. posting off = today's report-only behavior.)
If yq is missing, the reader prints a yq not found note and falls back to defaults + env; the run proceeds normally (config files are simply ignored). rules/config.md documents the one-time brew install yq (mikefarah v4) needed to use config files.
0.2 Detect available providers
Probe which reviewers are available on this machine. Refer to rules/providers.md for detection methods.
Run this detection command verbatim — do not hand-roll or abbreviate it. The agy probe is the one most often dropped when detection is improvised, which silently collapses the Google slot to a gemini that cannot authenticate on Google Workspace accounts (IneligibleTierError: DASHER_USER). agy is the default Google reviewer and MUST be probed explicitly — including its known install path (~/.local/bin/agy), in case it isn't on PATH:
# agy: probe PATH first, then common install dirs (a minimal PATH may omit them)
AGY="$(command -v agy 2>/dev/null || true)"
if [ -z "$AGY" ]; then
for d in "$HOME/.local/bin" /opt/homebrew/bin /usr/local/bin; do
[ -x "$d/agy" ] && { AGY="$d/agy"; break; }
done
fi
GEM="$(command -v gemini 2>/dev/null || true)"
CDX="$(command -v codex 2>/dev/null || true)"
# one key=value per line — never space-join (a $HOME with a space would break parsing)
echo "codex=${CDX:-none}"
if [ -n "$AGY" ]; then
echo "google=antigravity"
echo "agy=$AGY"
echo "gemini_fallback=${GEM:-none}"
elif [ -n "$GEM" ]; then
echo "google=gemini"
echo "gemini=$GEM"
else
echo "google=none"
fi
echo "perplexity=$([ -n "$PERPLEXITY_API_KEY" ] && echo set || echo unset)"
Interpret the output (each value is on its own line — read the whole line as the value, so paths containing spaces stay intact):
- Claude: Always available.
- Codex:
codex=<path>→ available (CLI). Ifcodex=none, check whether themcp__codex__codextool is available — if so, available (MCP); otherwise unavailable. - Google (Antigravity / Gemini) — one slot shared by both Google CLIs (see
rules/providers.md→ "Google-family reviewer"):google=antigravity→ available as Google (Antigravity).agyis primary — invoke it via the resolvedagy=<path>;gemini(fromgemini_fallback=<path>) is fallback only. Announce it as "Google (Antigravity)", never "Gemini", becauseagyis what will actually run.google=gemini→ available as Google (Gemini). Note thatgeminiis ineligible for Workspace/Dasher Google accounts and may fast-fail auth (IneligibleTierError). If the user's account is a Google Workspace / managed domain, double-check thatagytruly isn't installed (e.g. in a dir the probe missed) before accepting a gemini-only slot — agoogle=geminiverdict there usually means theagyprobe missed it, which is the exact silent collapse this detection is meant to prevent.google=none→ unavailable. Never count this as two reviewers. Pass the resolved primary tool and its path, plus the fallback tool and its path, to the Google reviewer subagent.
- Perplexity:
perplexity=set→ available;unset→ unavailable.
0.2.1 Optional health probe (gated on settings.health_probe)
Default off. If settings.health_probe is false (the default, resolved in 0.1), skip this step entirely — the roster is exactly what detection found, byte-identical to today's behavior. Set it true only when you want available/min_reviewers to reflect usable rather than merely installed providers, accepting that each probe costs up to settings.health_probe_timeout_seconds (default 20s) of wall-clock plus a real (trivial) provider call.
When settings.health_probe is true, probe the two CLI slots that route through the provider script — Codex and Google only (the native Claude/Security reviewers are always-available and never probed; Perplexity is a curl API with no probe binary). For each such slot that detection found available as a CLI (an MCP-only Codex has no binary to probe — skip it), run the script's --probe mode with the same resolved paths you'd pass in Round 1, on the invocation:
# Codex slot (CLI only): fallback positional is empty
RC_HEALTH_PROBE_TIMEOUT=<effective settings.health_probe_timeout_seconds> \
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" --probe "<codex-path>" ""
# Google slot: agy primary, gemini fallback (or gemini primary + "" when agy is absent)
RC_HEALTH_PROBE_TIMEOUT=<effective settings.health_probe_timeout_seconds> \
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" --probe "<agy-path>" "<gemini-path-or-empty>"
The probe prints exactly one verdict line — HEALTHY, UNHEALTHY: <reason>, or INCONCLUSIVE. Act on it fail-open:
UNHEALTHY: <reason>— positive hard-fail evidence (auth / terminal quota / overload on every probed tool in the slot). Drop the slot from the roster for this run, exactly as if detection had found it unavailable; carry the<reason>into the Step-0 announcement (e.g. "Skipped: Codex (health probe — quota exhausted)").HEALTHYorINCONCLUSIVE— keep the slot. INCONCLUSIVE means cold/slow/empty/network-blip/rate-limited, never a health failure on its own — a cold-but-usable provider (agy cold start) must never be dropped, and neither must one that merely hit a per-minute throttle during the probe.
A slot dropped here then flows through the settings.min_reviewers reconciliation in 0.3 identically to an undetected provider.
0.3 Apply the configuration to the roster
Reconcile the config (0.1) with detection (0.2):
- Roster (reviewers). Drop any provider whose
reviewer.<p>.enabled=false— it does not participate even if installed. Of the reviewers that remain enabled, those that detection found available make up the participating roster. Config gates the roster; detection gates availability — both must pass. The Google slot counts as ONE reviewer towardsettings.min_reviewersno matter how many Google CLIs are installed: it is a single dispatch unit (onerc-invoke-provider.shcall → one result), andagy↔geminiis an internal fallback, never two reviewers (see Step 3 → Reviewer: Google). - Models. Where
reviewer.<p>.modelis non-empty, pass that model to that reviewer's invocation (e.g. the Google slot's model, or the Perplexity model — defaultsonar). An empty model means "use the tool's own default" — pass nothing. - Lens bindings (record for Round 1). Record each
lens.<l>.enabledandlens.<l>.providers(auto, or a comma-joined provider list). Step 3 (Lens Assignment) consumes these to pin lenses to providers — here in Step 0 you only read and record the bindings. Notelens.security.replaces_dedicated: whentrue, the pinnedsecurity.providersreplace the dedicated security reviewer (do not run both); whenfalse, security stays on its default/autopath. - Settings. Load the
settings.*values for this run and use them wherever the orchestration rules reference a run knob (min_reviewers,reviewer_timeout_seconds,run_budget_seconds,auto_retry, etc.). The reader already folded anyRC_*env override into each value at the correct precedence, so treat each resolvedsettings.*as the effective value of itsRC_*knob. When a later step consumes anRC_*env var (e.g. the reviewer timeout wrapper), pass that effective value on the invocation — e.g.RC_REVIEWER_TIMEOUT=<effective settings.reviewer_timeout_seconds> <cli> …— rather than relying on the ambient environment carrying across separate tool calls. Seerules/orchestration.md→ "Run Settings". - Absent config / absent
yq→ today's defaults, byte-identical to pre-config behavior. Disabling reviewers still honorssettings.min_reviewers: if too few remain to reach it, the existing min-reviewers handling applies (single-reviewer mode or the usual prompt).
Announce: "Review Council — [N] reviewers available: [list]. [Skipped: reason for each unavailable or config-disabled provider]". When the Google slot is available, name the actual tool — e.g. "Google (Antigravity)" — so it's clear agy (not gemini) is the one running. Also state the running plugin version on this line — read it from this skill file's own path (…/review-council/<version>/skills/run). A long-lived session can keep running a stale cached copy until /reload-plugins; if the announced version looks older than expected (e.g. posting is enabled in config but this copy predates that feature), tell the user to run /reload-plugins and re-run before trusting the result.
Posting commitment — state it as a pending action HERE. If Step 0 resolved pr_comments.enabled=true, say so now, as an explicit pending action for this run, using the words offer and wait for your approval (do not paraphrase the approval away) — e.g. "PR-comment posting is ON for this repo → after the report I will offer to post the review digest and wait for your approval (Step 6.6)." A pending action stated early is the form a compaction summary is most likely to keep, so the commitment can survive even if the tail of this skill is later summarized away. Treat Step 6.6 as a stage you must reach and offer, never one you may silently drop at the report — but "reach and offer" never means post without the human gate (Step 6.6 never auto-posts). A summary can still drop this, so do not rely on memory: re-resolve pr_comments.enabled at report time (see the end of Step 6).
If only Claude-family reviewers (Claude + the dedicated Security reviewer) are available — i.e. no different-family reviewer to cross-verify against — proceed in single-reviewer mode and note it in the output. Suggest running /review-council:setup to see how to add more reviewers.
Step 0.5: Recall Learnings
Recall the team's shared learnings so past decisions shape this run. Gated on settings.learn (resolved in Step 0): if settings.learn is false, skip this step entirely.
If settings.learn is true, read .review-council/learnings.md from the target repo (the CWD where the review runs, alongside .review-council/config.yml). If the file is absent, skip silently — no warning, no error. A missing file is the normal case. The file has two sections (format in rules/config.md → learnings):
- Conventions — project-specific rules about what not to flag (e.g. "Migrations are auto-generated; do not flag missing down-migrations"). Fold this section into the Step-2 baseline context package (see Step 2) under a clear "Team Learnings — Conventions" heading. Because it rides the shared package, it is injected once and reaches every reviewer — do NOT paste it separately into each dispatch.
- Suppressions — known false positives keyed by fingerprint. Hold this section for the judge synthesis step (Step 5). It is not injected into reviewer prompts; carry it forward so the judge can suppress matching findings (see Step 5, §recalibration).
Step 1: Detect Review Target
Analyze the user's input: $ARGUMENTS
Auto-detection rules (in order):
- Number (e.g.,
42,#42) — PR review. Fetch withgh pr view <number> --json title,body,baseRefName,headRefName,changedFiles,additions,deletions,reviewDecision,comments,reviewsandgh pr diff <number>. - Path to
.mdfile insidedocs/,plans/,adr/, or similar documentation directory — Plan/document review. Read the file and any documents it references. - Path to source code (file or directory) — Code review. Read the files. If a directory, identify the key files (skip node_modules, dist, etc.).
- No argument — Auto-detect:
a. Check for open PR on current branch:
gh pr view --json number,title,body,baseRefName,headRefName,changedFiles 2>/dev/nullb. If PR found — PR review c. If no PR — check for staged changes:git diff --cached --statd. If staged changes — Code review of staged diff e. If nothing staged — check unstaged:git diff --statf. If unstaged changes — Code review of working changes g. If nothing — ask the user what to review
Announce your detection: "Review Council — Reviewing [PR #42: title | plan: path | code: path | staged changes]"
Step 2: Gather Context — Baseline Context Package
Collect context appropriate to the detected type. This becomes the baseline context package — the identical context every reviewer receives. Gather mechanically using exact commands; do not summarize or interpret.
PR review:
- PR metadata:
gh pr view <number> --json title,body,baseRefName,headRefName,changedFiles,additions,deletions,reviewDecision,comments,reviews - Full diff:
gh pr diff <number> - List of changed files with additions/deletions
- Git log for changed files:
git log --oneline -10 -- <changed_files> - Git blame for changed hunks:
git blame -L <start>,<end> -- <file>for each changed hunk - Any existing review comments or discussions
- Project conventions: raw contents of CLAUDE.md, CONTRIBUTING.md, README (if present)
Plan/document review:
- Full document content
- Any documents referenced or linked (ADRs, related plans)
- Project conventions: raw contents of CLAUDE.md, CONTRIBUTING.md, README (if present)
Code review:
- Full file contents (or diff if reviewing changes)
- Git log for changed files:
git log --oneline -10 -- <files> - Git blame for changed hunks
- Project conventions: raw contents of CLAUDE.md, CONTRIBUTING.md, README (if present)
Team Learnings — Conventions (from Step 0.5). If Step 0.5 recalled a Conventions section, append it here under a "Team Learnings — Conventions" heading. Folding it into this shared package (rather than each dispatch) injects it once yet reaches every reviewer. (The Suppressions section is NOT included here — it is held for the judge synthesis step, Step 5.)
Package this as a structured text block. You will send this same package to each reviewer — this is the shared baseline. Reviewers may explore further using their own tools, but the baseline ensures equal starting context. If Step 2.5 runs the static scan, its Tier B signals are appended to this same package (under a "PRE-EXISTING STATIC-ANALYSIS SIGNALS — corroborate or dismiss" heading) so every reviewer sees them once — hold that step's output and fold it in before dispatching Round 1.
Step 2.5: Deterministic Static Scan
Gated on static_analysis.enabled (resolved in Step 0). If it is false, skip this step entirely — run no scan, and note static analysis: off in the Step-6 Pipeline header. A missing or erroring script is never a reason to abort (see 2.5.3): the run always continues.
When enabled, run the deterministic scanners once per run (never per reviewer) and split their output into the two-tier evidence model:
- Tier A (verified secrets, known CVEs) → carried forward untouched to the judge (Step 5) as pre-
[verified]findings. Tier A never enters Round 1 and is never sent through a reviewer prompt. - Tier B (SAST/lint signals) → folded into the Step-2 baseline context package so every Round-1 reviewer sees it and either corroborates or dismisses it.
Static analysis is not a voting reviewer — it never counts toward settings.min_reviewers, gets no lens, and is never refuted in Step 4. See rules/static-analysis.md for the tool registry, tier map, per-tool install commands, and the normalize→§3.1 mapping.
2.5.1 Dispatch the scan (one general-purpose subagent)
Reuse the BASE ref and changed-file list already computed in Step 1/2 — do NOT re-derive the diff. Determine the two refs and the changed-file list from the detected target type:
- PR review → base = the PR base branch (
baseRefName), head = the PR head branch (headRefName); changed files = thechangedFilesset fromgh pr view(orgh pr diff --name-only). - Staged code review → base =
HEAD, head = the working tree (pass the worktree path, e.g..); changed files =git diff --cached --name-only. - Unstaged working changes → base =
HEAD, head = the working tree; changed files =git diff --name-only. - Plan/document review → there is no code diff; skip Step 2.5 entirely (nothing for the scanners to scope to) and note
static analysis: off (no code diff).
Write the changed-file list (one repo-relative path per line) to a temp file with Bash (CF="$(mktemp "${TMPDIR:-/tmp}/rc-cf.XXXXXX")"; printf '%s\n' path/a path/b … > "$CF" — mktemp so the path honors macOS's $TMPDIR and the Step-8 cleanup glob can reclaim it), then dispatch one general-purpose Agent with the prompt below. Pass the effective static_analysis.* values resolved in Step 0 as env vars on the invocation (the script reads them via env, matching rc-config.sh's output; do not rely on ambient env carrying across separate tool calls):
You are running the Review Council deterministic static scan. Run the bundled scanner script once and return its output verbatim. Do NOT interpret, summarize, re-run individual tools, or add commentary.
Run exactly (substitute the effective values and paths given to you):
RC_STATIC_ANALYSIS=<effective static_analysis.enabled> \ RC_STATIC_TOOLS=<effective static_analysis.tools, comma-joined> \ RC_STATIC_TIMEOUT=<effective static_analysis.timeout_seconds> \ RC_SEMGREP_CONFIG=<effective static_analysis.semgrep_config> \ RC_STATIC_DOCKER_TOOLS=<empty on the first pass; a comma-joined tool list ONLY on a Docker re-dispatch — see 2.5.2> \ "${CLAUDE_PLUGIN_ROOT}/scripts/rc-static-scan.sh" "<base-ref>" "<head-ref-or-worktree>" "<changed-files-list-file>"On the first dispatch, leave
RC_STATIC_DOCKER_TOOLSempty — that is today's behavior exactly (the Docker daemon is never even probed by the script). It is set to a comma-joined tool list only when the user opts a missing scanner into Docker at the 2.5.2 gate, which re-dispatches this same subagent with those tools added.The script probes each configured tool (
command -v), checks its domain trigger against the changed files, runs the present + triggered ones under a per-tool timeout, and prints aTIER_Ablock, aTIER_Bblock, and oneSKIPPED: <tool> — <reason>line per skipped tool. It always exits cleanly — a tool's own non-zero "found something" exit is a normal outcome, not a script failure.If the script is missing, non-executable, or errors out before producing its blocks, do NOT abort — return exactly
STATIC-SCAN-UNAVAILABLE: <one-line reason>and nothing else. The orchestrator treats that as "all tools skipped."On success, return the script's stdout verbatim (the
TIER_A,TIER_B, andSKIPPED:lines) — nothing added, nothing removed. The output is already compact (normalized pipe linestier|tool|severity_raw|file|line|rule|message), so return it whole.
2.5.2 Missing CONFIGURED tools — inform + ASK (never silently drop)
Before consuming the results, scan the SKIPPED: lines for any tool skipped with reason not installed that is in the configured static_analysis.tools list. The user expects these tools, so they must never be silently dropped.
- Domain-not-triggered skips stay quiet (
SKIPPED: <tool> — not triggered (no matching files)): nothing to install, so just fold them into the status line. Same fordisabled,semgrep off,semgrep config 'auto' …(theauto-needs-metrics skip), andnetwork-unreachable(trufflehog's live-verification egress absent → treated as "ran, 0 findings"; not a missing tool). - Ran-but-failed skips are surfaced, not install-prompted (
SKIPPED: <tool> — timed out, orSKIPPED: <tool> — execution failed (exit N)): the tool is installed and was attempted but produced no usable result this run. Note it visibly in the status line (e.g.semgrep (skipped: timed out)/gitleaks (skipped: execution failed)) so the gap is not silent, but do not ASK to install it (it's present) and do not treat it as a quiet expected skip — it's a transient/tool failure, not a missing tool. - For each tool skipped as
not installed: TELL the user which configured tools are missing and how to install each — look up the per-tool install command inrules/static-analysis.md(the tool registry; e.g.brew install gitleaks). Also probe Docker once (docker info >/dev/null 2>&1); if it succeeds, note which of the missing tools can run via their official image with no local install — only the four core scanners gitleaks, trufflehog, semgrep, osv-scanner have images (the lint tools do not). Then ASK, conversationally, offering the options that apply:"These configured static-analysis tools aren't installed: [tool → install cmd, one per line]. [If Docker is up and any are Docker-supported:] I can run [those] via their official Docker image for this run instead — no local install. Options: (a) install natively and I'll re-run, (b) run the Docker-supported ones via Docker now, or (c) proceed without them?"
- Proceed → continue with the tools that did run; note the missing ones in the status line (
skipped: not installed — proceeding). - Install / wait → pause. After the user confirms they're installed, re-dispatch the same subagent (2.5.1) — the newly-present tools are picked up automatically by the script's
command -vprobe, no restart needed. Re-check theSKIPPED:lines and repeat the ASK if anything is still missing. - Run via Docker → re-dispatch the same subagent (2.5.1) with
RC_STATIC_DOCKER_TOOLS=<comma-joined list of the Docker-supported missing tools>added on the invocation. The script runs each such tool from its pinned image (repo mounted read-only at/src, filesystem-mode scan) and a Docker finding is indistinguishable downstream from a native one. Native install always wins — a tool present onPATHignores its Docker entry, so this only affects the still-missing scanners; a tool with no image (any lint tool, or an unsupported name) simply staysnot installed. Caveats: first use pulls the image (a few hundred MB, one-time);trufflehog's verified-mode outbound egress is unchanged; images are:latest(convenience over digest-pinning). Re-check theSKIPPED:lines after the re-run.
- Proceed → continue with the tools that did run; note the missing ones in the status line (
This is still non-blocking — the user can always choose proceed — it just never silently drops an expected tool. (setup is print-only; this interactive proceed/pause/Docker ASK lives here, at review-run time, where a real run is about to skip a tool the user configured.)
2.5.3 Route the results
- Tier A → to the judge (Step 5), pre-
[verified]. For eachTIER_Aline, build a §3.1-shaped candidate:location=<file>:<line>;symbol= the enclosing construct if the line names one, elseN/A;concern= the wirerulefield as-is (it already carries the tool prefix — e.g.gitleaks:<rule>,osv-scanner:<id>,trufflehog:<detector>), lowercased — do NOT re-compose<tool>:<rule>fromsource+rule, which would double-prefix (osv-scanner:osv-scanner:<id>);source=<tool>;confidence=high;severity= the inherited Tier A severity (secrets → Critical; osv-scanner → its CVSS→severity map, perrules/static-analysis.md). Carry these to Step 5 untouched — they do not enter Round 1, are exempt from the suggestion cap, and the judge does not downgrade them (severity is inherited, not judge-assigned). (gitleaks caveat: precision-by-rule, not live-verified — trufflehog is the live-verified secret scanner.) - Tier B → into the Step-2 baseline package and forward the raw list to the judge (Step 5). Append the
TIER_Blines to the shared context package under a new heading "PRE-EXISTING STATIC-ANALYSIS SIGNALS — corroborate or dismiss", one line per finding (tool · rule · file:line · message). Do not editorialize severity here — that's the judge's call at Step 5. Because it rides the shared package, it is injected once and reaches every reviewer (do NOT paste it separately into each dispatch). Also carry the same rawTIER_Bcandidates forward to Step 5 as tool candidates, exactly as Tier A is carried — the judge needs them directly, not only via a reviewer echo. A Tier B signal that no reviewer corroborates would otherwise never reach the judge, making theTOOL-ONLYverdict /[tool-only:<rule>]badge (§5.3, §5.5) unreachable: corroborated signals dedup into the reviewer's finding (§5.2), uncorroborated ones survive as capped[tool-only:<rule>]suggestions. - Script unavailable → if the subagent returned
STATIC-SCAN-UNAVAILABLE:(or nothing usable), treat it as all tools skipped: no Tier A, no Tier B, status line readsStatic analysis: unavailable (<reason>). Never abort the run.
2.5.4 Print the status line
Print a compact status line (same observable-artifact spirit as the Phase-1 lens map / routing table), one entry per configured tool — ok, <count> for a tool that ran, or skipped: <reason> otherwise:
Static analysis: gitleaks (ok, 0), osv-scanner (ok, 2 CVEs), semgrep (ok, 5 signals), ruff (skipped: no *.py in diff), shellcheck (skipped: not installed — proceeding), actionlint (skipped: no workflow changes), hadolint (skipped: no Dockerfile changes)
Carry this line into the Step-6 report header. If Step 2.5 was skipped (disabled, or a plan/document review), the header notes static analysis: off. For any tool the user opted into Docker (2.5.2), annotate its entry ok, <count> (via docker) — the tool ran, just from its image rather than a local install.
Step 3: Round 1 — Independent Review (Parallel)
Launch all available reviewers in parallel — emit every reviewer's Agent call in a single message so they all run concurrently. Do NOT dispatch a subset and add the rest in a later message: partial batching serializes the fan-out and throws away the wall-clock savings (the slow reviewer, e.g. agy's cold start, should overlap the others, not follow them). The whole fleet goes out at once. They must not see each other's output — this ensures truly independent perspectives.
Lens Assignment (before dispatching)
Before launching the reviewers, assign each one a lens — an emphasis that diversifies coverage. Assignment is deterministic and diff-aware. This step decides which lens each reviewer gets; rules/delegation-format.md supplies the exact per-reviewer lens block text to prepend.
If settings.personas is false: skip lens assignment. Every reviewer gets the identical non-lens prompt (legacy behavior) — omit the ## LENS section from each dispatch. The dedicated reviewer-security still runs. Then go straight to the dispatch blocks below.
Otherwise, assign lenses:
-
Invariants (fixed, not diff-driven):
reviewer-security→ Security (dedicated, always).- Correctness & concurrency → CORE for every repo-capable frontier reviewer:
reviewer-claude, Codex, and Google. They always carry Correctness; the specialist overlay below layers on top. - Perplexity → Dependency / CVE / best-practices (diff-only — it has no repo tools).
-
Classify the diff → specialist overlays. Scan the changed files/hunks (from Step 2) against the signal table and collect the overlays whose signals are present:
Diff signal Overlay to hoist Migrations, schema, SQL, constraints, ORM models Data-integrity & migration New endpoints/routes, request parsing, auth/crypto (Security — dedicated) + boost input-validation + Cross-file Exported signatures, response shapes, serializers, protobuf/OpenAPI Cross-file / API-contract Hot paths, loops, queries, caching, concurrency Performance & reliability / concurrency Dependency manifests / lockfiles Dependency/CVE → Perplexity .github/workflows/, CI config, Dockerfiles, IaCConfig/workflow correctness Frontend components / state / styles UI-state & accessibility Broad multi-file refactor Cross-file impact -
Assign ONE specialist overlay per frontier reviewer (Claude, Codex, Google — on top of CORE Correctness), drawing from the overlays present in the diff in this deterministic tie-break order (signals outrank slots): Data-integrity → Cross-file → Performance → Config → UI → Design. Walk the order and hand each present overlay to the next frontier reviewer that has none yet. Iterate the repo-capable frontier reviewers in the order Claude, Codex, Google when assigning overlays, so the reviewer↔overlay pairing is reproducible. If reviewers remain after the present overlays run out, give them Design & maintainability (the always-useful default). If overlays outnumber reviewers, the earliest in the order win.
-
Config pins override the diff-aware default. For each lens
lin {security,correctness,cross_file,performance,design,dependency}, Step 0 resolvedlens.<l>.enabledandlens.<l>.providers:lens.<l>.enabled=false→ that lens is not assigned this run.lens.<l>.providers= an explicit provider list → pin that lens to exactly those providers (overriding the diff-aware pick).autoor empty → use the diff-aware assignment above.lens.security.replaces_dedicated=truetogether with a pinnedlens.security.providers→ the pinned provider(s) take the Security lens in place of the dedicatedreviewer-security(do NOT run both). When it isfalse(default), the dedicatedreviewer-securityruns and anysecurity.providerspin is layered as an additional security emphasis on those providers.
-
Print the chosen lens map so the assignment is observable, and carry it into the Step 6 report header. Example:
Lens map (personas on): reviewer-security Security (dedicated) reviewer-claude Correctness + Data-integrity & migration Codex Correctness + Cross-file / API-contract Google Correctness + Performance & reliability Perplexity Dependency / CVE / best-practices -
Prepend each reviewer's lens block to its dispatch prompt — the
## LENSsection for the native subagents (Claude, Security), and the delegation prompt written to the temp file for the CLI/API wrappers (Codex, Google, Perplexity). Every lens block states the emphasis and the floor obligation: lens = emphasis, not blinders — still flag any critical you see outside your lens.
Reviewer: Claude (native subagent) — Always
Use the Agent tool with subagent_type: "reviewer-claude".
Pass the resolved settings.claude_max_turns (from Step 0; default 100, deliberately lenient) to the Agent tool as maxTurns. The reader already folded any RC_CLAUDE_MAX_TURNS override into that effective value at the correct precedence — use the effective settings.claude_max_turns, not the ambient env var.
IMPORTANT: Embed the full baseline context package and delegation prompt directly in the Agent tool's prompt parameter. Use this exact template — fill in the bracketed sections with the actual content:
TASK
[Review type]: Review the following [PR/plan/code] as one member of a multi-agent review council. Other AI models are reviewing the same material simultaneously.
LENS
[If
settings.personasis true, prepend the per-reviewer lens block assigned in Step 3 (Lens Assignment) — use the exact block text fromrules/delegation-format.md. The lens names your emphasis (Correctness & concurrency as CORE, plus one specialist overlay). Lens = emphasis, not blinders — still flag any critical issue you see outside your lens. Ifsettings.personasis false, omit this section: every reviewer gets the identical non-lens prompt.]REVIEW PROCESS
Follow these steps in order:
- Understand intent — What is this PR/code/plan trying to achieve? Read carefully before judging.
- Evaluate correctness — Does it achieve its stated goal? Are there logic errors, missed edge cases, or incorrect assumptions?
- Identify risks — What could go wrong in production? Consider security, performance, reliability, data integrity, and failure modes.
- Check completeness — What's missing? Error handling, tests, documentation, migration steps, rollback plans.
- Assess design — Is this the right approach? Is there a simpler way? Will this be maintainable in 6 months?
CONTEXT
[Paste the COMPLETE baseline context package here — full diff, file contents, git log, git blame, project conventions. Do NOT summarize or truncate.]
CONSTRAINTS
- For PRs: focus on what the change introduces, what it might break, and whether it achieves its stated goal
- For plans: focus on feasibility, completeness, risks, and missing considerations
- For code: focus on correctness, security, performance, error handling, and maintainability
- You have Read, Glob, and Grep tools available. Review the provided context and produce your structured findings FIRST. Then use tools to verify concerns and explore for issues the context may have missed (e.g., check callers of a changed function, look for side effects). Always produce your structured output — exploration supplements the review, it does not replace it.
MUST DO
- Provide specific file:line or section references, and explain WHY each finding matters — the impact, not just the symptom.
- Suggest a concrete fix for each finding.
- Also assess test adequacy: are the change's new/changed behaviors covered by tests, and are the important edge cases tested? Flag material gaps (not trivial coverage).
- Quality over quantity — a few well-substantiated findings beat a long list of nitpicks.
- Explore only after reviewing the provided context and producing your findings — exploration supplements the review, it does not replace it.
WHAT NOT TO FLAG
- Theoretical risks requiring unlikely preconditions.
- Defense-in-depth when the primary defense is already adequate.
- Pure style / formatting / naming preference.
- Pre-existing issues outside the change's blast radius (review what the change affects, including unshown callers — not unrelated legacy).
- Speculative "could be a problem" with no concrete trigger.
- Anything matching a recalled learnings suppression (unless you argue the context changed).
FINDINGS CAP
Cap suggestions at ~5. NEVER cap critical or important findings — report every one you find. (The single judge does final curation in a later pass.)
OUTPUT FORMAT
You MUST produce output with these exact sections:
Findings
For each finding, emit every field below (these exact field names):
- severity: critical | important | suggestion
- confidence: high | medium | low — your own, pre-synthesis
- location: <relpath>:<line>
- symbol: <enclosing function/class/section, if any>
- concern: <free-form kebab slug, e.g. missing-null-check> — HINT ONLY; do NOT author a fingerprint (the single judge computes the canonical one)
- issue: one sentence — what's wrong
- why_it_matters: impact if unaddressed
- recommendation: concrete fix / alternative
- how_to_verify: a concrete HUMAN-runnable check (command/input/trace) + expected observation — nothing executes it in this phase
- source: <reviewer-id>
If no issues: write "No issues found."
What's Good
Brief list of things done well.
Overall Assessment
One paragraph: readiness, biggest risk, most important thing to address.
Reviewer: Security (native subagent) — Always
Use the Agent tool with subagent_type: "reviewer-security". This dedicated security reviewer is always dispatched — in parallel with Claude and every available frontier reviewer, independent of which external providers are present. Its lens is fixed to Security (authz/authn, injection, secrets/PII, SSRF, path traversal, unsafe deserialization, missing input validation, insecure defaults, session handling, supply-chain).
Use the same inline template as the Claude reviewer above (TASK / LENS / REVIEW PROCESS / CONTEXT / CONSTRAINTS / MUST DO / WHAT NOT TO FLAG / FINDINGS CAP / OUTPUT FORMAT) — including the §3.1 finding schema, the what-not-to-flag + cap policy, and the test-adequacy line — with the Security lens block prepended in the ## LENS section. Embed the full baseline context package the same way.
Pass the resolved settings.claude_max_turns (default 100) to the Agent tool as maxTurns, same as the Claude reviewer.
Exception — a security pin replaces the dedicated reviewer. If Step 0 resolved lens.security.replaces_dedicated=true and lens.security.providers names one or more providers, do NOT dispatch reviewer-security. Instead assign the Security lens to those pinned provider(s) and dispatch them with it (see Lens Assignment, step 4). Otherwise the dedicated reviewer-security always runs.
Reviewer: Codex — If Available
IMPORTANT: Use an Agent subagent to invoke Codex. This keeps the full review response out of the orchestrator's context window — only the structured findings return.
Provider dispatch goes through the tested state machine scripts/rc-invoke-provider.sh — it owns binary resolve, the hard timeout cap, the fast-empty retry, fast-fail auth/quota/overload classification, non-empty + Findings/Overall Assessment validation, and the attributed SKIPPED: line (all frozen and bats-tested). The subagent no longer discovers CLI syntax, hand-builds a timeout/gtimeout/watchdog wrapper, validates, retries, or classifies — the script does all of it. Codex has no CLI fallback, so the fallback positional is the empty string "". The one thing a shell script can't do is call the mcp__codex__codex MCP tool, so that fallback stays in the subagent, gated on the script's SKIPPED note class (soft → try MCP once; hard → don't).
Dispatch a general-purpose Agent with this prompt. Pass the resolved Codex CLI path from Step 0.2 (codex=<path>); if Codex is MCP-only (codex=none but the mcp__codex__codex tool is available), tell the subagent so — it skips the script and calls MCP directly (see below). Also pass a stable, run-unique <result-file> path (e.g. ${TMPDIR:-/tmp}/rc-codex.<run-tag>.result) — the subagent tees the script's stdout there so you can recover the result at the Step-3.5 join barrier if it ever wedges.
You are invoking the Codex reviewer for a Review Council review. Codex's CLI path is
<codex-path>(or you were told Codex is MCP-only — jump to "MCP-only mode" below).Step 1: Write the delegation prompt — the lens block (if assigned) plus the §3.1 output contract below — to a temp file, and prepare a stderr capture file:
PF="$(mktemp "${TMPDIR:-/tmp}/rc-codex-prompt.XXXXXX")"; ERR="$(mktemp "${TMPDIR:-/tmp}/rc-codex-err.XXXXXX")" # …write the delegation prompt into "$PF"…Step 2: Invoke via the script (CLI path), exactly once — Codex has no fallback, so the second positional is empty.
teestdout to the<result-file>the orchestrator gave you (for recovery, below). Cap the FOREGROUND attempt's reviewer budget atFG_CAP = min(<effective settings.reviewer_timeout_seconds>, 570)seconds (the 570s clamp keeps the Bash-tool wait's 30s cushion under the harness's 600000 ms ceiling — see the foreground note):RC_REVIEWER_TIMEOUT=<FG_CAP = min(effective settings.reviewer_timeout_seconds, 570)> \ "${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" "<codex-path>" "" "$PF" 2>"$ERR" | tee "<result-file>"The script prints
TOOL: Codexthen the review to stdout (on success), or a single attributedSKIPPED: Codex unavailable — codex: <note>line (on failure); and exactly oneELAPSED: <n>line (plus free-form notes) to stderr ($ERR). Do NOT re-resolve the binary, add a shelltimeout/gtimeoutaround it, validate the output, retry it, or re-attribute — the script already did all of it. Do not retry the script (its retry is internal and terminal for the slot).Run it in the FOREGROUND; escalate only on a ceiling hit. Make this a foreground Bash-tool call — set the Bash tool's
timeoutparameter to(FG_CAP × 1000) + 30000ms (withFG_CAP = min(RC_REVIEWER_TIMEOUT, 570)) and do not proactively background it. ClampingFG_CAPto 570s guarantees this wait is ≤ 600000 ms — the harness's hard Bash-tool ceiling — so the script's own internal cap (FG_CAPseconds) always fires first, and the 30s cushion survives (without the clamp, the defaultRC_REVIEWER_TIMEOUT=600would make600×1000+30000collapse onto the 600000 ceiling and cut Codex off exactly as it finishes). That is the Bash tool's wait, distinct from the shelltimeoutyou must not add — the script owns its own internal cap. Codex is single-attempt and self-bounds within its cap, so a synchronous call returns promptly and avoids the background-completion lag that can leave even a fast (<1 min) call looking stuck. Escalate only if the Bash call is actually killed by itstimeout(a real ceiling hit — rare for Codex, since the script self-caps atFG_CAPseconds, comfortably inside theFG_CAP + 30sBash wait): re-run the exact same command once withrun_in_background: trueand the FULLRC_REVIEWER_TIMEOUT=<effective settings.reviewer_timeout_seconds>, returning the result when the completion notification fires (the harness notification is reliable; a fresh run repeats the slot cleanly). Do NOT re-run merely because the configured cap is > 570 — that would double every default-timeout call for no benefit. A foreground call that returned any result is done: a review is returned as-is, and aSKIPPED(includingtimed out) follows the Step-3 branching below (a softtimed out/empty/rate limited→ the MCP fallback, not a background rerun). Recovery: becauseteecopied stdout to<result-file>, the result survives on disk — if this subagent wedges, the orchestrator reads it at the Step-3.5 join barrier.Step 3: Branch on the result:
- Prints
TOOL: Codex→ return the script's stdout verbatim.- Prints
SKIPPED:with a SOFT reason — the reason text mentionstimed out,empty output, orrate limited(a transient blip a shell script can't route around) → try the MCP fallback once: callmcp__codex__codexwith the same delegation prompt ($PF). On success, return its review prefixed with aTOOL: Codexline. On failure, return the script'sSKIPPED:line verbatim.
rate limitedmeans a tokens-per-minute throttle, not a dead quota — the script already retried once with a backoff inside its budget and was throttled again. A different transport may not be throttled, so MCP is worth one attempt. Never treat it as hard: the council's own Round-1 → refutation burst is what trips these limits, and dropping the slot costs a whole model family for the run.- Prints
SKIPPED:with a HARD reason — the reason text mentionsauth failure,quota exhausted, oroverloaded→ do NOT try MCP (retrying a dead auth/quota/overload won't help); return theSKIPPED:line verbatim. Notequota exhaustednow means a genuinely terminal quota (a reset or upgrade is required) — a bare HTTP 429 is reported asrate limitedinstead.MCP-only mode (no CLI path given): skip the script entirely and call
mcp__codex__codexwith the delegation prompt directly. Return its review prefixed withTOOL: Codex, orSKIPPED: Codex unavailable — <reason>on failure.Always append, as the last line of what you return, the
ELAPSED:line captured from$ERR(grep '^ELAPSED:' "$ERR"), so the orchestrator can meter the run budget (Step 3.5 / Step 4.0). In MCP-only mode (no script ran), appendELAPSED: notionalinstead.§3.1 output contract (write this into
$PF; preserve it): Codex's review must have aFindingssection where every finding carries all of these fields, by these exact names —severity(critical|important|suggestion),confidence(high|medium|low),location(<relpath>:<line>),symbol,concern(free-form kebab slug — a HINT only; no fingerprint),issue,why_it_matters,recommendation,how_to_verify(a human-runnable check + expected observation),source— plus theWhat's GoodandOverall Assessmentsections. Same what-not-to-flag rules and cap policy (cap suggestions at ~5; never cap critical/important), and include the test-adequacy line: "Also assess test adequacy: are the change's new/changed behaviors covered by tests, and are the important edge cases tested? Flag material gaps (not trivial coverage)." If a lens block was assigned in Step 3, prepend it to the delegation prompt (lens = emphasis, not blinders; still flag any critical outside the lens).
Reviewer: Google (Antigravity / Gemini) — If Available
IMPORTANT: Use an Agent subagent to invoke the Google-family reviewer, same pattern as Codex — keeps the response out of the orchestrator's context. agy (Antigravity) and gemini share one slot.
Double-vote invariant. The Google slot is a single dispatch unit — one script call, one result, counted once; agy↔gemini is an internal fallback, never two reviewers. The subagent makes exactly one rc-invoke-provider.sh call and returns exactly one TOOL:/SKIPPED: result; agy→gemini fallback happens inside the script. There is structurally no path for the slot to emit two reviewer rows or count twice toward settings.min_reviewers (holds identically in Round 1 and Step 4.3 refutation).
Provider dispatch goes through the tested state machine scripts/rc-invoke-provider.sh — it owns every clause the old prose spelled out: binary resolve (by resolved path, not PATH), the hard timeout cap, agy's unit-suffixed --print-timeout "${cap}s" (the bare-integer missing unit in duration bug is pinned inside), the one agy fast-empty retry (time-boxed to the remaining budget), agy→gemini fallback (fresh full budget, no fallback retry), fast-fail auth/quota/overload classification, the heading-anchored non-empty + Findings/Overall Assessment validation, and the SKIPPED: attribution that leads with agy whenever it was primary — six bug-fix commits' worth of edge cases. The subagent no longer resolves binaries, builds timeouts, validates, retries, or re-attributes; it never re-runs the script (its retry/fallback is internal and terminal for the slot).
Dispatch a general-purpose Agent with this prompt. Pass the resolved paths from Step 0.2 detection — agy=<path> as the primary positional and gemini_fallback=<path> as the fallback positional (use "" for the fallback when there is none; a gemini-only slot passes its gemini=<path> as the primary and "" as the fallback). Pass the effective settings.reviewer_timeout_seconds as RC_REVIEWER_TIMEOUT, and — when Step 0 resolved them — the Google model as RC_GOOGLE_MODEL and the repo path as RC_GOOGLE_ADD_DIR, on the invocation (these are the only Google env passthroughs the script reads). Also pass a stable, run-unique <result-file> path (e.g. ${TMPDIR:-/tmp}/rc-google.<run-tag>.result) — the subagent tees stdout there so you can recover the result at the Step-3.5 join barrier if it wedges:
You are invoking the Google-family reviewer (Antigravity
agyprimary, Geminigeminifallback) for a Review Council review. Write the delegation prompt — the lens block (if assigned) plus the §3.1 output contract below — to a temp file, and prepare a stderr capture file:PF="$(mktemp "${TMPDIR:-/tmp}/rc-google-prompt.XXXXXX")"; ERR="$(mktemp "${TMPDIR:-/tmp}/rc-google-err.XXXXXX")" # …write the delegation prompt into "$PF"…Then run the script exactly once (
teestdout to the<result-file>the orchestrator gave you, for recovery). Cap the FOREGROUND attempt atFG_CAP = min(<effective settings.reviewer_timeout_seconds>, 570)seconds (the 570s clamp keeps the Bash-tool wait's 30s cushion under the 600000 ms ceiling — see the foreground note):RC_REVIEWER_TIMEOUT=<FG_CAP = min(effective settings.reviewer_timeout_seconds, 570)> \ [RC_GOOGLE_MODEL=<model>] [RC_GOOGLE_ADD_DIR=<repo>] \ "${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" "<agy-path>" "<gemini-path-or-empty>" "$PF" 2>"$ERR" | tee "<result-file>"Run it in the FOREGROUND; escalate only on a ceiling hit. Make this a foreground Bash-tool call — set the Bash tool's
timeoutparameter to(FG_CAP × 1000) + 30000ms (withFG_CAP = min(RC_REVIEWER_TIMEOUT, 570)) and do not proactively background it (the tool's wait, not a shelltimeoutyou add). ClampingFG_CAPto 570s keeps that wait ≤ 600000 ms — the harness's hard Bash ceiling — so the script's internal cap fires first and the 30s cushion survives (unclamped, the defaultRC_REVIEWER_TIMEOUT=600collapses onto the ceiling and loses it). Note the Google slot can runagyand thegeminifallback (up to ~2× the reviewer cap), so even at the clamp the single foreground call may hit the ceiling before the script finishes its fallback. Escalate only if the Bash call is actually killed by itstimeout(a real ceiling hit — this can happen for the Google slot: whenagytimes out and thegeminifallback then runs, the two attempts can together exceed theFG_CAP + 30sforeground wait): re-run the exact same command once withrun_in_background: trueand the FULLRC_REVIEWER_TIMEOUT=<effective settings.reviewer_timeout_seconds>, returning the result when the completion notification fires (the fresh run repeats the slot — still oneTOOL:/SKIPPED:result, so the double-vote invariant holds). Do NOT re-run merely because the configured cap is > 570 — that would double every default-timeout call for no benefit. A foreground call that returned any result is done (a Google slot that ranagythen thegeminifallback within the wait has already produced its one result). Recovery:teeleft the result in<result-file>, recoverable at the Step-3.5 join barrier if this subagent wedges.Return its stdout verbatim — it is already
TOOL: Antigravity(foragy) /TOOL: Gemini(forgemini) followed by the review, or a correctly-attributed singleSKIPPED:line (e.g.SKIPPED: Google (Antigravity) unavailable — agy: empty output after retry; gemini fallback: ineligible (auth/DASHER)). Do NOT reword it, re-attribute it, validate it, or retry it — the script's retry/fallback is internal and terminal for the slot (do not let Step 3.5 re-run it). Then append, as the last line, theELAPSED:line captured from$ERR(grep '^ELAPSED:' "$ERR"), so the orchestrator can meter the run budget (Step 3.5 / Step 4.0).§3.1 output contract (write this into
$PF; preserve it): the review must have aFindingssection where every finding carries all of these fields, by these exact names —severity(critical|important|suggestion),confidence(high|medium|low),location(<relpath>:<line>),symbol,concern(free-form kebab slug — a HINT only; no fingerprint),issue,why_it_matters,recommendation,how_to_verify(a human-runnable check + expected observation),source— plus theWhat's GoodandOverall Assessmentsections. Same what-not-to-flag rules and cap policy (cap suggestions at ~5; never cap critical/important), and include the test-adequacy line: "Also assess test adequacy: are the change's new/changed behaviors covered by tests, and are the important edge cases tested? Flag material gaps (not trivial coverage)." If a lens block was assigned in Step 3, prepend it to the delegation prompt (lens = emphasis, not blinders; still flag any critical outside the lens).
Reviewer: Perplexity — If Available
IMPORTANT: Use an Agent subagent to invoke Perplexity, same pattern — keeps the API response out of the orchestrator's context.
Dispatch a general-purpose Agent with this prompt:
You are invoking the Perplexity reviewer for a Review Council review. Your job is to call the Sonar API, collect its response, and return the structured findings.
Step 1: Build the JSON payload using
jqto avoid manual escaping. Usemktempfor the payload/response files so the paths honor macOS's$TMPDIRand concurrent runs never clobber each other:PAYLOAD="$(mktemp "${TMPDIR:-/tmp}/rc-perplexity-payload.XXXXXX")" RESP="$(mktemp "${TMPDIR:-/tmp}/rc-perplexity-response.XXXXXX")" PROMPT="[the full delegation prompt text]" jq -n --arg model "sonar" --arg prompt "$PROMPT" \ '{model: $model, messages: [{role: "user", content: $prompt}]}' \ > "$PAYLOAD"Step 2: Call the API (measure elapsed around the curl for the run budget). Perplexity stays an inline
curlpath — it is not routed throughrc-invoke-provider.sh(the script is CLI-only: a binary to resolve + exit/stderr classification, whereas Perplexity is an HTTP API with an HTTP-status failure taxonomy). To still feed the Step-4.0 budget long pole, measure its wall-clock withdatearound the curl:RC_START=$(date +%s) curl -fsS --max-time "${RC_REVIEWER_TIMEOUT:-600}" https://api.perplexity.ai/v1/chat/completions \ -H "Authorization: Bearer $PERPLEXITY_API_KEY" \ -H "Content-Type: application/json" \ -d @"$PAYLOAD" \ -o "$RESP" RC_ELAPSED=$(( $(date +%s) - RC_START ))Note:
-fmakescurlexit non-zero (22) on any HTTP 4xx/5xx without exposing the status code. Treat any such failure as terminal — fast-fail (return SKIPPED, no retry), do not loop. Perplexity has no fallback tool, so there's nothing to gain from distinguishing 401/403/429 from other errors. (If you ever need the exact status, capture it with-w '%{http_code}'and drop-f.)Step 3: Parse the response:
jq -er '.choices[0].message.content' "$RESP"If curl or jq fails: Return "SKIPPED: Perplexity unavailable — [error details]"
Output contract (the
$PROMPTdelegation text in Step 1 already specifies this — preserve it): the review must have aFindingssection where every finding carries all of these fields, by these exact names —severity(critical|important|suggestion),confidence(high|medium|low),location(<relpath>:<line>),symbol,concern(free-form kebab slug — a HINT only; no fingerprint),issue,why_it_matters,recommendation,how_to_verify(a human-runnable check + expected observation),source— plus theWhat's GoodandOverall Assessmentsections. Same what-not-to-flag rules and cap policy (cap suggestions at ~5; never cap critical/important), and include the test-adequacy line: "Also assess test adequacy: are the change's new/changed behaviors covered by tests, and are the important edge cases tested? Flag material gaps (not trivial coverage)." As the diff-only Dependency / CVE / best-practices reviewer, Perplexity's lens block is already prepended to$PROMPT(lens = emphasis, not blinders; still flag any critical outside the lens).Return the full structured review output verbatim (Findings with all the fields above, What's Good, Overall Assessment). Then append, as the last line of what you return,
ELAPSED: <RC_ELAPSED>(the measured curl seconds from Step 2) so the orchestrator can meter the run budget — the same closure the CLI reviewers get, done inline (Perplexity uses thisdatemeasurement rather than the shared script; seerules/orchestration.md→ Budget). Append the sameELAPSED: <RC_ELAPSED>line even on a SKIPPED (curl/jq failure); if the failure occurred before the curl ran, appendELAPSED: notionalinstead.
Delegation Prompt
For all reviewers (including Claude and Security), use the delegation format from rules/delegation-format.md — which carries the §3.1 finding schema, the what-not-to-flag + cap policy, and the test-adequacy line. The prompt structure and review criteria are identical for every provider — only the transport and the prepended lens block differ. When settings.personas is true, prepend each reviewer's assigned lens block (from Lens Assignment) to its delegation prompt; when it is false, send every reviewer the identical non-lens prompt. The baseline context package from Step 2 (including any recalled Team Learnings — Conventions) goes into the CONTEXT section.
Step 3.5: Well-formed Check & Recover
Join barrier — wait for the whole fleet before proceeding. First, wait until every reviewer dispatched in Step 3 has returned (each is bounded by RC_REVIEWER_TIMEOUT, so a stuck reviewer times out rather than blocking forever — a reviewer still running is not the same as one that failed). Do NOT begin the well-formed check, and do NOT proceed to the refutation pass, with only a subset of reviewers back. Collect all Round-1 results first, then run the checks below over the full set.
Recovering a wedged CLI wrapper. A CLI-reviewer subagent (Codex/Google) runs its rc-invoke-provider.sh call in the foreground and tees stdout to the <result-file> you gave it (see §Reviewer: Codex / Google). If such a subagent is slow to return but its call has already finished — the rare "backgrounded a fast call, watcher lagged" case — read its <result-file>: a populated TOOL:/SKIPPED: line there is the result (pair it with the ELAPSED: from that reviewer's stderr, or notional if unavailable), so you need not wait on the wedged subagent. A <result-file> that is absent or has no TOOL:/SKIPPED: line means the call has not produced a result yet — keep waiting (bounded by RC_REVIEWER_TIMEOUT), do not fabricate one.
Parse each reviewer's measured elapsed. Every CLI/API reviewer subagent (Codex, Google, Perplexity) returns a trailing ELAPSED: <n> line — the whole seconds rc-invoke-provider.sh measured (Codex/Google) or the inline date delta around the curl (Perplexity). As you collect each Round-1 result, read that last line and record the reviewer's elapsed: <n> (measured). If the line is missing or reads ELAPSED: notional (an MCP-only Codex, or a reviewer that failed before reaching its transport), record elapsed: notional so a missing measurement is visible, never silently zeroed. The native reviewer-claude / reviewer-security subagents don't emit a CLI-measured number — record them elapsed: notional (maxTurns-bounded). The max of the measured values is the Round-1 long pole the Step-4.0 budget gate keys on (§4.0).
Before the refutation pass, validate that each reviewer's output is well-formed — a structural check (required sections present; finding fields present and in-enum), NOT a truth check of whether any finding is correct. Refer to rules/orchestration.md → Output Validation for full rules.
Validate
For each reviewer's response:
- Check for
## Findings(or### Findings) section — present? - Check for
## Overall Assessment(or### Overall Assessment) section — present? - If Findings section exists and contains findings, check each finding carries all required fields from
rules/orchestration.md→ Field-Level Validation (the §3.1 schema:severity,confidence,location,symbol,concern,issue,why_it_matters,recommendation,how_to_verify,source) - If Findings section says "No issues found" or equivalent — mark as CLEAN
- If sections are missing or findings lack required fields — mark as FAILED
Report & Recover
Count VALID, CLEAN, and FAILED results.
If all VALID or CLEAN: Announce results and proceed to Step 4.
Round 1 complete. All N reviewers produced valid output:
Claude [ok] 3 findings
Codex [ok] 5 findings
Antigravity [clean] no issues found
If any FAILED but enough remain (>= RC_MIN_REVIEWERS, default 2):
Report the status and ask the user conversationally:
"Round 1 complete: [list results]. N of M reviewers succeeded. Should I retry the failed reviewer(s) (will use additional tokens), proceed with the N successful reviews, or abort?"
If RC_AUTO_RETRY is set to true, skip the prompt and retry automatically.
If any FAILED and not enough remain (< RC_MIN_REVIEWERS):
"Round 1 complete: [list results]. Only N reviewer(s) succeeded — council mode requires RC_MIN_REVIEWERS. Should I retry the failed reviewer(s) (will use additional tokens), or abort? Proceeding without retry means single-reviewer mode."
If all FAILED: Report the failure and abort.
Retry
If retrying:
- Re-dispatch only the FAILED reviewers using the same dispatch method as Round 1
- Validate the retry results using the same checks
- One retry max per reviewer — if it fails again, mark as unavailable
- Merge all validated results (first-pass and retried) into a single pool
- Proceed to Step 4 with the merged pool
Step 4: Refutation Pass
Round 1 produced independent findings. Instead of asking reviewers to revise toward a shared synthesis (the old anchoring Round 2 — deleted), this step tries to refute each candidate finding with a fresh, cross-family verifier that has never seen the other reviewers' output. An UPHELD is then independent corroboration; a REFUTED (with counter-evidence) is a genuine counter-finding. Verdicts feed the judge (Step 5). Use the Refutation Template in rules/delegation-format.md.
Gated on settings.verify (resolved in Step 0). If settings.verify is false, skip this step entirely: carry every Round-1 finding forward tagged [unverified], and go straight to the judge (Step 5).
If settings.verify is true, apply these gates and rules in order.
4.0 Skip gates (check first, before any routing)
- Solo-Claude mode — if only Claude-family reviewers (Claude + the dedicated Security reviewer) are available — i.e. no different-family reviewer to cross-verify against — skip refutation entirely. Tag every finding
[1 reviewer · unverified]and go to Step 5. Do NOT self-verify with another Claude spawn — one model refuting itself is correlated-error theatre, not cross-family evidence. - Budget check FIRST (see
rules/orchestration.md→ Budget). Because Round-1 reviewers run concurrently, the elapsed cost so far is the long pole — the maximum measured elapsed any single Round-1 CLI/API invocation reported (e.g.agy's cold start), parsed from the trailingELAPSED:lines at the Step-3.5 join barrier — not the arithmetic sum of all of them (they overlapped in wall-clock), and not a stopwatch you watch. (Each CLI/API reviewer returns its measuredELAPSED; the long pole is the max of those. Native Claude/Security subagents still don't emit a CLI-measured number — they are bounded bymaxTurns(settings.claude_max_turns) and, being local, are rarely the long pole, so the budget keys on the measured CLI/API max.) If that long-pole elapsed has already reachedsettings.run_budget_seconds, skip refutation: tag all findings[unverified], printstopped at budget: <n>s, and go straight to Step 5. Never hard-abort — degrade.
If neither gate fires, run the pass.
4.1 Select candidates
- Merge all Round-1 findings into one pool. To detect cross-family agreement, cluster them provisionally by
(location, concern-slug)— a lightweight grouping; the judge computes the authoritative fingerprint in Step 5. - Skip findings already raised by ≥2 different families — they are already corroborated, so no verification is needed. They pass to the judge as cross-family-corroborated (eligible for the +1-tier promotion) without consuming a verifier slot.
- From the remaining (single-family) findings, take the top
settings.verify_max_findings(default 12) by severity (critical > important > suggestion, then confidence). Any beyond the cap ship to the judge tagged[unverified].
4.2 Route (print the routing table)
Assign each selected candidate to a verifier that is repo-capable and from a DIFFERENT family than the finding's origin (families: Claude-fresh / Codex / Google). Rules:
- NEVER route a code-tracing finding to Perplexity — it is diff-only and tool-less; it cannot Read/Grep/trace, so it can neither uphold nor refute a code claim.
- The verifier family must differ from the finding's origin family (a model does not verify its own finding).
- If no different repo-capable family is available for a given finding (e.g. only Claude is repo-capable this run), that finding cannot be cross-verified — ship it to the judge
[unverified]rather than self-verifying.
Print the routing table so the assignment is observable. Example:
Refutation routing (verify cap 12):
finding origin → verifier
auth.ts:42 missing-authz Codex → Claude-fresh
db.ts:88 n-plus-one Claude → Google
api.ts:20 unvalidated-input Google → Codex
[skipped: raised by ≥2 families] payment.ts:5 race (Claude+Codex)
[unverified: over cap] util.ts:9 minor-leak
4.3 Dispatch — ONE fresh subagent per verifier family
Batch by verifier family: spawn exactly one fresh Agent per verifier family, handing it all the findings routed to that family (isolation only requires hiding the synthesis, not a spawn per finding). Each subagent gets the Refutation Template (rules/delegation-format.md) with its assigned findings pasted in and the baseline context — but NOT any other reviewer's output or any synthesis.
- Claude-fresh → a new
reviewer-claudeAgent (Read/Glob/Grep), refutation prompt only. - Codex → a
general-purposeAgent that dispatches Codex through the same collapsed pattern as Round 1 (see §Reviewer: Codex) — write the refutation prompt to$PF, call"${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" "<codex-path>" "" "$PF" 2>"$ERR", keep the soft-SKIPPED→mcp__codex__codex-once fallback, and append the trailingELAPSED:line so a slow verifier also feeds the budget. No separate machinery — the script owns transport + reliability by reference. - Google → a
general-purposeAgent that dispatches the Google slot through the same collapsed pattern as Round 1 (see §Reviewer: Google) — write the refutation prompt to$PF, call"${CLAUDE_PLUGIN_ROOT}/scripts/rc-invoke-provider.sh" "<agy-path>" "<gemini-path-or-empty>" "$PF" 2>"$ERR"once (agy→geminifallback is internal; the slot stays one dispatch unit, one result), and append the trailingELAPSED:line.
Hard rule: a verdict is only valid from a fresh Agent spawn. If no verifier subagent actually ran for a finding (over the cap, no eligible different family, a verifier that SKIPPED/failed, or budget-degraded), that finding is [unverified] — it is NOT refuted. Absence of a spawn never means REFUTED.
4.4 Collect verdicts
Each verifier returns a 3-way verdict per assigned finding — UPHELD / REFUTED (with cited counter-evidence) / INCONCLUSIVE. Carry the verdicts (plus the ≥2-family corroboration status from 4.1) into the judge. Remember: INCONCLUSIVE is not REFUTED — a finding only drops on positive counter-evidence.
Step 5: Judge Synthesis
A single active judge (you, the orchestrator) makes one pass over the Round-1 findings plus the Step-4 verdicts. It does not re-review the code — it dedups, recalibrates, suppresses, and emits a ledger.
5.1 Compute the canonical fingerprint
For each finding, compute fingerprint = <relpath>::<normalized-symbol-or-hunk>::<normalized-concern>. The judge computes it — reviewers never author it (their concern slug is a hint only). Normalize: repo-relative path; symbol lowercased/trimmed (or the hunk range if no symbol); concern reduced to its core kebab phrase.
Tool findings included. The Tier A candidates and the raw Tier B candidates both carried from Step 2.5 (§2.5.3) are fingerprinted the same way — a semantic reduction, not a string match between a tool's rule id and an LLM's concern slug (a semgrep:<rule> id and a Codex free-text concern are never string-identical; the judge recognizes them as the same issue by file/line proximity + semantic reading, exactly as it already does for two differently-worded LLM findings). No new machinery — tool findings flow through the existing fingerprint/dedup path.
5.2 Semantic cross-model dedup
Collapse findings with the same fingerprint into one. Keep the most specific/actionable text and union their origin-families (so a finding raised by Claude and Codex records both). This is the authoritative dedup — the Step-4 provisional clustering was only for corroboration detection.
When a Tier B tool signal and an LLM finding share a fingerprint, they collapse into one finding; record the tool name(s) on the merged finding (this populates the ledger's tool? column, §5.5) and mark it tool-corroborated for §5.3. Tier A candidates also participate in dedup, but a Tier A hit is never downgraded by merging — see §5.3.
5.3 Recalibrate (§recalibration)
- +1 tier (severity or confidence) when a finding has cross-family corroboration — it was raised by ≥2 different families, or it was UPHELD in Step 4 by a different family (these are the two
[cross-reviewed]paths). A same-family-only pile-up is NOT a promotion signal. - Drop a finding ONLY if it was REFUTED with positive counter-evidence. Never drop on absence of proof.
- Keep and tag
[unverified]if the verdict was INCONCLUSIVE, or the finding was never verified at all (over the cap, or budget-degraded) — in a multi-reviewer run. Solo-Claude findings are already tagged[1 reviewer · unverified]in Step 4.0; they do not additionally get the plain[unverified]badge. - Never demote a
criticalout of Critical for being single-reviewer. Tag it[1 reviewer · unverified]and keep it Critical. - Suppress any finding whose fingerprint matches a learnings Suppression entry recalled in Step 0.5. Count the suppressions.
- Tier A tool finding (from Step 2.5) → enters at its inherited severity (secrets → Critical; osv-scanner → its CVSS→severity map), badge
[verified], ledgerverdict = TOOL-VERIFIED(it never went through Step-4 refutation). It is exempt from the suggestion cap (moot in practice — Tier A is rarely asuggestion— but state it for completeness) and the judge never downgrades it: severity is inherited, not judge-assigned. It is pre-verified evidence, not an opinion. - Tier B tool finding, no LLM match on its fingerprint → stays a
suggestion, badge[tool-only:<rule>], subject to the normal suggestion cap. - Tier B tool finding, LLM match on the same fingerprint → promote to
[verified]at the higher of the tool's context-weight and the LLM's self-rated severity. This is the one place[verified]and[cross-reviewed]can coexist — a finding can be both tool-grounded and cross-family-corroborated; badge it[verified](the stronger badge, see Step 6 precedence) and note the cross-review in prose.
5.4 Apply the what-not-to-flag filter
Drop any surviving finding that matches the WHAT NOT TO FLAG list (the same policy the reviewers were given: theoretical/precondition-heavy risks, redundant defense-in-depth, pure style, out-of-blast-radius legacy, speculative-with-no-concrete-trigger).
5.5 Emit the ledger (BEFORE the prose report)
Print one row per surviving finding, then the suppression count:
Judge ledger:
fingerprint | origin-families | verdict | suppression? | tool? | final-severity | final-confidence
config.yml::_::hardcoded-secret | — | TOOL-VERIFIED| no | gitleaks | critical | high
auth.ts::checkauth::missing-authz | codex,claude | UPHELD | no | semgrep | critical | high
db.ts::listrows::n-plus-one | claude | INCONCLUSIVE | no | — | important | medium
api.ts::handler::unvalidated-input | google | REFUTED | (dropped) | — | — | —
run.sh::deploy::sc2086-word-split | — | TOOL-ONLY | no | shellcheck| suggestion | medium
Suppressions applied: 1
The tool? column (was always — in Phase 1) now carries the tool name(s) whose finding hit the same fingerprint, or — if none did. Two verdict values are specific to static analysis: TOOL-VERIFIED for a Tier A finding (never went through Step-4 refutation) and TOOL-ONLY for a Tier B tool-only signal no reviewer corroborated (badge [tool-only:<rule>]). A Tier B signal a reviewer did corroborate merges into that reviewer's row (its verdict stays UPHELD/INCONCLUSIVE from Step 4, tool? records the tool, and it is [verified]). Emit the ledger before the Step-6 prose so the judge's reasoning is auditable.
Step 6: Report
Produce the final curated output, grouped by severity FIRST. Agreement is a per-finding badge, never the sort key.
Badges:
[verified]— tool-grounded: a Tier A deterministic finding (secrets/CVE), or a Tier B tool signal an LLM reviewer corroborated on the same fingerprint (§5.3).[cross-reviewed]— UPHELD by a different family, or raised by ≥2 different families.[1 reviewer · unverified]— a single reviewer, not cross-verified (a single-reviewercriticalkeeps Critical severity and carries this badge).[unverified]— verification was skipped / inconclusive / over-cap / budget-degraded, orsettings.verifywas off.[tool-only:<rule>]— a Tier B tool signal no reviewer corroborated (stays a Suggestion, §5.3).
Badge precedence when more than one could apply — show the strongest, mention the rest in prose: [verified] > [cross-reviewed] > [1 reviewer · unverified] > [unverified]. (A finding that is both tool-grounded and cross-family-corroborated shows [verified] and notes the cross-review in prose.)
The judge ledger from Step 5 is printed before this prose report. Then emit this format:
Review Council Report
Target: [what was reviewed — PR #N, file path, etc.]
Type: [PR | Plan/Document | Code]
Reviewers: [reviewers that participated] ([N] participating — [skipped: reasons])
Lens map: [the per-reviewer lens assignment printed in Step 3 — e.g. reviewer-security: Security · reviewer-claude: Correctness + Data-integrity · Codex: Correctness + Cross-file · Perplexity: Dependency. If settings.personas is false, note "personas off (legacy prompt)".]
Pipeline: [prepend static scan → when Step 2.5 ran] Round 1 → well-formed check → refutation → judge → report [append → post digest (pending) when pr_comments.enabled=true — this structured line must NOT imply the run ends at the report when posting is on; Step 6.6 is still owed] [append · stopped at budget: <n>s if the run degraded on budget; note · refutation skipped (solo-Claude) or · refutation off (verify=false) when applicable, and · static analysis: off when Step 2.5 was disabled/skipped — these are the pipeline stages actually run]. Append the Step-2.5 status line (Static analysis: …) beneath this when the scan ran.
Badge legend — each finding carries a badge for how much corroboration it has (strongest → weakest; when several apply, the strongest is shown and the rest noted in prose). Print this block verbatim in every report so the labels are self-explanatory; include only the rows for badges that actually appear, or the whole list — your call, but never leave a badge unexplained:
[verified]— tool-grounded: a Tier A deterministic hit (secret/CVE), or a Tier B tool signal an LLM reviewer confirmed on the same fingerprint.[cross-reviewed]— corroborated across model families: raised by ≥2 different families, or UPHELD by a different family in the refutation pass.[1 reviewer · unverified]— one reviewer, no cross-check available (solo-Claude runs, where no different family exists to verify against). Severity is kept as-is — a single-reviewercriticalstays Critical.[unverified]— not cross-verified: the refutation pass was inconclusive, skipped (over the cap or budget-degraded), or turned off. Not a knock on the finding — just uncorroborated.[tool-only:<rule>]— a static-analysis (Tier B) signal no reviewer corroborated; kept as a low-priority Suggestion.
Critical Issues
[Every critical finding, each with its badge. Single-reviewer criticals stay here, tagged [1 reviewer · unverified].]
If none: "No critical issues identified."
Important Findings
[Every important finding, each with its badge.]
If none: "No important findings."
Suggestions
[Every suggestion finding, each with its badge.]
If none: "No additional suggestions."
Dissenting Opinions
[Genuinely unresolved disagreement the judge could not reconcile — e.g. one family UPHELD and another REFUTED the same fingerprint. Record both perspectives. This is a report section, not a round.]
If none: omit this section entirely.
What's Done Well
[Brief — things reviewers praised. Keep to 2-3 bullet points max.]
Do not treat the report as the end of the run. Re-resolve pr_comments.enabled now by re-running rc-config.sh — always read it fresh rather than trusting a remembered value (a re-executed config read is reliable where a recollection is not, and it reflects any change during the run). If it resolves true and the review target is a PR (or a code review on a branch that can open one), proceed to Step 6.6 and offer to post the digest — the report is a checkpoint, not the finish line. Step 6.6 still stops and waits for your explicit approval before anything is posted ("proceed" means reach that gate and offer, never post on your own). For a plan/document review, or when posting is off, the report is the end — skip Step 6.6.
Step 6.6: Post the Review Digest to the PR (optional)
Gated on pr_comments.enabled (resolved in Step 0). If it is false (the default), skip this step entirely — the run stays report-only, byte-identical to today. This is the plugin's one sanctioned outward-posting path, and it never auto-posts: a human confirms before anything leaves the session (the reply-pr pattern). Posting is fail-soft — any failure is surfaced and the run continues; it never blocks or mutates the review already emitted.
Posting anchors to a PR, so it applies to a PR review (or a code review whose branch has / can open a PR). All GitHub calls go through the deterministic plumbing in scripts/rc-post.sh (it does the mechanics, never any judgment — you compose the digest, it posts it).
6.6.1 Resolve the target PR
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-post.sh" detect
pr=<n>(withurl,head) → that open PR is the target.pr=(empty) → no open PR. Ask the author whether to open one as a draft so the digest has a home. Only on an explicit yes:
It pushes the branch if needed and prints the new"${CLAUDE_PLUGIN_ROOT}/scripts/rc-post.sh" open-drafturl=; re-rundetectfor the number. On the default branch or with no remote it declines (exit ≠ 0) — note it and skip posting (don't error the run).
6.6.2 Triage walk (shared with Step 7)
Drive off the Step-5 ledger and the Step-6 grouping. Present the surviving findings and, for each, ask the author to mark one disposition:
- fix — they'll address it.
- defer — not now (optionally a one-line reason).
- won't-fix — declining it (optionally a one-line reason).
- all — apply one disposition to the remainder.
Show it as an editable proposal (a sensible default per finding is fine), never auto-applied. Collect these dispositions once — Step 7 (learnings) reuses them; do not walk the author twice.
6.6.3 Compose the digest
Build the single digest markdown — three sections:
- What was found — and by whom — a table: severity · finding ·
location· found by (the ledger's origin-families) · council verdict (verified / cross-reviewed / upheld / unverified / dropped-refuted). - What the council decided — the judge's synthesis: promotions on cross-family agreement, drops on refutation, the final tally, any dissent.
- What was fixed — finding → status from the 6.6.2 dispositions (✅ Fixing / ⏭️ Deferred / ❌ Won't fix).
Body rules:
- The first line is the hidden marker
<!-- rc:report -->, verbatim — the engine keys idempotency off it (andrc-post.shrefuses to post a body that lacks it). - End with the identity-aware footer:
by @<user> (not a personal comment)when posting as the authenticated user, orreview-council[bot]when a bot token is set. - Escape cell content: a
|or newline in anylocation/finding string is escaped (\|) so untrusted (model/repo-derived) text can't break the table or the comment. - Never echo secret material. A Tier A secret finding (gitleaks/trufflehog) is referenced by rule +
path:lineonly — never the credential value (or any recognizable fragment). The digest may be posted to a public PR; do not republish what the scanner just flagged as sensitive.
Write it to a temp file with Bash: RC_DIGEST="$(mktemp "${TMPDIR:-/tmp}/rc-digest.XXXXXX")".
6.6.4 Confirm, then post (human-confirmed only)
Show the composed digest and the target and stop and wait. In the confirmation, state explicitly: the PR (#<n> + repo), and the identity that will post — resolve and name it (run gh api user --jq .login, prefixed with the bot token when pr_comments.bot_token_env names a set var) — plus, when a bot token is used, the env-var name it came from. This lets the author catch an unexpected identity (e.g. a mis-set token var) before anything is posted. On explicit approval:
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-post.sh" post <pr-number> "$RC_DIGEST" "<pr_comments.bot_token_env>"
Pass the resolved pr_comments.bot_token_env (from Step 0) as the third argument. The script reads that env var; when it is unset/empty the comment posts as the authenticated gh user (the default identity), otherwise as the bot that token authenticates. It finds the marked comment and updates it in place, else creates it, printing action=created|updated and url=. Surface the url. On non-zero exit, surface the script's stderr and do not claim a post happened. Then rm -f "$RC_DIGEST".
Decline at any gate → post nothing, write nothing.
Step 7: Capture Gate (learning capture)
Gated on settings.learn (resolved in Step 0). If settings.learn is false, skip this step entirely — no gate, no prompt, no writes. (Symmetric with Step 0.5's recall gate: the one knob turns both the read and write sides off.)
When settings.learn is true, after the Step-6 report, run a short capture gate that distills this run's skips into persisted learnings — the write side of the loop whose read side ran in Step 0.5. It mirrors reply-pr's human gate: propose, then stop and wait, then write only what the human confirms. It is record-only — it never edits code, never re-runs a reviewer, never changes the report already emitted.
7.1 Walk the findings
Reuse the Step-6.6 walk if it ran. When Step 6.6 posted a digest it already collected a per-finding disposition — map it here rather than re-asking: fix → tackle, defer / won't-fix (with a reason) → skip (with that reason). Only walk the author from scratch when Step 6.6 was skipped (posting off).
Drive off the Step-5 ledger (its rows already carry the judge's canonical fingerprint) and the Step-6 grouping. Present the surviving findings as a compact list and, for each, mark one of:
- tackle — they'll fix it → capture nothing (a real finding they're acting on is not a false positive).
- skip — they're not acting on it, with a one-line reason.
- skip all — blanket-skip the remainder.
Show it as an editable proposal (a sensible default per finding is fine), never auto-applied.
7.2 Distill a skip into a learning
Only a skip with a generalizable reason becomes a learning; a one-off skip ("not now / out of scope") captures nothing.
- Reason framed as "this specific finding is a known false positive here" → a Suppression, keyed by the finding's ledger fingerprint (§5.1 — use it verbatim; never re-author a fingerprint by hand).
- Reason framed as a general rule ("we never flag X in this repo") → a Convention (a one-line rule).
- If several skips this run share one general reason, propose a single Convention rather than N Suppressions (one confirmation, less noise).
7.3 Confirm, then write (human-confirmed only — D7)
Before writing anything, show the exact entries that would be appended:
- Suppression →
- fingerprint: <fp> | reason: <reason> | added: <today> - Convention →
- <one-line rule>
Wait for explicit approval. On approval, append each via Bash — one call per entry (the script writes the §3.6 format; you never hand-format learnings.md):
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-learn.sh" add-suppression "<ledger-fingerprint>" "<reason>"
"${CLAUDE_PLUGIN_ROOT}/scripts/rc-learn.sh" add-convention "<one-line rule>"
The script creates .review-council/learnings.md (canonical §3.6 skeleton) if absent, appends under the right section, and is idempotent — re-approving an existing learning is a safe no-op, so the gate never has to dedupe against the file. Decline (or settings.learn off) → write nothing; no confirmation for a given entry → skip just that entry.
Check each call's exit status before summarizing. rc-learn.sh exits 0 on a successful (or idempotent no-op) write and 2 on a validation failure — a | or control character in the fingerprint/reason/convention, an empty field, a malformed date — printing the reason on stderr and writing nothing. If a call exits non-zero, surface its stderr message to the user and do not count that entry as captured (the rest still write; the run isn't blocked). Count only the entries that actually landed.
Print a one-line summary of what actually wrote (e.g. Captured: 1 suppression, 1 convention → .review-council/learnings.md; note any failures, e.g. 1 skipped: reason contained "|"), the same observable-artifact spirit as the lens map / ledger — the summary must never report a capture that didn't happen. These entries feed Step 0.5 recall and Step 5 suppression on the next run.
Step 8: Cleanup
Remove any temporary files created during the review:
TMP="${TMPDIR:-/tmp}"
rm -f "$TMP"/rc-*-prompt.* "$TMP"/rc-*-err.* "$TMP"/rc-*.result \
"$TMP"/rc-perplexity-* "$TMP"/rc-cf.*
Note: use "${TMPDIR:-/tmp}", not a hardcoded /tmp — on macOS mktemp puts these files under $TMPDIR (/var/folders/…), so a /tmp-only glob reclaims nothing. The globs cover every temp the run's subagents create: reviewer prompt files (rc-*-prompt.*), stderr captures (rc-*-err.*), the tee'd result files (rc-*.result), Perplexity's payload/response (rc-perplexity-*), and the changed-file list (rc-cf.*). Subagents handle their own temp files, so this is a belt-and-suspenders sweep for anything that leaked (e.g. from a wedged subagent).
Orchestration Rules
- No debate rounds. The pipeline is Round 1 → well-formed check (3.5) → refutation (4) → judge (5) → report (6). There is no Round 2/Round 3 and no revise-toward-consensus; unresolved disagreement is a Dissenting Opinions report section, not another round.
- Substance over style. Aggressively filter out nitpicks, formatting opinions, and subjective preferences.
- Severity is decoupled from agreement. Agreement is a per-finding badge (
[cross-reviewed]/[1 reviewer · unverified]/[unverified]), not the sort key. Cross-family corroboration (≥2 different families, or an UPHELD from a different family) can raise a finding one tier; a single-reviewercriticalstays Critical. Same-family-only agreement promotes nothing. - Be actionable. Every finding must say what to do, not just what's wrong.
- Respect the user's time. The output is a curated, prioritized list — not a dump of everything all reviewers said. Fewer high-quality findings > many low-quality ones.
- Severity definitions:
- Critical — Will cause bugs, security vulnerabilities, data loss, or system failures. Blocks merge/ship.
- Important — Significant quality, performance, or maintainability concern. Should fix.
- Suggestion — Minor improvement opportunity. Nice to have.