Adversarial Review
Review a change-set with a two-stage multi-agent pipeline: find (parallel
read-only reviewers, each owning one failure dimension) then verify (an
independent skeptic per finding, prompted to refute it). Only findings that
survive verification are reported. This kills the two classic failure modes of
LLM review: plausible-but-wrong findings, and one reviewer trying to hold every
concern at once.
This skill authorizes use of the Workflow tool for the orchestration.
1. Establish scope (cheap, inline)
- Default: the working tree —
git diff HEAD --stat plus git status --short
for untracked files. If the tree is clean, use commits ahead of the default
branch (git log origin/main..HEAD); if none, the last commit.
- If args name a base ref, PR number (
gh pr diff <n> --stat), or paths, scope
to that instead.
- Build a SCOPE block: repo path, the changed-file list grouped by
feature/area if discernible (read the recent commit messages), and one line
of context per group ("X was just implemented by engineer A who did not know
about Y"). If several independent changes landed in sequence, SAY SO in the
scope — it unlocks the seams dimension.
- Note anything reviewers must NOT do (modify files, run test suites that
would race a separate verification run).
2. Pick dimensions
Default four; drop or add based on what the diff actually contains:
| Dimension |
Hunts for |
| correctness |
real bugs: off-by-one, wrong clamps, unsorted-input assumptions, binary-search edges, stale caches, async races, undo/transaction leaks (begin without end on throw) |
| seams |
CROSS-CHANGE integration bugs — each change was made by someone who didn't know the later ones existed; name concrete interaction pairs to check (the highest-yield dimension on multi-feature diffs) |
| silent-failures |
swallowed errors: empty catch, no response.ok check, fire-and-forget void promises, schema .optional() hiding malformed data, handlers reporting success on failure, UI showing stale state when an async step fails |
| test-gaps |
the 3–5 most dangerous untested behaviors only (persistence round-trips, undo of multi-step ops, boundary math, timestamp math at hour boundaries) — not blanket coverage demands |
Useful extras when relevant: security (injection, path traversal, secrets,
authz), perf (hot-path regressions, N+1, unbounded growth), concurrency.
3. Run the workflow
The script below is saved as a named workflow at
~/.claude/workflows/adversarial-review.js. Invoke it by name instead of
regenerating it (a fresh script is 7-15k output tokens, 2-4 minutes of typing):
Workflow({ name: 'adversarial-review', args: {
scope: '<repo, change-set description, per-area context>',
dimensions: [ /* optional [{key, prompt}]; defaults: correctness, silent-failures, integration, test-gaps */ ],
votes: 1, // 3 for very high stakes: three lenses per finding, majority vote
} })
Reference shape (only paste inline if the named workflow is unavailable; keep
the verification contract verbatim):
export const meta = {
name: 'adversarial-review',
description: 'Adversarial review of the current change-set',
phases: [
{ title: 'Find', detail: 'read-only finders per dimension' },
{ title: 'Verify', detail: 'adversarial verification of each finding' },
],
}
const SCOPE = `<repo, change-set description, per-area context, READ ONLY:
do not modify files or run builds/tests. Cite file:line for every claim.>`
const FINDINGS = {
type: 'object', required: ['findings'],
properties: { findings: { type: 'array', items: {
type: 'object', required: ['title', 'file', 'severity', 'detail'],
properties: {
title: { type: 'string' },
file: { type: 'string', description: 'file:line' },
severity: { type: 'string', enum: ['critical', 'major', 'minor'] },
detail: { type: 'string', description: 'what is wrong, the concrete failure scenario, and the suggested fix' },
} } } },
}
const DIMENSIONS = [ /* { key, prompt } per chosen dimension */ ]
phase('Find')
const found = await pipeline(
DIMENSIONS,
(d) => agent(
`${SCOPE}\nYour dimension: ${d.prompt}\nReport at most 8 findings; only report things you are confident are real after reading the actual code (not the diff alone — open the files). No style nits.`,
{ label: `find:${d.key}`, phase: 'Find', schema: FINDINGS }),
(result, d) => parallel((result?.findings ?? []).map((f) => () =>
agent(
`${SCOPE}\nAdversarially verify this finding from a ${d.key} reviewer. Read the cited code and trace the actual behavior. Default to refuted unless the failure scenario is concretely reachable. Finding:\n${JSON.stringify(f, null, 1)}`,
{ label: `verify:${f.title.slice(0, 30)}`, phase: 'Verify', schema: {
type: 'object', required: ['real', 'reason'],
properties: { real: { type: 'boolean' }, reason: { type: 'string' }, fixHint: { type: 'string' } },
} }).then((v) => ({ ...f, dimension: d.key, verdict: v }))
))
)
const flat = found.filter(Boolean).flat().filter(Boolean)
const confirmed = flat.filter((f) => f.verdict?.real)
log(`${confirmed.length} confirmed, ${flat.length - confirmed.length} refuted`)
return {
confirmed,
refutedTitles: flat.filter((f) => f.verdict && !f.verdict.real).map((f) => f.title),
}
Why each piece matters — keep these properties when adapting:
- Finders are read-only and capped ("at most 8, no style nits") — volume
is the enemy; verification costs one agent per finding.
- Verifiers see the finding but not the finder's reasoning chain, read the
cited code fresh, and default to refuted. A finding must describe a
concretely reachable failure to survive. This is the false-positive filter.
- pipeline(), not a barrier — each dimension's findings verify while other
dimensions are still searching.
- For very high stakes, use 3 verifiers per finding with distinct lenses
(reachability / severity / does-the-fix-direction-hold) and majority vote.
If the Workflow tool is unavailable, degrade gracefully: run the finders as
parallel Agent (subagent) calls in one message, then a verification subagent
per finding, same prompts.
4. Report and act
- Report ONLY confirmed findings: severity, file:line, the reachable failure
scenario in one or two sentences, and the fix direction. List refuted titles
in one line (it shows the filter worked).
- Spot-check any confirmed
critical yourself by reading the cited code
before fixing — verifiers are good, not infallible.
- If some verifications failed to run (limits, errors), say so explicitly and
vet those findings yourself by reading the code — never silently drop an
unverified critical.
- With
--fix (or when the user asks): fix confirmed findings in
severity order, each with a regression test where the failure scenario is
testable, then re-run the project's tests/lint/build and report.
1---2name: adversarial-review3description: Multi-agent adversarial code review - parallel finders, verified by a skeptic. Use for "adversarial review" or "did the agents break anything".4---56# Adversarial Review78Review a change-set with a two-stage multi-agent pipeline: **find** (parallel9read-only reviewers, each owning one failure dimension) then **verify** (an10independent skeptic per finding, prompted to refute it). Only findings that11survive verification are reported. This kills the two classic failure modes of12LLM review: plausible-but-wrong findings, and one reviewer trying to hold every13concern at once.1415This skill authorizes use of the Workflow tool for the orchestration.1617## 1. Establish scope (cheap, inline)1819- Default: the working tree — `git diff HEAD --stat` plus `git status --short`20 for untracked files. If the tree is clean, use commits ahead of the default21 branch (`git log origin/main..HEAD`); if none, the last commit.22- If args name a base ref, PR number (`gh pr diff <n> --stat`), or paths, scope23 to that instead.24- Build a SCOPE block: repo path, the changed-file list grouped by25 feature/area if discernible (read the recent commit messages), and one line26 of context per group ("X was just implemented by engineer A who did not know27 about Y"). If several independent changes landed in sequence, SAY SO in the28 scope — it unlocks the seams dimension.29- Note anything reviewers must NOT do (modify files, run test suites that30 would race a separate verification run).3132## 2. Pick dimensions3334Default four; drop or add based on what the diff actually contains:3536| Dimension | Hunts for |37|---|---|38| correctness | real bugs: off-by-one, wrong clamps, unsorted-input assumptions, binary-search edges, stale caches, async races, undo/transaction leaks (begin without end on throw) |39| seams | CROSS-CHANGE integration bugs — each change was made by someone who didn't know the later ones existed; name concrete interaction pairs to check (the highest-yield dimension on multi-feature diffs) |40| silent-failures | swallowed errors: empty catch, no `response.ok` check, fire-and-forget `void` promises, schema `.optional()` hiding malformed data, handlers reporting success on failure, UI showing stale state when an async step fails |41| test-gaps | the 3–5 most dangerous untested behaviors only (persistence round-trips, undo of multi-step ops, boundary math, timestamp math at hour boundaries) — not blanket coverage demands |4243Useful extras when relevant: `security` (injection, path traversal, secrets,44authz), `perf` (hot-path regressions, N+1, unbounded growth), `concurrency`.4546## 3. Run the workflow4748The script below is saved as a named workflow at49`~/.claude/workflows/adversarial-review.js`. Invoke it by name instead of50regenerating it (a fresh script is 7-15k output tokens, 2-4 minutes of typing):5152```js53Workflow({ name: 'adversarial-review', args: {54 scope: '<repo, change-set description, per-area context>',55 dimensions: [ /* optional [{key, prompt}]; defaults: correctness, silent-failures, integration, test-gaps */ ],56 votes: 1, // 3 for very high stakes: three lenses per finding, majority vote57} })58```5960Reference shape (only paste inline if the named workflow is unavailable; keep61the verification contract verbatim):6263```js64export const meta = {65 name: 'adversarial-review',66 description: 'Adversarial review of the current change-set',67 phases: [68 { title: 'Find', detail: 'read-only finders per dimension' },69 { title: 'Verify', detail: 'adversarial verification of each finding' },70 ],71}7273const SCOPE = `<repo, change-set description, per-area context, READ ONLY:74do not modify files or run builds/tests. Cite file:line for every claim.>`7576const FINDINGS = {77 type: 'object', required: ['findings'],78 properties: { findings: { type: 'array', items: {79 type: 'object', required: ['title', 'file', 'severity', 'detail'],80 properties: {81 title: { type: 'string' },82 file: { type: 'string', description: 'file:line' },83 severity: { type: 'string', enum: ['critical', 'major', 'minor'] },84 detail: { type: 'string', description: 'what is wrong, the concrete failure scenario, and the suggested fix' },85 } } } },86}8788const DIMENSIONS = [ /* { key, prompt } per chosen dimension */ ]8990phase('Find')91const found = await pipeline(92 DIMENSIONS,93 (d) => agent(94 `${SCOPE}\nYour dimension: ${d.prompt}\nReport at most 8 findings; only report things you are confident are real after reading the actual code (not the diff alone — open the files). No style nits.`,95 { label: `find:${d.key}`, phase: 'Find', schema: FINDINGS }),96 (result, d) => parallel((result?.findings ?? []).map((f) => () =>97 agent(98 `${SCOPE}\nAdversarially verify this finding from a ${d.key} reviewer. Read the cited code and trace the actual behavior. Default to refuted unless the failure scenario is concretely reachable. Finding:\n${JSON.stringify(f, null, 1)}`,99 { label: `verify:${f.title.slice(0, 30)}`, phase: 'Verify', schema: {100 type: 'object', required: ['real', 'reason'],101 properties: { real: { type: 'boolean' }, reason: { type: 'string' }, fixHint: { type: 'string' } },102 } }).then((v) => ({ ...f, dimension: d.key, verdict: v }))103 ))104)105const flat = found.filter(Boolean).flat().filter(Boolean)106const confirmed = flat.filter((f) => f.verdict?.real)107log(`${confirmed.length} confirmed, ${flat.length - confirmed.length} refuted`)108return {109 confirmed,110 refutedTitles: flat.filter((f) => f.verdict && !f.verdict.real).map((f) => f.title),111}112```113114Why each piece matters — keep these properties when adapting:115116- **Finders are read-only and capped** ("at most 8, no style nits") — volume117 is the enemy; verification costs one agent per finding.118- **Verifiers see the finding but not the finder's reasoning chain**, read the119 cited code fresh, and **default to refuted**. A finding must describe a120 concretely reachable failure to survive. This is the false-positive filter.121- **pipeline(), not a barrier** — each dimension's findings verify while other122 dimensions are still searching.123- For very high stakes, use 3 verifiers per finding with distinct lenses124 (reachability / severity / does-the-fix-direction-hold) and majority vote.125126If the Workflow tool is unavailable, degrade gracefully: run the finders as127parallel Agent (subagent) calls in one message, then a verification subagent128per finding, same prompts.129130## 4. Report and act131132- Report ONLY confirmed findings: severity, file:line, the reachable failure133 scenario in one or two sentences, and the fix direction. List refuted titles134 in one line (it shows the filter worked).135- Spot-check any confirmed `critical` yourself by reading the cited code136 before fixing — verifiers are good, not infallible.137- If some verifications failed to run (limits, errors), say so explicitly and138 vet those findings yourself by reading the code — never silently drop an139 unverified critical.140- With `--fix` (or when the user asks): fix confirmed findings in141 severity order, each with a regression test where the failure scenario is142 testable, then re-run the project's tests/lint/build and report.