Two agents, two repositories, one refused collision
Two coding agents ran in parallel under declared write scope. One collision was refused before spawn, one scope escape was caught at exit.
I ran two coding agents in parallel across two repositories under declared write scope. A third task tried to write into ground one agent already claimed, and the coordinator refused it before that process ever spawned. A second run caught a real scope escape, an agent that wrote outside its declared paths, at exit.
A third task was submitted while two coding agents were already running. It asked to write in paths one of the live agents had declared before it spawned. It was refused at claim time, and the process behind it never started.
That is the sentence I wanted to be able to write when I began this. The argument for declaring write scope before a process spawns is easy to make on paper: state the paths, check them against every live claim, refuse the overlap before anyone spends a token. The argument only becomes interesting when a real collision arrives while real work is in flight, and the refusal costs nothing because there is nothing yet to unwind.
So here is what actually ran, exactly as it was recorded, including the run where the mechanism caught me rather than someone else.
What actually ran
Two agents, two repositories, tasks with no overlap between them. One worked in a knowledge base of markdown and scripts. The other worked in an infrastructure repository of compose files and runbooks. Each ran in its own clone, on its own branch, under its own contract, with a human watching a live event stream.
| Agent A | Agent B | |
|---|---|---|
| Repository | Knowledge base: markdown, scripts | Infrastructure: compose files, runbooks |
| Declared write paths | 7 | 6 |
| Wall time | 12 minutes | 39 minutes |
| Output | 1 pull request, 21 files, +288 / -1302 lines | 1 pull request, 4 files, +733 / -27 lines |
Both produced mergeable pull requests with verified outcomes. Neither agent knew the other existed, and that is deliberate: two agents holding claims that do not touch have nothing to say to each other. Awareness is the coordinator’s job, done once, rather than a burden each agent carries in its context for the whole run.
The refusal, and what it cost
Partway through, a third contract was submitted. Its declared paths overlapped one of Agent A’s seven.
The coordinator refused the claim. Refused here means not granted, which is not the same as rejected: the request came back naming Agent A as the holder and quoting Agent A’s stated task in plain words, not as an error code. The collision was written to the journal as an event, alongside every other claim and grant in the run. The third process never spawned.
That last clause is the economics of the whole idea. Nothing had been read, nothing generated, no branch created, no diff produced. The only spend was the check itself, plus whatever planning had already gone into drafting the contract. Compare that with the usual shape, where two agents both run to completion and the overlap is discovered by a human at merge time, after both have spent their tokens and their wall time, and the human now has to reconstruct two intents from two diffs.
The plain-words part matters more than it looks. An agent refused with no reason retries, rephrases, retries again, and burns a loop’s worth of tokens learning nothing. An agent told which session holds the ground, what that session is doing, and how far along it is can narrow its own claim, wait, or hand its intent over. The reason is what stops the escalation.
The third process never spawned. The only spend was the check itself.
Run two, and the escape it found
The first run exposed a bug in my own exit check: it computed what an agent had produced from the uncommitted working tree, so an agent that committed its work exactly as briefed looked like it had produced nothing. The fix was to diff from the snapshot commit the claim already records.
With that repaired I ran it again. Two more agents, one per repository, both finished inside eleven minutes, two more pull requests.
This run recorded a genuine scope escape. One agent regenerated a derived index file that sat outside its seven declared paths. The write was committed. The fulfilment check, now diffing from the claim’s snapshot commit, caught it and recorded a breach against the contract.
Why a breach was the right call on a change that belonged
Here is the uncomfortable part, and the part I would not want smoothed over.
The change was correct. The derived index really did need regenerating, and the file genuinely belonged in that pull request. If you looked only at the diff you would approve it without a second thought. The agent did something sensible and got marked as having broken its contract for doing it.
That is still the right outcome, and not because the change was wrong. The value is not that undeclared writes are always bad. The value is that an undeclared write surfaces at all, rather than passing silently into a branch nobody was watching for it. Once it surfaces, a human gets to make the actual decision, which is a different question entirely: was the declaration too narrow, or did the agent overreach?
Without the check, that question is never asked, because nobody knows it needed asking. The change lands, it looks plausible, and it is reviewed by people who had no idea it was outside the boundary. A scope escape is not a merge conflict. Nothing collides, so no merge tool will ever mention it to you.
A breach is not a verdict on the change. It is a prompt for a decision a human would otherwise never be offered.
What two runs do not prove
I would rather state the limits than have someone else find them.
Scope claims cannot catch divergent architecture. Two agents can stay strictly inside their own declared files and still build parts that do not fit each other: one returns a list, the other expects an object. Git merges clean and the software is wrong. Nobody crossed a boundary, so the mechanism has nothing to detect. The model’s answer to that failure class is a shared interface sheet, agreed before code and checked at exit, and it has not been trialled.
Two runs, one estate, one operator. This is a report of a working mechanism and what it caught. It is not a controlled study, and it should be read and cited as one report, not as evidence about anyone else’s setup.
Claims were exercised at path level only. Symbol-level claims, narrower than a file, are specified and not implemented, so everything above holds at the granularity of paths and nothing finer.
The coordinator is a single process on one machine. Nothing here addresses distributed coordinators, or coordinators that outlive the machine they run on.
Takeaway
Three things came out of two runs, stated at the confidence two runs actually support.
- A collision refused before spawn costs the price of a check. That number is not close to the price of two finished agents and a human reconciling their intents.
- A refusal that carries the other side’s identity and stated task can be planned around. A bare refusal produces a retry loop.
- An undeclared write that surfaces as a breach is worth having even when the write was correct, because the alternative is a decision nobody knew to make.
The next question is whether the guard can move inside the process itself. A coordinator that knows one repository will always be blind to the others, and a post-hoc diff is not a write guard. A hook on every write, checked against the live claim before the write lands, would be. That is the experiment this points at, and it has not been run.
The full paper behind this post, with the mechanism, the contract model and the failures in more detail, is published at 10.5281/zenodo.22670722 under CC BY 4.0.
Written by Sagar Thakkar, AI systems architect specialising in large-scale data processing, cost-optimised cloud-native systems, and reliable production infrastructure. More at sagarthakkar.com.