How I run parallel AI agents on a 559k-line repo without it rotting
engineeringJuly 7, 20268 min read
By

How I run parallel AI agents on a 559k-line repo without it rotting

One Next.js repo, half a million lines, several Claude Code sessions a day, ~65 PRs a week. Everyone predicts the same three deaths: slop, rot, and merge hell. The repo is alive anyway, because I stopped treating those as risks to be careful about and started treating them as engineering problems with mechanical answers.

My site is one Next.js repo. As I write this it holds 559,435 lines of JavaScript across src/ and scripts/, 350 page routes, 615 API route handlers, and 71 GitHub Actions workflows. It is maintained by me plus several Claude Code sessions running concurrently, most days. The July repo audit counted 1,800 merged pull requests lifetime and 949 commits in the sixteen days before it was written, which works out to roughly 65 pull requests a week.

The three predicted deaths

Everyone who hears those numbers predicts the same three deaths. The code will be slop, because models generate fast and review cannot keep up. The architecture will rot, because nobody holds half a million lines in their head. And the merges will be hell, because parallel sessions editing one repo is a conflict machine. These are good predictions. They describe exactly what happens by default. The repo is alive anyway, and the reason is not that the models got better. It is that I stopped treating the predictions as risks to be careful about and started treating them as engineering problems with mechanical answers.

The bottleneck is not code generation

Here is the thing nobody expects until they run this setup for a month: generating correct code is the easy part. A frontier model pointed at a well-factored codebase writes competent diffs all day. The actual bottlenecks are two boring things. First, context: what does this session know about the repo, and is it true? Second, conflict: what happens when three sessions land on main in the same hour? Both problems compound with scale in a way code quality does not. A mediocre function is a local problem. A session working from a stale mental model of the auth layer produces a pull request that is wrong everywhere it touches. Rot is not bad code accumulating. Rot is context divergence accumulating. So the operating system I built manages context and conflict, and mostly leaves code generation alone.

CLAUDE.md is a router, not an encyclopedia

Every session loads CLAUDE.md into context at start. The default failure is obvious: every session that learns something appends it there, the file balloons, the useful signal drowns, and every pull request now touches the same file, so every merge conflicts. Mine is written as a router. The core is a domain-map table: one row per surface, a one-line description, and a pointer to a deep-dive doc in docs/surfaces/ (70 files today: posture, schemas, env vars, trust invariants). A session touching the calendar reads the calendar doc; a session touching billing reads the billing doc; neither pays context for the other. The rule is stated in the file itself and it is blunt: a new surface documents itself in a new doc plus one table row, env vars go in one env doc, and never append large sections to this file. The file even explains its own reason. That is what keeps it out of every pull request's conflict set.

Merge hell, demoted to a checklist

The concurrent-session machinery is one small script. scripts/claude-session-sync.sh is wired as a SessionStart hook, firing on startup, resume, and context compaction. If the branch is behind origin/main and the tree is clean, it merges main in; on conflict it aborts, lists the conflicting files, and reminds the session of the keep-both convention; on a dirty tree it only reports, including whether any known hotspot file changed upstream. The written doctrine completes it: being behind main is normal, not an emergency; ignore it mid-task; merge once, right before opening the pull request; never loop rebases. Merge hell did not get solved. It got demoted to a checklist.

Drift gates instead of discipline

The second-order problem with documentation is that it lies. Any doc that depends on a human, or an agent, remembering to update it will drift, and at 65 pull requests a week it drifts in days. My answer is to turn every piece of governance I actually care about into a failing test. I call these drift gates, and they are the closest thing this repo has to a substitute for a staff engineer saying no. A secrets-inventory audit scans the code and workflows for every token source and fails if any is unclassified in a ledger; it runs inside the normal test suite, so an agent cannot introduce an untracked environment read and still go green (it has already caught a real hardcoded-password bug). A SQL-inventory audit does the same for every migration file. The newest example shipped this week: the Burn dashboard that lists every scheduled job has a catalog which a test keeps in exact sync with the workflow files, so a cron cannot exist without the dashboard knowing it should have run.

