Elenchus
ἔλεγχος — the Socratic cross-examination: you do not confirm what the author meant, you hunt the one input that refutes the claim the change rests on. A change is innocent of nothing until you have tried to break it and failed. You are the independent adversary in the chair, never the author defending the diff.
This skill exists because the same model that writes a change is a poor judge of it: the author reviews what they intended, under ship-it momentum, against a mental model that hides what the code actually does. The fix is not a smarter model — it is an independent reader running a fixed adversarial protocol against the whole repo, reproducing as it goes. That is the entire method. Everything below operationalizes it.
The cardinal rule: independence is mechanical
If you (or this session) authored the change under review, you are the wrong reviewer by construction — you cannot un-know what it was supposed to do. Restore independence with tools, not willpower:
- Primary: run
/code-reviewathigh(spawns independent local review agents) orultra(multi-agent cloud review) so the verdict comes from a context that never wrote the code. Scale to risk:ultra/highfor anything destructive, security/governance-sensitive, or shipping to a sensitive target. - Fallback / supplement: dispatch a fresh review subagent (the
Task/Agent tool) with the diff and this rubric but no authorship narrative.
Never hand-read your own diff and call it reviewed. A clean self-eyeball is not a review; it is the author agreeing with themselves.
Repo context
cat .claude/skill-context.md 2>/dev/null || echo "(no .claude/skill-context.md — elenchus still runs tier-0; infer the test/run commands from the Makefile or ask. Add an optional \`## elenchus\` block for repo-specific hints; see Scaffolding below.)"
Read the ## elenchus section if present (optional tier-1 hints — known destructive ops, load-bearing surfaces, reproduce recipes, what "the feature works" means here). For the commands to actually run the code, fall back in order to ## audit (lint/test targets), ## theoros (repl_command), then the repo Makefile. Tier-0 needs no config.
Protocol
Run these phases in order. Do not skip a phase because the change "looks small" — the highest-value bugs hide in changes that look small.
Phase 0 — Frame the load-bearing claim
State, in one sentence, the single claim the change rests on ("this fix makes X portable", "this feature lets the model cite summaries", "this refactor is behavior-preserving"). That claim is your hypothesis to break. Everything after is an attempt to find the input, state, or path that falsifies it.
Phase 1 — Drive the engine
Run /code-review (effort per the independence rule). Treat its findings as input, not verdict — you still owe the three passes below, which /code-review does not force.
Phase 2 — The three passes diff-reading skips
The bugs that pure diff review loses live here. Each one is invisible in the +/- and surfaces only by reading or running the code around the change.
- Reproduce the load-bearing claim. Run it. Build the input matrix and check the boundary cells:
null/undefined/ empty-string / zero-rows / first / last / the>= Nedge. Re-derive any number, table, or "all green" the change claims rather than trusting it. If you cannot reproduce, say so — that is a finding. - Trace every consumer.
grep/globevery call-site of every symbol the change touched, across the whole tree, not the diff. Ask of each: does this caller still hold? Did the change reach the surface that actually matters (the docstring the model reads, the wrapper that gets registered, the path that ships)? (This is how a feature passes every test and still ships dead: the enriched guidance landed on the inner function, not the wrapper the harness surfaces.) - Review against
main, not the diff. Read the unchanged code the change now interacts with. Old persisted state, sibling code paths, and callers outside the diff are all in scope.
Phase 3 — The rubric
Walk every cell. For each, either find the falsifying case or explicitly clear it.
Phase 4 — Report
Emit the structured report (format below). End with an explicit merge verdict and what you are holding on, if anything.
Phase 5 — Post the distilled summary
If an open PR exists for the branch under review, post the distilled summary (contract in Posting the summary below) as a single gh pr comment. This is default behavior, not opt-in — the review of record belongs on the PR. Skip only when there is no PR (local / pre-PR review → the report stays in-session) or the invoker explicitly said to keep it local.
The in-session report and the posted comment are not the same document. The report ends in a verdict; whether the comment may is decided by who authored the PR. Derive that before writing the body — never assume.
The rubric (load-bearing)
| # | Cell | The question that finds the bug |
|---|---|---|
| 1 | Destructive-op reachability | For every rm -rf / overwrite / DROP / truncate / force-push, name the exact input or invocation that reaches it. Don't assume it's unreachable — find the user command that makes the source resolve under the wipe. |
| 2 | Parallel-path mirroring | When a path mirrors an existing one, were the guards and the regression tests mirrored too — or only the structure? Find the cell the new path leaves untested. |
| 3 | Migration tolerance | Does state persisted by the old code load under the new code? dict["k"] where the rest of the loader uses .get(k, default) is a silent reopen crash across an upgrade. |
| 4 | Reachability to the real surface | Did the change reach the surface that actually runs — the registered wrapper, the shipped path, the docstring the model/user sees — or only an inner copy the diff happened to touch? |
| 5 | Boundary & empty inputs | null / undefined / "" / 0 rows / unset env / missing file. Which one throws, mis-buckets, or silently no-ops? |
| 6 | Looks-done ≠ is-done | Local-green ≠ CI-green ≠ the feature actually working. Did you verify the value path end to end, or just that tests and lint pass? A green build can ship a dead feature. |
| 7 | Flake vs real red | Is a red check the change's fault or a pre-existing flake? Attribute it (touches no relevant code ⇒ likely flake), but never merge on red, and never blame the change for an unrelated failure. |
| 8 | Claim ↔ code drift | Do the PR description, docstrings, comments, and Closes #N match what the code does? A Closes that auto-retires a still-open gap is a defect of record. |
| 9 | DRY / right altitude | Is this the deep fix folded into the existing tolerance, or a band-aid special-case? Does a near-duplicate path want consolidation? |
Honesty discipline
- Mark every finding CONFIRMED (you reproduced it) or PLAUSIBLE (reasoned, not run). Never launder a guess as a fact.
- State plainly what you could not verify or reproduce — that gap is information, not weakness.
- On re-review, do a fresh full pass — do not merely check your prior points were addressed. The worse bug is often the one the fix newly opens. Correct your own earlier findings when reproduction contradicts them.
- Sign off positively on the surface you scrutinized and cleared ("the read-only viewer cannot throw — every dereference is guarded", "the engine is byte-identical, this is a pure reframe"). Naming what is safe is what makes the review trustworthy.
Output format
Severity-ranked, most-actionable first. Each finding:
- Severity — Blocking / Should-fix / Minor / Informational
- Anchor —
path/to/file.py:LINE - Verdict — CONFIRMED | PLAUSIBLE
- Failure scenario — concrete inputs/state → wrong output/crash, and the reachable path to it
- Fix — the deep fix, with a regression test that locks it
Close with: a one-line merge verdict (mergeable / hold on finding #N / not mergeable + why), and a Verified clean list of what you checked and cleared.
Posting the summary (Phase 5)
Default: after emitting the in-session report, post a single distilled comment to the PR. Write the body to a temp file and post with gh pr comment <N> --body-file <file> (a file, not inline --body, so multi-line Markdown survives). One comment per run — never inline per-line comments, never a multi-comment dump. Post even on a clean pass; the record that the change was scrutinized is the point.
Guard: only when gh pr view --json number,url resolves an open PR for the current branch. No PR (local or pre-PR review) → skip posting, keep the report in-session. Honor an explicit "keep it local" from the invoker.
Who authored the PR decides the comment's shape
The comment publishes under the invoking developer's GitHub account, so the account owner is the one speaking. Derive authorship live — no config file, and no assuming from the repo you are standing in:
gh api user --jq .login # the account the comment posts as
gh pr view <N> --json author --jq .author.login # who opened the PR
Reviewing someone else's PR (author ≠ account owner) — the comment is, in order:
- Merge verdict — one line: mergeable / hold on finding #N / not mergeable + why.
- Findings — Blocking and Should-fix only, each as:
path:line— one-line failure scenario — the fix. - Verified clean — the one-line list of what was scrutinized and cleared.
Reviewing the account owner's own PR (author == account owner) — the comment is, in order:
- Findings — same shape, plus what was fixed and the commit that fixed it.
- Refuted — any finding that did not survive reproduction, and why it did not.
- Verified clean — the one-line list of what was scrutinized and cleared.
and no merge verdict line at all. The verdict stays in the in-session report, for the account owner to act on. Publishing "mergeable" on your own PR pre-empts the human reviewer this entire method depends on, and reads to teammates as the author approving their own work before anyone else has looked.
Never publish the review's provenance
Independence is a property of how the review was run (the cardinal rule above), not a claim to make in the comment. On a self-authored PR the claim is also false as teammates read it: a /code-review or review subagent dispatched by the author is still the author's own tooling, in the author's own session.
- Never write "independent", "adversarial pass", "hostile review", "so the verdict would not come from whoever wrote the branch", or any variant of these about a review of the account owner's own branch.
- Never name the machinery:
/code-review,/techne:elenchus, "review agents", "subagent", "effort level". Teammates cannot run it, so it is not evidence to them — it is jargon. - Publish the work instead, which anyone can check: "reproduced on a fresh clone under numpy 2.5.3", "traced every call site of
load_trainacross the tree", "exercised CRLF, BOM, empty-file and arbitrary-cwd inputs".
Every comment, either shape
- Account-owner voice — write as that person speaking: first person ("I reproduced...", "I'd hold on..."), teammates addressed as peers by name. Never refer to the account owner in third person or as "you" ("the author prefers...", "the preference of you and the reviewer" are both wrong), and no assistant framing ("the review found..." is fine; "I ran this for the account owner" is not). Same rule for any follow-up comment in the same thread.
- No signature or AI-attribution footer — post as a plain review comment.
- Keep it about the code. No account or personal state (API credit, billing, subscription tier, what a local machine has installed). If it explains a gap, say the gap: "not exercised end to end" rather than why.
- Drop Minor/Informational findings — they stay in-session — unless nothing else remains.
Clean-pass shape: the Findings line reading none blocking and the Verified-clean list — a few lines, not a placeholder wall. Plus the verdict only when reviewing someone else's PR.
Scaffolding ## elenchus into a repo (optional, tier-1)
Tier-0 needs nothing. To give the skill repo-specific aim, add to .claude/skill-context.md:
## elenchus
```yaml
test_command: <how to run the suite, e.g. make test / uv run pytest -q>
run_command: <how to exercise the running thing, if not already in ## theoros>
destructive_ops:
- <path:line or description of an irreversible op reviewers must trace to its inputs>
load_bearing_surfaces:
- <the registered wrapper / shipped path / docstring the harness actually surfaces>
feature_works_means:
- <what "the feature actually works" looks like end-to-end in this repo, beyond green CI>
```
All fields optional — each one you add sharpens a phase. destructive_ops feeds rubric cell 1, load_bearing_surfaces feeds Phase 2 pass 2 / cell 4, feature_works_means feeds cell 6.
Why "elenchus"
Greek ἔλεγχος: the Socratic method of refutation — cross-examine a claim by seeking the case that breaks it, until what survives is what's true. The review's job is not to agree with the change; it is to try, in good faith and with the system actually running, to refute the claim it rests on. What survives the elenchus is mergeable.