21STARK
All posts
12 min read views

Three AIs review my PRs. Mostly it's two. Sometimes it's 21.

The honest anatomy of my cross-vendor review fleet: three model vendors under three GitHub Apps, a 21-subagent full matrix, a Gemini that spent three months benched, and a house rule that no finding ever gets lost.

Two. Not three. This post was supposed to be called "Three AIs review every PR I merge." Then I checked my own config. The default PR-review roster in the fleet I built:

"agents": ["claude", "codex"]

global/config.json, the live default PR roster.

Gemini is fully wired: keyed, prompted, holding its own GitHub App. It's also benched from the default PR roster, and has been for most of the repo's life. Meanwhile the full matrix, when I open the throttle, is 3 agents × 7 domains = 21 subagents on a single PR. And the cheap path I run most days is a single agent.

Three, two, twenty-one, one. Every one of those numbers is more interesting than the hook I had planned. So this is the honest version.

The system is stark-skills, my personal review fleet. Born 2026-03-16 as stark-review, renamed five days later, 866 commits in four months. Claude, Codex, and Gemini CLI agents dispatched as headless subagents across review domains. Findings come back as JSON, get deduplicated across agents, and land on the GitHub PR, each model posting under its own bot identity. The receipts: 676 PRs reviewed, 1,455 review rounds on disk, 47,654 lines of tool code. It's the layer my PRs pass through.

One reviewer, one distribution of misses

My personal repos have no second human. There's me, and there's whatever review layer I build. The tempting move is to pick the strongest model and standardize on it. I already wrote a post against that reflex. The operational version:

A single model reviewing code gives you a single distribution of misses. When it approves, you can't distinguish "verified clean" from "didn't look there". Nothing inside one model tells you which one you got. Add a second model from a different vendor and you get two signals at once. Agreement becomes confidence. Two models built by different companies flagging the same line independently is a different class of evidence. Disagreement becomes triage. A finding only one model raised is exactly where your attention should go.

The house rule that makes this bite, from the repo's CLAUDE.md: "Don't drop, downgrade, or summarize findings away", and never merge with open findings unaddressed. "Fix them, or reply on the thread saying why not." Every finding becomes a thread on the PR. Skills block on unresolved threads. Which means the review layer can't be allowed to produce garbage, because I'd drown in garbage threads I'm obligated to answer. The whole design problem is precision at volume: many eyes, deduplicated, adversarially filtered, with receipts.

The mechanism

Domains before agents

The multiplication that matters isn't the model count. It's the domains. A full PR review fans out across 7 of them: architecture, behavior, type-safety, security, test-coverage, spec-conformance, ssot. It was 9. I cut and merged two in PR #434, "for self-use simplicity". More domains meant more volume, not more signal.

Each agent gets its own prompt per domain. Not one prompt with a model parameter. global/prompts/claude/01-architecture.md and global/prompts/codex/01-architecture.md are different files, tuned separately, because the models don't fail the same way and shouldn't be briefed the same way. The full prompt tree is 94 markdown files.

The dispatcher runs the matrix as a bounded worker pool. The literal constant at tools/plan_review_dispatch_lib.ts:47:

const MAX_WORKERS = 21;

Three agents times seven domains. The round loop logs each pass as "X agents × Y domains = Z sub-agents".

Honesty note, again: 21 is the ceiling, not the default. The everyday path, /stark-review, is deliberately single-agent: Claude triages the diff and picks the domains, Codex reviews them. The autonomous build loop (stark-phase-execute, which reviews, classifies findings, fixes, tests, pushes, and re-reviews) dispatches whatever roster is configured, today 2 agents × 7 domains = 14. The full 21 opens on demand, when the diff deserves it.

Disagreement is the signal

Every subagent returns findings as JSON. Then deduplicateFindings in tools/multi_review_lib.ts (2,128 lines) makes three passes:

  1. Exact key: file, plus line bucketed to 5, plus normalized title.
  2. Fuzzy: title-word overlap (Jaccard at 0.5 or above, stop words stripped) within 5 lines.
  3. Cross-agent collapse on exact file and line.

The survivor keeps the receipts:

best.description += ` (also flagged by: ${confirmers.join(", ")})`;
best.cross_validated_by = [...seen];

tools/multi_review_lib.ts, the cross-agent collapse.

That cross_validated_by field is the whole philosophy in one line. A finding two vendors reached independently outranks a finding one vendor reached confidently.

Comic-style drawing of two detectives in blue and orange trench coats, each with a magnifying glass, pointing at the same bug on a printed page.