js
// The pattern, everywhere: the invariant IS a test, put where an agent
// cannot route around it. (burn catalog-drift, paraphrased)
const actual = scheduledWorkflowFiles();      // .github/workflows/*.yml
const claimed = catalogWorkflowFiles();        // src/lib/burn/catalog.mjs
expect({
  missing: actual.filter((f) => !claimed.includes(f)),
  phantom: claimed.filter((f) => !actual.includes(f)),
}).toEqual({ missing: [], phantom: [] });
// Add a cron -> add a catalog row, or the suite is red. No memory required.

The purest example is public

The research Claims Ledger is the drift-gate idea taken all the way. Every computed figure on that page is recomputed in continuous integration and re-verified live in the visitor's browser, and sourced figures are cross-checked against the essays that cite them. If an essay edit changes a number, the build fails before the page can lie. The design principle is the same everywhere: never ask an agent to remember an invariant. Encode the invariant as a check, put the check where the agent cannot route around it, and let the prose documentation be a courtesy explanation of why the check exists.

The 57-second suite is load-bearing

None of the above works without one enabling fact: the full test suite is fast enough that every session runs all of it, every time, without thinking. At the last recorded full run that was 706 test files and 6,571 tests, green in 57 seconds. That speed is engineered, not lucky, and the mechanism is worth naming: pure-module extraction. Business logic lives in pure modules separated from React and from input/output, so nearly the whole suite is functions in, assertions out, running in a plain Node environment. Booting a browser-like environment per test file used to dominate the runtime, so the default environment is Node and the handful of DOM-rendering files opt in with a one-line pragma.

The gates a unit test cannot express

Above the unit suite sit the gates the test runner cannot express. An ops harness exists because of a real outage: a Sentry middleware wrapper that only ran in the deploy build path took the site down, and the postmortem produced tools instead of resolutions. A prod-parity build now runs that deploy-only path on every pull request, which makes that outage class un-mergeable. For the games library, the pattern is two rungs: a nightly language-model smoke test screenshots every game on a cold mobile profile and files issues, while a deterministic Playwright gate blocks merges. I wrote that one up separately in a two-rung gate for AI-tested games. Judgment upstairs, determinism at the gate.

What still requires me

Three things, and I hold them deliberately. First, gates on everything outbound. Every rail that leaves the site (the newsletter, the social studio's networks, LinkedIn) waits at a human step, backed by a red-lines module every publish path must call before it can send. Agents draft; I sign. Second, taste. Agents will build immaculate scaffolding around near-zero content forever if you let them; deciding what deserves to exist, what to kill, and what reads as slop is not delegable, and pretending otherwise is how a repo like this actually rots. Third, distribution. The systems can make the work good and keep it true. Getting it in front of the right people is relationships and judgment, and no hook fires for that.

What it costs, and where it creaks

The tax is real. Every surface owes a doc, an env-table row, and gate wiring, on every pull request, forever. Any number written in prose is stale within days, which is precisely why the gates check invariants rather than counts. The domain map is still one shared file, so two sessions adding surfaces still both touch it; keep-both is a convention, not a mechanical guarantee, and a careless resolution can drop a row. The fast suite buys unit-level confidence only; the outage that motivated the harness shipped through a green suite. Each of those gates exists because its absence burned me first, and where a gate has a blind spot, the blind spot ships. I accept that trade with open eyes, and I spend the freed attention on the three things above.

Right about the default, wrong about the ceiling

The predictions were right about the default and wrong about the ceiling. Slop, rot, and merge hell are what you get when parallel agents meet a repo with no operating system. Context routing, mechanical governance, and a suite fast enough to be un-skippable are what you get to have instead. If you want the honest version of the trade: this is not a story about models replacing engineering judgment. It is a story about moving the judgment from the diff, where it does not scale, into the gates, where it runs on every change forever.

Experience it yourselfI install systems like this for teams
ShareXLinkedInHacker NewsEmail

Get the next one

An occasional note when something genuinely new ships here — essays, free tools, projects. No schedule, no filler, easy out.

Need something like this built?

I design and ship AI tools, full-stack apps, and data pipelines — end to end, to production. Tell me the problem in a sentence; I'll give you an honest read on fit within a day.

Work with me →