The finding that started this
I was looking at my session logs one afternoon and noticed something that should not have been there. An accessibility agent had run on a diff that only touched a TypeScript model file. No JSX, no rendered output, nothing user-facing: just types and pure functions. The agent produced findings. I read them. None of them applied.
I had paid to run a review that had nothing to review.
That is when I started thinking about what "running a review" actually means. The default assumption is that review agents are cheap enough that running all of them on every diff is fine. That assumption is wrong, and it gets more wrong as the number of agents grows.
What I was actually spending
Before I built the classifier, my review workflow was static. Same agents, every diff. Accessibility, performance, code quality, EVPMR patterns: all of them, all the time.
Some of those runs cost nothing meaningful. Others were pure waste. A performance agent on a diff that only adds display strings. A pattern checker on a file that only contains TypeScript types. An accessibility agent on, as it turns out, a model file.
I did not have good numbers on how often this happened, so I started logging. About 40% of the agent runs in a typical week produced zero findings. Not low findings. Zero. The agent ran, read the diff, and had nothing to say because the diff was not in its domain.
Forty percent of my agent spend was buying silence.
Reading the diff before spawning anything
The fix is to read the actual diff before deciding which agents to run. Not filenames: content. A file named ModelCheckout.ts might contain JSX if someone got creative. The classifier reads what is actually there.
diff shows: agents selected:
──────────────────────────────────────────────────────────
View*.tsx → code-quality + fe-review + fe-a11y
Presenter*.ts → code-quality + fe-review
Model*.ts → code-quality (type/correctness focus)
Entry*.tsx or Resource*.ts → fe-review
auth / payment / credential paths → code-quality (security emphasis)
test files only → Phase 2 SKIPPED ENTIRELY
That last line is the one I come back to most. When a diff touches only test files, Phase 2 does not run at all. Fast gates check the test code (tsc, lint, the test suite itself), and the review is done. A diff that only adds tests has already described itself.
The classifier also has two automatic escalations. A diff over 300 lines gets a warning in the synthesis report (not a block, just a flag that it is big enough to consider splitting). Three or more EVPMR layers changed in one diff adds the adversarial agent, which I will come to at the end.
Three commands, one structure
/parallel-review, /parallel-ship, and /parallel-build all follow the same two-phase shape. Phase 1 runs fast gates in parallel: tsc, lint, test. No LLM cost. If Phase 1 fails, Phase 2 never starts.
I like this gate for a reason that sounds obvious but took me a while to internalize. There is no point asking an agent whether your code follows the right patterns if the code does not compile. The fast gate is not just cost control. It is also priority ordering: fix the broken thing first.
"review this"
↓
PHASE 1 ─── fast gates, no LLM cost ────────────────────────
├── tsc ──┐
├── lint ──┼── all run at once
└── test ──┘
↓ all pass? (any fail → BLOCKED, stops here)
classifier reads actual diff content
↓
PHASE 2 ─── only agents that apply ─────────────────────────
├── code-quality (any non-test code changed)
├── fe-review (View / Presenter / Entry / Resource)
├── fe-a11y? (View only)
└── adversarial? (3+ EVPMR layers changed)
↓ all run in parallel, results merged
merged report ─── sorted by severity
Review runs Phase 2 after clean gates, merges findings from all selected agents into one report sorted by severity.
Ship adds a coverage gate to Phase 1: lines, branches, functions, and statements all need to hit 93%. Phase 2 adds fe-performance when View or Presenter files changed. The final output is binary: READY TO MERGE or BLOCKED — list blockers. I wanted something I could glance at, not a report I had to interpret.
Build is the one that surprised me most when I designed it. It starts sequential on purpose. Generate a context doc, scaffold the five-file EVPMR module, implement. Then parallel fast gates. Only after those pass does the classifier run. Tests are written last, after the implementation has cleared the agents.
The sequence matters in build. Scaffolding before review means Phase 2 is reading finished decisions, not half-formed ones.
"build this feature"
↓
fe-context → generate docs/context.md (sequential)
↓
fe-scaffold → create 5-file EVPMR module (sequential)
↓
implement (sequential)
↓
PHASE 3 ─── fast gates ──────────────────────────────────────
├── tsc ──┐
└── lint ──┘
↓ all pass? (any fail → BLOCKED)
classifier reads what was actually built
↓
PHASE 5 ─── LLM agents ─────────────────────────────────────
├── fe-review
├── fe-patterns
├── fe-a11y?
└── fe-performance?
↓ no [ERROR]? (any ERROR → BLOCKED)
fe-test → write tests, enforce ≥93% coverage (sequential)
↓
DONE
When nothing runs
A diff that touches only test files is worth describing in detail because the behavior is counterintuitive the first time you see it.
Phase 1 runs. tsc and lint check the test code. The test suite runs. Then the classifier reads the diff, sees test files only, and skips Phase 2 entirely. No agents, no synthesis, no spend.
This is the right behavior. A diff that only adds tests has already told you what it does. Sending it to an accessibility agent or a code-quality agent produces work with no return. The review completes in seconds.
regular diff test-only diff
───────────────── ─────────────────
Phase 1: tsc+lint+test Phase 1: tsc+lint+test
↓ ↓
classifier selects agents classifier: test files only
↓ ↓
Phase 2: LLM agents run Phase 2: SKIPPED
↓ ↓
merged report done ($0 LLM cost)
The adversarial agent
When three or more EVPMR layers change in a single diff (Entry, View, Presenter, Model, Resource in any combination), an agent joins Phase 2 that does not belong to any domain. No accessibility checklist, no performance heuristic, no pattern library. Its only job is to argue the strongest case against merging: a specific objection, not a balanced summary.
I added it because I noticed a pattern in my review history. Large multi-layer diffs had the most findings but also the most gaps. Each domain agent found problems in its own area and stopped there. Nobody was looking at what the diff meant as a whole, or whether the combination of changes introduced an assumption that any single layer would consider fine.
The adversarial agent reads everything the other agents produced and asks what they got wrong collectively. It fires rarely. When a diff is large and touches many layers, that is exactly when a second opinion with no domain bias is worth having.
Everything described here is part of CraftKit. No separate setup. The classifier, the phase structure, and the adversarial trigger all run the moment you say "review this."
npm install -g @raditia/craftkit
GitHub: raditia/craftkit