Layer Contracts: Taming a Swarm of AI Agents
Running several coding agents in parallel without collisions. How scoping each agent to one typed layer — with a contract the compiler enforces — kept the build safe.

Running one agent is easy. Running several without them tripping over each other is the actual problem — two agents editing the same seam produce merge conflicts and, worse, quiet contradictions. My answer on this blog was to stop thinking about agents and start thinking about layers: give each agent one layer of the stack and a contract at its boundary that the compiler enforces. Part of the Building with AI series.
A contract is a boundary you can typecheck
The monorepo already had the boundaries; I just made them load-bearing. Three layers, each depending only on the one beneath it: the Sanity schema in apps/cms, the typed query layer in packages/service, and the Next.js app in apps/web. The contract between them isn't a document — it's an exported type.
1// The whole contract apps/web is allowed to know about:
2export async function getPostCards(): Promise<PostCard[]>;
3export async function getPostBySlug(slug: string): Promise<Post | null>;An agent working in apps/web sees only these signatures. It never learns GROQ, never imports the Sanity client, never guesses a field name — the typed service layer is the only surface it touches.
Give each agent one layer
Scoping an agent to a layer is mostly a matter of telling it where the walls are, and the walls are already spelled out in CLAUDE.md: components never import @sanity/client, and data access goes through packages/service. So a subagent assigned the post page gets the service contract and the design tokens, and nothing else it could reach past to be unhelpfully clever.
A contract an agent can typecheck is a boundary it can't cross by accident.
The graph has to stay acyclic
Parallelism only works if the layers form a line, not a loop. If apps/web depended on apps/cms and cms reached back into web, two agents could deadlock on the same edit forever. Turborepo keeps the graph honest: typecheck waits on the upstream build, so a cycle stops compiling before it can ship.
1{
2 "tasks": {
3 "typecheck": { "dependsOn": ["^build"] }
4 }
5}That ^build means service must build before web can typecheck. Wire a cycle and the task runner simply refuses — the same acyclic discipline I argued into the content model shows up again in the build graph.
Why this made a swarm safe
When each agent owns one layer behind a typed contract, a mistake is local and loud. Change the shape of getPostCards and every caller fails to compile at the seam — not silently at runtime three packages away. That's what let me run agents in parallel across the stack without babysitting each one, and it's a big part of why the 24-day retro counts more wins than scars.
Don't manage agents; manage the contracts between their layers. Fence each one into a single typed boundary and the compiler becomes the referee — which is the whole reason the afternoon that started this could scale into a build.