Two more structures make the disagreement deliberate instead of accidental:

  • Leader and second from different vendors, per domain. Architecture: Claude leads, Codex seconds. Behavior, type-safety, security, test-coverage, ssot: Codex leads, Claude seconds. Security is a consensus domain: agreement required.
  • Cross-vendor refutation. The red-team pipeline uses Claude to refute the Codex committee's findings. A genuine second opinion, not a model grading its own homework. The refuter can only drop or downgrade, never add, and every drop must cite a verbatim span from the artifact. Noise reduction, with the burden of proof on the refuter.

One badge per model

Each model posts to GitHub as itself. Three GitHub Apps:

export const APPS: AppRegistry = {
  "stark-claude": STARK_CLAUDE,               // appId 4094779
  "stark-codex":  { appId: "4094776", ... },
  "stark-gemini": { appId: "4094781", ... },
  default: STARK_CLAUDE,
};

tools/github_app_lib.ts, the APPS registry.

This is the cheapest observability I've ever shipped. Comment authorship is model attribution. Which model finds real bugs, which model nags, which model went quiet for a month: it's all in the PR history, queryable through GitHub itself, no telemetry pipeline needed. Finding threads stay single-author too. The agent that raised a finding is the App that resolves its thread.

Three toy robot minifigures wearing orange, teal, and purple sashes, each pinning a blank note card to its own section of a corkboard.

The library behind the badges is 841 lines with zero npm dependencies: RS256 JWTs via node:crypto, private keys in the macOS Keychain, an on-disk token cache at 0600 permissions, five-minute early expiry. The apps were repointed on 2026-06-19 from employer-account-owned apps to new least-privilege apps under my personal org, with a .bak of the old registry kept for rollback at cutover. Boring identity plumbing, done before it was urgent.

Nothing gets lost

review_doc_findings.ts post opens one resolvable review thread per distinct finding, idempotent via an HTML marker (stark-review-finding:<id>). resolve replies with the fix and closes the thread through GraphQL resolveReviewThread. Merge is blocked while threads are open. The trail survives the merge, which is the point: six months from now the question is never "did anyone catch this", it's a thread with an author, a fix, and a resolution.

The meter runs

Fan-out without a meter isn't a system, it's a subscription. The guardrails live in config: $50 a week budget, $15 a day alert, $100 hard stop. Per-model dollar rates per million tokens sit next to the model pins (currently claude-opus-4-8, gpt-5.6-sol, and gemini-3.1-pro-preview on Vertex), so every dispatch computes real USD in cost_lib.ts. Dollars, not token counts I'd never look at.

The dead ancestor that proved the math

Before the CLI fleet there was stark-agents: six domain agents behind a Python MCP server on Cloud Run, pgvector RAG on Cloud SQL, Firestore persistence, full Terraform. Bootstrapped 2026-03-28. The full implementation landed the next day, written by competing agents scored across 22 autopilot tournaments. Its consensus scoring was the thesis in its purest form:

if num_agreeing >= 3:   confidence = 0.9
elif num_agreeing >= 2: confidence = 0.7
else:                   confidence = 0.4

stark_agents/scoring.py, the agreement ladder.

Severity was the median across agreeing models. One provider down capped confidence at 0.7. Two down meant advisory-only. ADR 0003 (2026-03-28) said it flat: "Each LLM catches different classes of issues", and findings that multiple models agree on carry higher confidence. It priced the tradeoff honestly too: triple the API cost per review, mitigated by file-pattern dispatch.

stark-agents is dormant now, no real development since 2026-05-11. Not because the idea failed. Because it was Python, the wrong side of my house language rule, and because CLI dispatch won: the same three-model consensus with no service to babysit. The agreement ladder lives on in the fleet's DNA.

What broke

The bench

Gemini's timeline in this fleet:

