If you want multiple AI coding agents working the same codebase without stepping on each other, the answer is not better prompts or a smarter merge queue. It's this: partition work by architectural component, not by file; schedule tasks in dependency order using a DAG derived from the code itself; give each agent an exclusive lease on its component; and rebuild the graph after every merge. The rest of this post is how to actually do that, and which parts you can run today versus build yourself.
Why file-level parallelism fails
The obvious way to parallelize agents is the way teams parallelize humans: split the ticket list, hand each agent a slice, resolve conflicts at merge time.
This fails faster with agents than with humans, and merge conflicts are the least of it. Conflicts are the visible symptom; the disease is hidden coupling. Two agents can work on "different files" that share an interface, a data shape, or an implicit contract — each produces a locally-correct diff, both merge cleanly, and the combination is broken. A human might have paused at the smell of it. An agent, optimizing exactly the context you gave it, will not.
You can't fix hidden coupling at the file level because files aren't the unit of coupling. Components are.
The graph is already in your repo
The structure you need for scheduling — components, their boundaries, and the dependency edges between them — is derivable from source. archsteer xray does this in one command: it statically parses a repo (Python, TypeScript, JavaScript) and emits model.json, a typed architecture model with every component, its exported APIs, and its resolved internal dependency edges.
That dependency graph is your scheduling DAG. A trimmed example:
{
"components": {
"billing/invoice.py": {
"exported_apis": ["Invoice", "create_invoice"],
"dependencies": [{ "target": "billing/tax.py", "external": false }]
},
"billing/tax.py": {
"exported_apis": ["compute_tax"],
"dependencies": []
}
}
}
billing/tax.py has no internal dependencies — it's a leaf. billing/invoice.py depends on it. If both need changes in your migration, the ordering is not a judgment call anymore.
Topological wave scheduling
Dispatch from the leaves up. A task becomes eligible only when every component it depends on has already been migrated, verified, and merged:
graph = load_model_json().internal_edges()
tasks = plan_delta(current_model, target_model) # what needs to change
while tasks:
wave = [t for t in tasks
if all(dep.status == MERGED for dep in t.component.deps)]
for task in wave:
agent = pool.claim(task) # lock, see below
agent.run(task)
tasks -= {t for t in wave if t.status == MERGED}
graph = rebuild_from_head() # re-index, see below
Waves have a property ticket queues don't: within a wave, tasks are structurally independent by construction. Agents in the same wave cannot invalidate each other's work through the dependency graph, because anything they share is already frozen in a previous wave.
Component leases, not file locks
When an agent claims a task, give it an exclusive lease on the component's node — the component itself plus its incoming and outgoing edges. Any other task touching that node waits.
The difference from file locking matters. A file lock says "don't edit tax.py." A component lease says "don't change the contract of the tax component — its exports, its dependencies, its data access — until the lease releases." That's the thing hidden coupling actually violates, and it's checkable, because the model records exactly what each component's contract is.
Blast-radius context injection
The wave scheduler decides when an agent works. Context injection decides what it knows, and the rule is: an agent gets the interfaces of its neighbors, not the whole repo.
An agent migrating the invoice component needs compute_tax's signature. It does not need tax's implementation — and giving it less is not a cost-saving compromise, it's a correctness feature: what an agent can't see, it can't "helpfully" refactor. Adjacent agents get the inverse injection in their steering files: "the tax component is under an active lease; treat its current API as immutable this session."
This part isn't hypothetical — it's what the shipped tooling does. archsteer steer writes component-scoped guardrails into CLAUDE.md / AGENTS.md / .cursor/rules, and the local MCP server (archsteer mcp, free, stdio, in the CLI) lets the agent query mid-edit: current_architecture for the live model, get_target_pattern for the pattern its file should follow, check_file for violations before it ever opens a PR.
Re-index on every merge
LLMs are non-deterministic. An agent can complete its task correctly and alter a boundary the plan didn't predict — add a dependency, export a new API, touch a table. If your DAG is a planning document, it's now silently wrong.
So the DAG must never be a document. It's a projection of the code, rebuilt on every merge:
PR opened → archsteer check # net-new violations block; the ratchet
PR merged → archsteer xray # model.json rebuilt from HEAD
→ re-plan remaining waves from the new graph
unexpected boundary change → flag a human architect before the next dispatch
archsteer check is the merge gate that makes the loop safe: it compares against a ratcheted baseline, so pre-existing debt doesn't block anyone, but a net-new boundary violation fails the PR — which for a fleet means one agent's drift is caught before it becomes ten agents' context.
What exists today vs. what you build
Honest inventory. Exists now, free and MIT-licensed in the CLI: the model (archsteer xray → model.json with resolved dependency edges), the steering layer (archsteer steer), the local MCP server with current_architecture / get_target_pattern / check_file, and the ratchet gate (archsteer check) for CI. You build: the dispatcher — the wave loop, the lease table, and the task-to-component mapping. That's deliberate; your task source (Jira, Linear, a migration spreadsheet) and your agent runner (Claude Code, Cursor CLI, your own harness) are yours. The dispatcher is roughly a hundred lines of glue over model.json, and every hard part — deriving the graph, scoping the context, gating the merge — is the part that already ships.
A reasonable adoption path: run archsteer xray and look at the graph before believing it (the architecture x-rays gallery shows what the output looks like on codebases you know); wire archsteer check into CI so the ratchet holds with even one agent; add archsteer mcp so your agents query the architecture instead of guessing it; then parallelize with the wave loop once single-agent runs stop drifting.
One agent with the right guardrails beats five agents with a ticket queue. Five agents with a DAG beat both.
pip install archsteer && archsteer xray
Start with the get-started guide, or see the model output on famous codebases in the gallery.