woo-review
Spawn a parallel swarm of review sub-agents against a pull request (or the local diff), validate their findings with a Skeptical Validator, and — when a PR is targeted — post a single batched GitHub Review.
This skill is host-agnostic: it works in any AI coding agent that supports sub-agent / task spawning (Claude Code, Cursor, Gemini CLI, opencode, etc.). Hosts without parallel sub-agents fall back to a sequential loop.
Commands
/woo-review— Auto-detect: if the current branch has an open PR (viagh pr view --json number), behave as/woo-review <PR#>. Otherwise review the local diff (no GitHub posting)./woo-review <PR#>— Fetch the PR viagh, run the swarm, and post a native batched GitHub Review./woo-review --full(or@review --fullin a PR comment) — Force a complete re-review even when a prior SHA marker exists. Skips the incremental path described below.woo-review install— Verify local deps (gh,jq,node) and pre-fetchimpeccable+react-doctor(run once per repo).woo-review status— Show the current PR's review status.woo-review address <PR#>— Walk through the PR's unresolved review threads: the skill recommends a verdict per thread (fix / push back / clarify), then you approve the batch or override specific threads before anything is applied. Local hosts only. See Addressing Reviews below.woo-review address <PR#> --auto— Skip the confirmation gate and address autonomously (fix or push back, commit, reply, resolve, and record accept-by-design dismissals to.woo-review/memory.md). This is the pre-walk-through behavior, for trusted/non-interactive runs.
PR-comment triggers (issue #19)
When the companion GitHub Action is installed, the following comment commands re-trigger the review without leaving the PR:
| Comment | Effect |
|---|---|
/woo-review |
Full re-review (sets incremental=off). Equivalent to @review --full. |
/woo-review recheck |
Incremental review of new commits since the last marker. Same path as a synchronize event. |
/woo-review force |
Bypass auto-skip (see Auto-skip below). Combinable: /woo-review force recheck. |
The legacy @review trigger phrase still works; /woo-review is an alias the example workflow's issue_comment if: recognizes.
Auto-skip (bot PRs + release rollups)
prefetch.sh short-circuits the review with a single one-line PR comment when either condition holds (before fetching the diff, so token cost is ~zero):
- PR author matches
authors_skip. Default list:dependabot[bot],renovate[bot],github-actions[bot]. Override with"authors_skip": [...]in.woo-review/config.json; explicit"authors_skip": []opts out entirely. - PR title matches
release_rollup_pattern(Python regex). Default:^(staging|release|chore\(release\)). Override with any string; explicit empty string opts out.
The skip comment carries a <!-- woo-review:skipped --> marker; subsequent triggers on the same PR detect the marker and re-skip silently (no comment spam). To force a full review of a skipped PR, post /woo-review force.
Incremental Mode
By default (incremental: auto on the GitHub Action), every posted review carries a hidden watermark:
<!-- woo-review:sha=<headRefOid> -->
On the next run, prefetch.sh scans bot-authored prior review bodies (the same BOT_NAME_PATTERN used elsewhere) for the marker — non-bot reviewers cannot forge a marker to narrow the window. If found, prefetch diffs <last_sha>...HEAD via the GitHub compare API instead of the full PR diff — only the new commits since the last pass are reviewed. Unresolved prior review threads (any author) are dumped to $OUTDIR/prior-findings.json and consumed by the posting stage as an event floor: any non-empty priors list keeps the new review at minimum REQUEST_CHANGES, a conservative gate so a stale open thread is never auto-resolved by a clean incremental pass.
Override paths:
- Action input
incremental: off(workflow-level opt-out). - A trigger comment containing
--full(e.g.@review --full) — fixed-string match, regex-injection safe. - Force-push that drops
<last_sha>from the branch history — the compare API returns 404; prefetch emits a::warning::and falls back to the full diff for that run.
When the incremental diff has no new commits (i.e. LAST_SHA == HEAD_SHA, e.g. someone re-triggers without pushing), prefetch emits skip=true with reason no new commits since last review (<last_sha>). To force a re-review of the same SHA, pass --full (or set incremental: off).
Marker semantics are state-light: the marker IS the state. There is no DB or workflow artifact retention beyond what GitHub already keeps in review history.
Cross-PR Memory (.woo-review/memory.md)
Reviews stay useful across PRs through a single plain-markdown file in the consumer repo: .woo-review/memory.md. It is the team's running list of gotchas, intentional design choices, and issues a prior review already surfaced and the team consciously accepted. There is no database, no sharded JSONL, no hooks — just a file you can read and edit by hand.
How it's used
- Read as context.
prefetch.shcopies.woo-review/memory.md(if present, 100KB cap) into$OUTDIR/memory.md. Every angle worker and both validator passes treat it as additional rubric and drop any finding the memory already records as known/accepted/wontfix. This is what keeps re-reviews quiet: an issue the team has consciously accepted is not re-flagged on the next PR. - Written inline (local). When you run
/woo-reviewlocally and dismiss a finding (or note a gotcha worth remembering), the skill records the learning in.woo-review/memory.md— first checking that no existing entry already covers it, so the file stays a small deduplicated set of reusable rules rather than a log of every dismissal. The local skill has direct write access — no post-session hook, no permission-isolated job. See Stage 6 below. - Curated by humans. The file is meant to be edited directly. Add a bullet, delete a stale one, group entries under headings — whatever keeps it readable.
Event-floor rule (prior threads)
prior-findings.json (unresolved + resolved threads on the current PR) is still produced for incremental mode, but it is used for one thing only: open prior threads are an event floor — a non-empty set keeps the new review at minimum REQUEST_CHANGES. Resolved threads do not gate the event; a clean incremental pass can APPROVE.
Noise control (severity_floor)
severity_floor defaults to high — by default only high-priority findings surface. Widen it per-repo in .woo-review/config.json ("severity_floor": "low" or "medium"). The validator applies the floor after its own severity check.
Knowledge Aggregation
woo-review wires in domain skills as tool calls inside specific angles, not as a runtime dependency:
| Source | Used by | How |
|---|---|---|
| pbakaus/impeccable | design |
npx -y impeccable detect --json (run once; feeds both quant + qual passes inside the angle prompt) |
| millionco/react-doctor | react |
npx -y react-doctor --diff <base> --offline |
| coreyhaines31/seo-audit framework | seo |
Embedded as the audit rubric in prompts/angles/seo.md |
| openai/security-best-practices | security |
Referenced from prompts/angles/security.md; fetch references/<language>-<framework>-<stack>-security.md via gh api |
| coreyhaines31/ai-seo | aeo |
Embedded as the rubric in prompts/angles/aeo.md; deeper references/ (platform-ranking-factors, content-patterns, content-types) fetched on demand via gh api |
| supabase/supabase-postgres-best-practices | database |
Referenced from prompts/angles/database.md; fetch references/<family>-<topic>.md (security-*, query-*, schema-*, conn-*, lock-*, data-*) on demand via gh api repos/supabase/agent-skills/contents/skills/supabase-postgres-best-practices/references/<file> |
The audit frameworks themselves are embedded in prompts/ (inside this skill bundle) so the skill is self-sufficient. Installing the recommended skills only enhances your host agent's general vocabulary.
Project Rules
Prefetch auto-discovers project rule files (AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules, GEMINI.md) at the repo root, and additionally walks up from each changed file path to collect any AGENTS.md / CLAUDE.md along the way. The discovered content is concatenated (each section prefixed by a ## SOURCE: <path> header, 100KB cap) into $OUTDIR/rules.md and surfaced to every angle as additional rubric. When that file is present, an extra conventions angle fires; the validator drops any finding that claims a rule violation but cannot quote the rule verbatim. Repos without rule files run unchanged.
Per-repo Configuration (.woo-review/config.json)
Drop an optional .woo-review/config.json in the consumer repo to tune the review without forking the skill. Prefetch parses it into $OUTDIR/config.json (canonical copy); downstream stages read from there. Missing file = defaults (severity_floor: high). All keys are optional — specify only the ones you want to override; the rest keep their built-in defaults. Invalid JSON or unknown keys → loud ::error file=.woo-review/config.json,line=N::<msg> annotation and the workflow fails (no silent fallback).
Minimal example — override one knob, everything else stays default:
{ "severity_floor": "medium" }
Full schema (every key shown; all optional):
{
"angles": {
"force": ["database"],
"skip": ["seo"]
},
"severity_floor": "high",
"ignore": [
"**/*.generated.ts",
"migrations/*.sql"
],
"project_rules": [
"constitution.md",
"docs/standards/*.md"
],
"authors_skip": [
"dependabot[bot]",
"renovate[bot]"
],
"release_rollup_pattern": "^(staging|release|chore\\(release\\))",
"models": {
"fast": "anthropic/claude-haiku-4-5",
"standard": "openai/gpt-5",
"deep": "anthropic/claude-opus-4-7"
},
"fix_commands": ["pnpm lint:fix", "pnpm format"],
"disable_adversarial": false,
"chunking": {
"max_loc": 4000
}
}
Key reference (JSON has no comments, so the per-key semantics live here):
angles.force— always run these, even if not auto-detected.angles.skip— never run these (bugs/securitycannot be skipped).severity_floor— one oflow|medium|high; drops findings below the floor. Defaulthigh— only high-priority findings surface. Setlow/mediumfor noisier reviews.ignore— fnmatch globs; ignored paths skip angle triggers + diff body.project_rules— fnmatch globs appended to auto-discoveredrules.md.authors_skip— PR author logins that short-circuit the entire review. Defaults:dependabot[bot],renovate[bot],github-actions[bot]. Set to[]to opt out.release_rollup_pattern— Python regex on the PR title (default shown above; note\\(to escape the paren in JSON). Empty string opts out.models— per-tier slug overrides; the action inputinputs.modelstill wins.fix_commands— reserved for--loopmode (issue #15).disable_adversarial— cost-sensitive opt-out for the prosecutor+defender validator (issue #13). Whentrue, only the defender pass runs and its output becomesfindings.jsondirectly.metrics: opt in to per-angle signal/noise metrics (bool, defaultfalse) — emitfindings.metrics.jsonper run and fold a rolling.woo-review/metrics.jsonaggregate (local only). See Stage 6.5.chunking.max_loc— diff-chunking threshold (issue #14). When the post-ignore diff exceeds this many changed lines, prefetch splits it into chunks honoring workspace package roots > top-level dirs > file-LOC-balanced groups; each angle fans out as angles × chunks parallel sub-agents.0disables chunking; missing => 4000.
Precedence: for the angle set, angles.force beats angles.skip when the same angle is listed in both. For model resolution, the action input inputs.model beats models.<tier> which beats the table default in prompts/_header.md. ignore is applied to both file paths and the per-file diff sections before angle gates evaluate.
/woo-review Workflow
When the user invokes /woo-review [PR#], the host agent MUST perform the following stages. All file paths below are relative to $WOO_REVIEW_ACTION_PATH.
Stage 0 — Resolve skill path
Set WOO_REVIEW_ACTION_PATH to the directory containing this SKILL.md (the installed skill bundle). All prompts/ and scripts/ assets ship inside that directory.
export WOO_REVIEW_ACTION_PATH="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
# Or however your host exposes the skill's install dir (e.g. $SKILL_DIR).
Stage 1 — Prefetch
Build the same $OUTDIR/ artifact tree the GitHub Action builds.
Atomic state.
prefetch.shwipes$OUTDIR(defaults to a per-project/tmp/pr-review-<hash>viascripts/resolve-outdir.sh) before recreating it. Hosts that invoke individual stages directly (skippingprefetch.sh) MUST do the same — stalefindings.<angle>.jsonfrom a prior run will otherwise re-enter the merge step and contaminate the review.OUTDIR override. All scripts (
prefetch.sh,load-config.sh,detect-angles.sh,merge-findings.sh,intersect-findings.sh,chunk-diff.sh,resolve-diff-line.sh) honor theOUTDIRenvironment variable. Hosts that cannot use$OUTDIR/(e.g. sandboxed runtimes with workspace-scoped temp dirs) MUST exportOUTDIR=<their_dir>to every sub-agent. Without that, sub-agents will write findings to a different directory than the merge step reads, silently dropping them.Default is per-project. When
OUTDIRis unset, scripts derive/tmp/pr-review-<hash>from the repo's git toplevel (viascripts/resolve-outdir.sh), so different repos on one machine isolate automatically. The orchestrator resolves it once and exports it to every sub-agent;prefetch.shalso printsoutdir=<path>. Two concurrent runs of the same repo still share one dir — setOUTDIRexplicitly to isolate those.
If a PR number was supplied — export it and invoke prefetch.sh directly. The script handles diff fetch, meta fetch, project-rule discovery, auto-skip checks, and prior-findings extraction. Hosts whose tool gating blocks caller-side $(...) substitution (notably Gemini CLI) MUST use this path — prefetch.sh self-resolves the PR number from the current branch when none is exported and GITHUB_ACTIONS != "true", so callers never need their own subshell.
# Resolve the per-project OUTDIR once and export it so prefetch.sh and every
# sub-agent share one tree. Default: /tmp/pr-review-<hash> derived from the git
# toplevel (scripts/resolve-outdir.sh), so different repos on one machine never
# collide. An explicit OUTDIR (e.g. a sandbox temp dir) is respected as-is.
source "$WOO_REVIEW_ACTION_PATH/scripts/resolve-outdir.sh" # sets + exports OUTDIR
export PR_NUMBER=<n> # optional; prefetch.sh derives it from the branch when unset
bash "$WOO_REVIEW_ACTION_PATH/scripts/prefetch.sh" # prints outdir=<path>; honors the exported OUTDIR
When prefetch resolves a PR number AND finds an open PR, it produces the full artifact tree (diff.txt, meta.json, last_sha.txt, prior-findings.json, rules.md when applicable, memory.md when the consumer repo has .woo-review/memory.md). When no PR resolves, it emits skip=true — the host then falls back to local-diff mode below.
Artifact reference. All paths are under $OUTDIR (default per-project /tmp/pr-review-<hash>/):
| Artifact | Written by | Consumed by | Notes |
|---|---|---|---|
diff.txt |
prefetch.sh |
angle workers, merge-findings.sh |
Full or incremental diff |
meta.json |
prefetch.sh |
all stages | PR metadata (title, files, SHA, author) |
last_sha.txt |
prefetch.sh |
Stage 5 watermark | Present only when a prior watermark was found |
prior-findings.json |
prefetch.sh |
event-floor gate | Unresolved + resolved prior review threads |
rules.md |
prefetch.sh |
conventions angle, validator |
Concatenated project rule files; triggers conventions angle when present |
memory.md |
prefetch.sh |
all angles, validator | Cross-PR memory (.woo-review/memory.md); findings it records as known/accepted are dropped. Present only when the consumer repo has the file |
angles.txt |
detect-angles.sh |
Stage 3 orchestrator | One angle name per line |
findings.<angle>.json |
angle workers | merge-findings.sh |
Raw per-angle output |
raw_findings.json |
merge-findings.sh |
validator passes | Merged, chunk-collapsed findings |
findings.json |
intersect-findings.sh |
Stage 5 posting | Final validated set |
validator-metrics.json |
intersect-findings.sh |
observability | prosecutor_count, defender_count, kept_count, disagreement_count, mode, degraded |
findings.metrics.json |
intersect-findings.sh |
metrics fold, telemetry | Per-angle signal/noise breakdown. Emitted only when metrics: true in config. Keyed by angle: raw_count, prosecutor_kept, defender_kept, kept, dropped_by_defender, dropped_by_prosecutor, blocking_count, nonblocking_count, severity |
If no PR number resolved (local mode):
source "$WOO_REVIEW_ACTION_PATH/scripts/resolve-outdir.sh" # per-project default OUTDIR (or honors an explicit override)
rm -rf "$OUTDIR"
mkdir -p "$OUTDIR"
BASE="$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)"
git diff "$BASE"...HEAD > "$OUTDIR/diff.txt"
# Synthesize meta.json from git for downstream scripts.
git diff --name-only "$BASE"...HEAD \
| jq -R . | jq -s '{
headRefOid: "'"$(git rev-parse HEAD)"'",
baseRefName: "'"$(git rev-parse --abbrev-ref "$BASE@{upstream}" 2>/dev/null || echo main)"'",
title: "(local diff)",
body: "",
files: [.[] | {path: .}]
}' > "$OUTDIR/meta.json"
Stage 2 — Detect Angles
bash "$WOO_REVIEW_ACTION_PATH/scripts/load-config.sh" # parses .woo-review/config.json (defaults severity_floor=high)
bash "$WOO_REVIEW_ACTION_PATH/scripts/detect-angles.sh"
Read the result from $OUTDIR/angles.txt (one angle per line). Always-on angles: bugs, security. Conditional (auto-detected from changed paths + diff body): conventions (when rules.md is present), seo, aeo, design, react, database, tests, api, infra, observability, types, i18n, docs, deps, architecture (when the diff touches general-purpose source files). See scripts/detect-angles.sh for per-angle gating heuristics.
Prefetch also produces optional chunking artifacts when the post-ignore diff exceeds chunking.max_loc (default 4000 LOC). When present, the host MUST fan out one sub-agent per (angle, chunk) pair in Stage 3:
$OUTDIR/chunks.txt— chunk IDs, one per line (chunk-0,chunk-1, …).$OUTDIR/chunks.json— manifest:[{id, files, loc, diff_path, boundary}].$OUTDIR/diff.chunk-<id>.txt— per-chunk diff (a validdiff --gitstream).
Boundary precedence: workspace packages (packages/<name>/, apps/<name>/, services/<name>/, libs/<name>/) → top-level directories → file-LOC-balanced split. When chunks.txt is absent the diff is under threshold and chunking is a no-op.
Stage 3 — Spawn Parallel Sub-Agents (one per angle, × chunk if chunked)
This is the swarm step. For each detected angle, spawn a sub-agent in parallel using your host's primitive:
- Claude Code:
Tasktool, one call per angle in a single message. - Cursor / Composer: parallel subagent dispatch.
- Gemini CLI: built-in
@generalistsubagent, one@generalistper angle in the same response (seeprompts/google.md). Parallel-vs-sequential dispatch of multiple@<agent>calls in a single turn is not formally documented today; treat as best-effort parallel — the isolation pattern still buys token economy even if Gemini serializes internally. - opencode: parallel subagent dispatch via the OpenCode runtime's primitive (see
prompts/opencode.md); falls back to a sequential loop when the build does not support it.
Each sub-agent receives the same brief:
You are the <angle> reviewer for this PR. Read:
- $WOO_REVIEW_ACTION_PATH/prompts/_header.md (shared contract)
- $WOO_REVIEW_ACTION_PATH/prompts/angles/<angle>.md (your scope)
- $OUTDIR/diff.txt, $OUTDIR/meta.json (OUTDIR is exported by the orchestrator; prefer it over any literal path)
Execute any shell commands the angle prompt specifies (e.g. impeccable detect,
react-doctor). Write your findings as a JSON array to
$OUTDIR/findings.<angle>.json per the schema in _header.md. The file MUST
start with `[` and end with `]` — no preamble, no commentary, no markdown
fences. Before writing each finding's `line` field, validate it via
`bash $WOO_REVIEW_ACTION_PATH/scripts/resolve-diff-line.sh --file <path> --line <N>`
and drop the finding when the helper prints `null` (the line is not anchorable
on the diff's RIGHT side and the GitHub API will reject the comment). EXIT.
Chunked fan-out. When $OUTDIR/chunks.txt exists, spawn one sub-agent per (angle, chunk_id) instead of one per angle. Pass the chunk ID in the brief, and tell the sub-agent to read $OUTDIR/diff.chunk-<id>.txt and write $OUTDIR/findings.<angle>.chunk-<id>.json. The validator pass still runs once globally — merge-findings.sh collapses any within-angle duplicates across chunks before validation, and the validator handles cross-angle dedup as today.
Sub-agents MUST NOT post comments, edit the PR, touch other angles' files, run prefetch.sh, or delete/recreate $OUTDIR. prefetch.sh is a Stage-1-only operation; re-running it mid-swarm wipes meta.json / prior-findings.json and corrupts the posting stage (issue #48).
Model routing (token optimization, host-agnostic). Each angle prompt and the validator declare a tier: in frontmatter — fast, standard, or deep. The host resolves the tier to a concrete model via the table in prompts/_header.md. Tier assignments:
| Stage | Tier | Why |
|---|---|---|
| Context+summary subagent | fast |
Mechanical summarization. |
bugs, security workers |
standard |
Reasoning-heavy: correctness + threat model. |
architecture worker |
standard |
Structural-quality / code-judo judgment; high-subjectivity, needs reasoning depth. |
design, react workers |
standard |
Heuristic + Rules-of-Hooks judgment after deterministic tools. |
database worker |
standard |
Postgres correctness, RLS reasoning, plan/index judgment. |
tests, api, infra workers |
standard |
Coverage/contract/IaC reasoning. |
seo, aeo workers |
fast |
Rubric checklists; no novel reasoning. |
observability, types, i18n, docs, deps workers |
fast |
Pattern matching + diff-anchored hygiene checks. |
| Skeptical Validator | deep |
Highest-leverage step — strictest false-positive filter pays for itself. |
Per-provider resolution (full table in _header.md):
| Tier | Anthropic | OpenAI | OpenRouter | |
|---|---|---|---|---|
fast |
claude-haiku-4-5 |
gpt-5-mini |
gemini-3-5-flash |
openrouter/deepseek/deepseek-v4-flash |
standard |
claude-sonnet-4-6 |
gpt-5 |
gemini-3-5-flash |
openrouter/deepseek/deepseek-v4-pro |
deep |
claude-opus-4-7 |
gpt-5 + reasoning_effort: high |
gemini-3-5-flash |
openrouter/deepseek/deepseek-v4-pro + reasoning_effort: xhigh |
- Google currently exposes only
gemini-3-5-flash— tier routing is a no-op on Gemini until a larger 3.5 model ships. - OpenAI GPT-5 reasoning is a
reasoning_effortparameter (minimal/low/medium/high), not a slug suffix. There is nogpt-5-pro. Newergpt-5.5family exists; upgrade once the Codex Action supports it. - OpenRouter exposes only
deepseek/deepseek-v4-flashanddeepseek/deepseek-v4-pro; reasoning is thereasoning_effortparameter (high/xhigh). Do not route todeepseek-r1— V4 supersedes it.
Host capability:
- Per-call routing (Claude Code
Task, opencode@subagent): honor each prompt'stier:verbatim. Maximum savings. - Single model per session (Codex Action, Gemini CLI): pin the run to the
standardtier — covers every angle safely.tier:becomes informational. Split into multiple jobs if you want fast-tier savings on rubric angles or deep-tier validation.
Stage 4 — Merge + Adversarial Validation
After every sub-agent has finished:
bash "$WOO_REVIEW_ACTION_PATH/scripts/merge-findings.sh"
# Produces $OUTDIR/raw_findings.json
Validation runs as an adversarial pipeline (issue #13): two opposing-bias deep-tier validator passes followed by a deterministic intersection. The intersection (findings BOTH passes agree to keep) is what authors see — this trades 2× validator cost for materially higher signal-to-noise.
Read disable_adversarial from $OUTDIR/config.json:
DISABLE_ADV="$(jq -r '.disable_adversarial // false' $OUTDIR/config.json 2>/dev/null || echo false)"
Stage 4a — Prosecutor pass (skip if DISABLE_ADV == true):
Run prompts/validator-prosecutor.md. Bias: assume each finding is real; drop only the clearly wrong. Writes $OUTDIR/findings.prosecutor.json and exits.
Stage 4b — Defender pass (prompts/validator.md):
- Dedupe across angles (keep the most actionable description; preserve the winner's
title/description/fix). - Defense-attorney audit: try to prove each finding wrong. Drop pedantic / style-only / lint-catchable / "maybe" findings.
- Severity check: you MAY downgrade (HIGH → MEDIUM, blocking true → false). You MAY NOT upgrade.
- Comment-shape check: every surviving finding has
title(bold headline ≤60 chars),description(issue only, no fix), andfix(recommended change in prose). Split overloadeddescriptionfields when an angle collapsed them. fix_typeenforcement: every surviving finding MUST carryfix_type("suggestion"or"prose"). Downgrade anyfix_type: "suggestion"that violates the ≤10-line / single-file / self-contained / no-placeholder / no-fence-break rules — setfix_type: "prose"andsuggestion: null. Full rule list lives inprompts/validator.mdstep 7.- Writes
$OUTDIR/findings.defender.json.
Swarm workers stop here. In the chat-host swarm the defender writes
findings.defender.jsonand EXITs — the orchestrator owns Stage 4c (intersect) and Stage 5 (post). LeaveWOO_REVIEW_SEQUENTIAL_VALIDATEunset when running as a swarm worker — the GitHub Action'svalidatemode sets it because there one sequential agent owns the whole tail; a swarm host has separate orchestrator and worker roles, so the worker must not see it. Pointing a swarm defender atvalidator.mdwith the flag unset is the safe default — its Step 3/3b/4 gate keeps it from racing the prosecutor, posting prematurely, or mutating$OUTDIR.
Stage 4c — Intersect:
bash "$WOO_REVIEW_ACTION_PATH/scripts/intersect-findings.sh"
Verify + retry (mirror the Stage 3 angle-retry guard). Before intersect, run this detection check; it only reports — you (the orchestrator) perform the re-launch described after it:
for f in findings.prosecutor.json findings.defender.json; do
if ! jq -e 'type == "array"' "$OUTDIR/$f" >/dev/null 2>&1; then
echo "missing/non-array: $f — orchestrator must re-launch this pass once (see below)"
fi
done
Re-launch a missing pass exactly once (prosecutor → validator-prosecutor.md; defender → validator.md), then re-run intersect. If a pass is still missing after the retry, intersect proceeds defender-only and sets degraded: true in validator-metrics.json.
Surface degradation. After intersect, read validator-metrics.json:
jq -r '.degraded // false' $OUTDIR/validator-metrics.json
If true, tell the user in your orchestrator summary that the review is defender-only / lower-confidence — the posting stage also appends a ⚠️ line to the review body (_header.md). A disable_adversarial: true opt-out reports degraded: false and needs no warning.
Produces $OUTDIR/findings.json — the final validated set — and $OUTDIR/validator-metrics.json with prosecutor_count, defender_count, kept_count, disagreement_count. Intersection key is (file, line, title-stem). When disable_adversarial: true is set or findings.prosecutor.json is absent, the script copies defender output verbatim and tags metrics mode: defender-only. Severity = min(prosecutor, defender), blocking = prosecutor.blocking AND defender.blocking, other fields take the defender's copy.
Stage 5 — Report
If invoked with a PR number — post a single native batched GitHub Review per the procedure in prompts/_header.md:
- Build the STATUS_LINE (
APPROVED/APPROVED WITH SUGGESTIONS/CHANGES REQUESTED). - Submit one
gh api repos/<repo>/pulls/<PR>/reviewsPOST containing all inline comments + the summary + status line. The reviewevent(APPROVE/COMMENT/REQUEST_CHANGES) is the native gate — any blocking finding triggersREQUEST_CHANGES. - DO NOT modify the PR title or body. DO NOT mutate PR labels.
If invoked locally (no PR#) — print the validated findings to the terminal grouped by severity, then stop. Do not touch any remote.
Stage 6 — Update cross-PR memory (local hosts)
After reporting, when the user dismisses a finding as a known/intentional/accepted issue, or tells you a gotcha worth remembering, record the learning — not the individual issue resolution. The goal is a small, deduplicated set of generalizable rules ("the team accepts X pattern because Y"), not a growing log of every finding ever dismissed.
Before writing anything:
- Read the existing
.woo-review/memory.md(it was already loaded to$OUTDIR/memory.mdin Stage 1; re-read the repo copy in case it changed). - Check coverage. If an existing entry already captures this learning — even phrased differently, or scoped more narrowly/broadly — do NOT append a duplicate. If the existing entry is close but the new dismissal generalizes it (e.g. the same pattern in a second file), edit that entry to widen its scope rather than adding a near-duplicate.
- Only when the learning is genuinely new, append one short bullet phrased as a reusable rule, then stop.
mkdir -p .woo-review
# Append ONLY after confirming no existing entry covers this learning.
printf -- '- %s\n' "<general pattern>: <why it is accepted / what not to re-flag>" >> .woo-review/memory.md
Phrase entries as patterns, not instances — prefer "Generated *.pb.go files are intentional; do not flag their style" over "dismissed line 42 in user.pb.go". The local skill writes this file directly — no post-session hook, no permission-isolated job. Only record on an explicit dismissal or a stated gotcha — never auto-record every finding. Do NOT write memory in CI: the GitHub Action's validator job holds contents: read and posts the review only; memory is curated locally and by humans editing the file. Memory is read back as review context on the next run (Stage 1) and the validator drops findings it records.
Addressing Reviews (woo-review address <PR#>, local hosts)
Stage 6 only fires when a finding is dismissed during a live local run. For
PR-targeted reviews the accept/dismiss decision happens later, on the PR
(often in a separate comment-addressing session) — so Stage 6's memory write
structurally never fires for the primary flow (issue #53). The address verb
closes that gap by owning the comment-addressing flow itself, with the memory
write at the moment a dismissal is confirmed — by the user in the default
interactive flow, or by the skill itself under --auto.
address is local only — it commits, pushes, and writes memory, none of
which the GitHub Action's contents: read validator job can do.
Lifecycle (A1→A7):
- Fetch — resolve the PR# (explicit arg, else the current branch's open PR), then
bash "$WOO_REVIEW_ACTION_PATH/scripts/fetch-threads.sh"writes every unresolved thread (any author) to$OUTDIR/address-threads.json. Memory + config are loaded as in Stage 1. - Precondition — the working tree must be clean and the current branch must be the PR head. Otherwise abort before any edit; tell the user to checkout the PR head on a clean tree.
- Reception loop (analysis only) — per thread, follow
prompts/address.md: read → understand → verify → evaluate → recommendFIX/ACCEPT/CLARIFY. The loop makes no working-tree edits, no replies, no resolves, and no memory writes; it stages a recommended verdict + reasoning per thread. - Verdict gate — default: present all recommendations as one batched table (thread → finding → recommended verdict → reasoning); the user approves all or overrides specific threads. The final verdict per thread is the override where given, else the recommendation. Skipped under
--auto(final = recommended). With no--autoon a non-interactive host where confirmation can't be obtained, abort with guidance to re-run with--auto— never act unapproved. - Commit + push — apply all final
FIXedits to the working tree → one commit referencing the threads → push to the PR head → capture<sha>(before any reply, so "Fixed in<sha>" is real). Never force-push. - Reply + resolve + memory — per handled thread,
scripts/resolve-thread.shposts the reply then resolves (CLARIFY threads useRESOLVE=0: reply only, left open). Only a finalACCEPTwrites memory. - Report — summary table: thread → recommended → final → action → memory-written?
Only a final ACCEPT (accept-by-design — an ACCEPT the user kept in the
default flow, or one the skill produced itself under --auto) writes memory,
deduplicated and phrased as a reusable pattern — never a log of every fix.
Memory is read back as context on the next review run (Stage 1), keeping
re-reviews quiet.
Stage 6.5 — Fold per-angle metrics (local hosts, opt-in)
Only when the consumer repo sets metrics: true in .woo-review/config.json. The
per-run findings.metrics.json (written by intersect-findings.sh) is folded into a
rolling, per-clone aggregate at .woo-review/metrics.json. The fold script also
ensures that path is gitignored — the aggregate is local data, never committed
(cross-host aggregation is the job of the opt-in central sink, a separate feature).
bash "$WOO_REVIEW_ACTION_PATH/scripts/metrics-fold.sh"
This is a no-op when metrics is off or no per-run record exists. As with memory, the
GitHub Action does not fold — its job is contents: read + post; metrics persistence
is local only (the action uploads findings.metrics.json as a build artifact instead).
Reading the aggregate. Rank angles by validator-drop rate (noise candidates first):
jq -r '.angles | to_entries
| map({angle: .key,
raw: .value.raw_total,
drop_rate: (if .value.raw_total > 0 then .value.dropped_by_defender_total / .value.raw_total else 0 end),
keep_rate: (if .value.raw_total > 0 then .value.kept_total / .value.raw_total else 0 end)})
| sort_by(-.drop_rate)[]
| "\(.angle)\traw=\(.raw)\tdrop=\((.drop_rate*100|floor))%\tkeep=\((.keep_rate*100|floor))%"' \
.woo-review/metrics.json
A high raw with a high drop rate is a noise candidate; a high keep rate is a useful angle.
Architecture
detect ─► fan-out (parallel sub-agents, one per angle) ─► merge ─► skeptical validator ─► post
This mirrors the cloud GitHub Action exactly (.github/workflows/reusable-review.yml), just with sub-agents standing in for GHA matrix jobs.
Companion GitHub Action
For a fully-managed CI flow, drop this into the consumer repo at .github/workflows/ai-review.yml:
name: AI PR Review
on:
pull_request:
types: [opened, reopened, ready_for_review]
issue_comment:
types: [created]
jobs:
review:
uses: howarewoo/woo-review/.github/workflows/reusable-review.yml@v0.1.0
with:
provider: anthropic
secrets:
anthropic_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
Zero local setup required in the consumer repo — the action ships its own prompts, scripts, and Node tools.
Best Practices
- Always parallelize Stage 3 when the host supports it; the validator pass is calibrated for ~5 angles' worth of input.
- Trust the Skeptical Validator. Disabling it produces noisy reviews.
- Honor angle-prompt tiers (
fast/standard/deep) when the host supports per-call model routing. Hosts that run one model per session should pin thestandardtier model (table above) — this matches the May 2026 flagship recommendation. - Pass
disable_anglesto skip optional angles when scope is narrow (e.g. backend-only PR →disable_angles: "seo,aeo,design,react,i18n").
Troubleshooting
- Missing artifacts in cloud mode — verify the
detectjob uploadedreview-artifacts. - Empty validator output — inspect
$OUTDIR/raw_findings.json. If empty, no angle wrote findings; check eachfindings.<angle>.json. - Sub-agents posting prematurely — re-read the Stage 3 brief; workers must write JSON only.
gh api ... /reviewsreturns HTTP 422 "Line could not be resolved" — a finding'slinefield did not map to a+or context line on the diff's RIGHT side. The merge step now drops these viaresolve-diff-line.sh, but mismatches outside the helper's reach can still slip through. Re-run with the resolver enabled (it runs by default inmerge-findings.sh) and inspect$OUTDIR/diff-line-cache.jsonto see which lookups returnednull.- Stale findings from a prior run —
prefetch.shnow wipes$OUTDIRbefore recreating it. Hosts that skipprefetch.shMUSTrm -rf "$OUTDIR"themselves; otherwise files likefindings.bugs.jsonfrom an earlier session leak into the merge step. detect-angles.shcrashes outside GitHub Actions — fixed: the script now emitsangles=/chunks_json=lines to stdout and writes$OUTDIR/angles.json+$OUTDIR/chunks-matrix.jsonwhen$GITHUB_OUTPUTis unset. Inspect those files when running locally.- Sub-agent writes findings to the wrong path — caused by host workspace drift (the sub-agent's CWD differs from the orchestrator's). Export
OUTDIRto every sub-agent — see Stage 1. - Adversarial validators dropped a finding both passes agreed on — the intersection now applies a fuzzy second pass (
±10lines, prefix-20 title-stem match). Check$OUTDIR/validator-metrics.jsonfordisagreement_count; surprises usually mean title-stem prefix mismatch, not line drift. - Caller-side
PR_NUMBER="$(gh pr view ...)"blocked by host sandbox — some hosts (Gemini CLI, sandboxed runtimes) reject inline$(...)substitutions on tool calls. Skip the caller-side resolution:bash $WOO_REVIEW_ACTION_PATH/scripts/prefetch.shderives the PR number itself from the current branch whenPR_NUMBERis unset andGITHUB_ACTIONS != "true". prefetch.shskipped with "bot already commented and trigger is not explicit" on a local run — fixed: that re-run guard now only applies inside GitHub Actions (`G
…(truncated)