DateEvent
2026-03-16Repo born
2026-03-20Gemini removed from the default roster (e34ffbc), four days in
2026-04-02Disabled across all skills (f985427)
2026-05-14Disabled by default, again (9ab8805)
2026-06-23Re-enabled via Vertex AI with runtime project resolution (b46b7f6, #609)

Three months benched. It came back with a rule attached ("never hardcode a GCP project id in source") and a specific seat: the IaC-review roster (["codex", "gemini"]) plus fallback duty. Still not in the default PR roster. That's not the multi-vendor idea failing. That's the multi-vendor idea working: a roster spot is a decision you keep re-earning, not a logo wall.

The gate that blocked itself

The red-team pipeline has a pre-dispatch injection scanner. It scanned the fully assembled prompt. The prompt's own preamble legitimately quotes "ignore previous instructions" as an example of what to look for. So the scanner tripped on itself. Every single run (#627).

The deeper version of the same bug: 68 out of 68 "prompt injection" findings across 201 red-team runs were critical false positives, every one flagging the artifact's own authored text. One hundred percent noise, on the scariest-sounding category. Two fixes killed it: scan only untrusted input, and require every injection finding to cite a quoted verbatim span, then let the cross-vendor refutation pass grind down what remains.

The review that ate its own doc

Autonomous doc-review fix loops had a failure mode I didn't predict: growth. A 200-line doc ballooned toward 80,000 lines over 10 rounds. Round N invents scope, round N+1 condemns the invented scope as over-engineering, the loop compounds. I call the pattern invent-then-condemn, and there's now a literal detector for it in stark_review_doc.ts, next to a hard 3x growth cap with rollback and a round-spike tripwire (over 1.5x growth in one round while findings aren't declining). There was a real casualty first: the kotodama spec bloated 2.34x before the fix landed. The fix that mattered most was making a declared V1 boundary binding on reviewers.

Ukiyo-e style woodblock print of a paper scroll coiled into a serpent biting its own tail, with waves and loose pages spiraling around the coil.

Convergence needed the same discipline. One real run's finding counts went 7, 7, 6, 8, 3 across rounds, because the wing reviewer kept re-litigating settled findings from fresh angles. Fixed 2026-07-17 with settled dispositions and one blunt invariant: the blocking set must shrink toward zero, and zero findings is the expected terminal approve, not an alarm.

The prompts have a changelog

The prompts driving all of this are production code, so they get a paper trail: 18 dated entries, 391 lines, in docs/prompt-changelog.md, each with the source PR, the diagnosis, and the per-file changes. /stark-review-improvement reads the review history and edits the prompts itself. The changelog is its audit trail.

My favorite entry, 2026-04-09: "All 18 sub-agents (2 agents × 9 domains) timed out on a 68-file, 6592-insertion PR." Every finding that round came from the orchestrator's runtime verification. The prompt-driven agents contributed zero. The fix: adaptive timeouts (900s stretching to 1800s past 40 files or 3,000 changed lines) plus triage guidance for large diffs. A review system that can't survive its own worst day is a demo.

Bench discipline

Bench discipline: a model earns its roster spot per domain, with evidence, and loses it the same way. Wired-in doesn't mean trusted, and enabled doesn't mean default. The vendor lineup is marketing; the roster is engineering. Gemini isn't out of the fleet. It sits where it earned a seat (IaC review, fallback paths) and nowhere it didn't. Codex leads five of the seven PR domains not because I like the vendor, but because the leader/second table is a record of results, not a diplomatic seating chart.

Bench discipline runs in the other direction too. The everyday review is one agent, because most diffs don't deserve 21 subagents, and a $50-a-week budget is a design constraint, not an accounting footnote. The diff earns the escalation, the model earns the seat.

Steal this

The transferable moves, in the order I'd build them:

  1. Two vendors before three. The second vendor buys you the agreement/disagreement signal. The third has to earn its seat. Mine took three months and still only covers IaC.
  2. One badge per model. Separate bot identity per reviewer from day one. Attribution, per-model analytics, and thread ownership fall out of the platform for free.
  3. Dedup across agents at file and line, and mark the survivors. A cross_validated_by field turns agreement into a ranking signal instead of duplicate noise.
  4. Make one vendor refute another. Refuters can only remove or downgrade, never add, and every removal cites a verbatim span. Noise drops without losing real findings.
  5. Findings are resolvable PR threads or they didn't happen. Block merge on open threads. The trail must survive the merge.
  6. Give your prompts a changelog. If review is load-bearing, a prompt edit is a production change, with a diagnosis and a source incident.
  7. Cap the spend in config. Real dollars per dispatch, a weekly budget, a hard stop.
  8. Circuit-break any loop that edits what it also judges. Growth caps, rollback, an invent-then-condemn detector. Ask my 80,000-line doc.

When the third seat opens

Overkill for personal repos? Backwards question. The personal fleet is exactly where I can afford to learn what breaks: the scanner that tripped on itself, the doc that ate itself, the three-month bench. 866 commits in four months, and the machinery reviewed its own changes the whole way. At some point review stopped being a favor I do the code. Now it's just the layer every change passes through.

And the day Gemini earns its PR-roster seat back, the promotion will be a one-line config diff. It'll go up as a PR, like everything else here. The other two will review it.

Get in touch

I write about AI-first engineering on LinkedIn. Specs in, production out, nobody types code. Follow along there, or send a note.

hi@21stark.com · LinkedIn opens my profile, message me from there


Or send it from here