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's 559,435 lines of JavaScript across src/ and scripts/, 350 page routes, 615 API route handlers, 71 GitHub Actions workflows. Who maintains it? Me, plus a handful of Claude Code sessions running at the same time, most days. The July repo audit put lifetime merged pull requests at 1,800, and 949 commits in the sixteen days before it ran. That's about 65 pull requests a week.
The three predicted deaths
The predictions are always the same, and they're not wrong. Slop code, because models spit out faster than review can catch. Rotting architecture, because who holds half a million lines in their head? Merge hell, because parallel sessions editing one repo is a machine built to generate conflicts. That's what happens by default. Every time. So why is the repo still alive? Not because the models improved. Because I stopped seeing those three things as risks I had to be careful about, and started seeing them as engineering problems that have mechanical answers.
The bottleneck is not code generation
Here's the thing nobody expects until they've run this setup for a month: generating correct code is the easy part. Point a frontier model at a well-factored codebase and it writes competent diffs all day. The real bottlenecks are boring. First, context: what does this session actually know about the repo, and is any of it true? Second, conflict: what happens when three sessions land on main in the same hour? Both problems get worse with scale in a way code quality never does. A mediocre function is a local problem. But a session working from a stale mental model of the auth layer ships a pull request that's wrong everywhere it touches. Rot isn't bad code piling up. Rot is context divergence piling up. So the operating system I built manages context and conflict. It mostly leaves code generation alone.
CLAUDE.md is a router, not an encyclopedia
Every session loads CLAUDE.md at start. The obvious failure: a session learns something, appends it there, the file balloons, the signal drowns, and now every pull request touches the same file so every merge conflicts. Mine is a router instead. The core is a domain-map table - one row per surface, a one-line description, 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 sits in the file itself, and it's blunt: a new surface documents itself in a new doc plus one table row, env vars go in one env doc, never append large sections here. The file even explains why it works this way. That's what keeps it out of every pull request's conflict set.
Merge hell, demoted to a checklist
All the concurrent-session machinery lives in one small script. scripts/claude-session-sync.sh runs as a SessionStart hook, so it fires on startup, on resume, and after context compaction. Behind origin/main with a clean tree? It merges main in. Hit a conflict and it aborts, lists the offending files, and reminds the session about the keep-both convention. Dirty tree, it just reports, and it tells you if any known hotspot file changed upstream. Then the written doctrine finishes the job: being behind main is normal, ignore it mid-task, merge once right before you open the PR, and never loop rebases. We didn't solve merge hell. We demoted it to a checklist.
Drift gates instead of discipline
Documentation lies. That's the second-order problem nobody warns you about. Any doc that leans on a human, or an agent, remembering to update it will drift, and at 65 pull requests a week it drifts in days. So I don't write docs I care about. I turn them into failing tests. I call these drift gates. They're the closest thing this repo has to a staff engineer standing in the doorway saying no.
Here's how it works. A secrets-inventory audit scans the code and workflows for every token source and fails if any of them isn't classified in a ledger. It runs inside the normal test suite, so an agent can't sneak in an untracked environment read and still go green. It already caught a real hardcoded-password bug. A SQL-inventory audit does the same thing for every migration file. Newest one shipped this week: the Burn dashboard lists every scheduled job, and a test keeps its catalog in exact sync with the workflow files. A cron can't exist without the dashboard knowing it should have run.
// 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 takes the drift-gate idea as far as it goes. Every computed figure on that page gets recomputed in continuous integration, then re-verified live in your browser, and any sourced figure gets cross-checked against the essays that cite it. Edit an essay so a number changes? The build fails before the page can lie to anyone. Same principle everywhere. Don't ask an agent to remember an invariant. Encode the invariant as a check, drop that check somewhere the agent can't route around it, and let the prose be a courtesy explanation of why the check is there.
The 57-second suite is load-bearing
None of the above works without one fact underneath it: the whole test suite is fast enough that every session runs all of it, every time, without me thinking about it. Last full run I recorded was 706 test files and 6,571 tests. Green in 57 seconds. That speed didn't happen by luck. I engineered it, and the mechanism has a name worth saying out loud: pure-module extraction. Business logic lives in pure modules, kept away from React and away from input/output, so almost the whole suite is functions in, assertions out, running in a plain Node environment. Booting a browser-like environment for every test file used to eat the runtime alive. So the default environment is Node now, and the handful of DOM-rendering files opt in with a one-line pragma.
The gates a unit test cannot express
There are gates above the unit suite that the test runner can't touch. Take the ops harness. It's there because of a real outage: a Sentry middleware wrapper that only ran in the deploy build path took the site down, and when I dug through the postmortem I came out with tools rather than answers. The fix was a prod-parity build that now runs that deploy-only path on every single pull request, which makes that whole outage class impossible to merge. The games library works differently, in two rungs. A nightly language-model smoke test screenshots every game on a cold mobile profile and files issues. Underneath it, a deterministic Playwright gate blocks the merge. I wrote that one up separately, in a two-rung gate for AI-tested games. Judgment lives upstairs; the gate stays deterministic.
What still requires me
Three things, and I hold onto them on purpose. First, gates on everything that goes out. Every rail that leaves the site waits at a human step - the newsletter, the social studio's networks, LinkedIn, all of it - and behind that step is a red-lines module every publish path has to call before it can send. Agents draft. I sign. Second, taste. Left alone, agents will build immaculate scaffolding around near-zero content forever. Deciding what deserves to exist, what to kill, what reads as slop? Not delegable. Pretending it is, is exactly how a repo like this rots. Third, distribution. The systems can make the work good and keep it true, sure. But 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, and it never stops. Every surface owes a doc, an env-table row, gate wiring. Every pull request. Forever. Any number I write into prose goes stale in days, so the gates check invariants, not counts. Then there's the domain map: still one shared file. Two sessions adding surfaces both touch it, and keep-both is just a convention I follow, not something the machine enforces, so a careless merge can quietly drop a row. What about the fast suite? It buys unit-level confidence, period. The outage that made me build the whole harness sailed through a green suite. Every gate here got built because its absence burned me first, and wherever a gate is blind, the blind spot ships anyway. I know all this. I take the trade anyway, and I put the attention it frees up back into those three things above.
Right about the default, wrong about the ceiling
They called the default right. They missed the ceiling. Turn a pack of parallel agents loose on a repo with no operating system and slop shows up, then rot, then merge hell. So you build the alternative: context routing. Mechanical governance. A test suite quick enough that skipping it never crosses your mind. Want the honest trade? Models aren't stepping in for engineering judgment here. You're just moving the judgment. Off the diff, where it never scaled anyway, and onto the gates, where it fires on every change, forever.
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 →