Writing
Reserving a file before you write it
Give three agents the same repository and they overwrite each other inside a minute. Last write wins, silently, and nobody finds out until the tests do. The obvious fix is a checkout each, and for the case we care about it is the wrong one.
What a checkout each actually costs
A worktree per agent is the right model for three unrelated bugs in a mature codebase. It is not the model for the thing people actually ask us for, which is several agents building one application, split up to go faster. On that case it breaks in four places.
The second agent cannot see the first agent’s code. The one writing the API needs a type the one writing the schema finished a minute ago, in another worktree on another branch. It does not see the type, so it declares the type again. You end with three divergent definitions of one thing, and the merge does not reconcile them: it produces a conflict on a file nobody owns. Composing in the open is the whole benefit of working in parallel, and separate checkouts forbid it by construction.
On a new project the conflict is maximal rather than minimal. Three agents starting from an empty repository each create a package manifest, a TypeScript config, a bundler config, an entry point and a lockfile. Those are add and add conflicts on generated files, which is the worst case a merge has, and a lockfile does not merge at all. It regenerates.
The preview lies. A preview has to serve one tree. Either it serves the integration branch, which is minutes behind the work the person is watching happen, or it serves one worktree, which is a third of the application.
And somebody pays for the merge. Either a human in a conflict resolution interface, or a fourth agent burning tokens to repair a fragmentation we created ourselves.
A lease, not a mutex
So the agents share one directory, and a reservation is what makes that survivable. A claim is a set of paths, an agent, a mode and an expiry. It prevents nothing physically. It declares an intention, and it gives the system the right to judge a write.
There are two modes. An exclusive claim has one holder. A shared claim announces a read, which is how an agent says it depends on a file and gets told when the owner of that file changes it.
A claim never blocks. A refusal comes back immediately, and it carries everything the refused agent needs to do something else instead.
export interface ClaimDenial {
path: string;
holder: string;
holderTask: string;
expiresInS: number;
hint: string;
}The holder, what the holder is working on, how many seconds are left, and what to do now. An agent that queued on a lock would burn tokens sitting still, so the refusal tells it to take other work in its own scope, or to message the holder, or to post itself as blocked so the orchestrator can re-plan.
Expiry is a lease and not a deadline. Any board call by an agent, and any write the watcher observes on a path it holds, pushes its expiry out by a full term. An agent that is working never silently loses its lease. The default term is fifteen minutes, the ceiling is an hour, and one claim covers at most fifty paths.
Death needs no heartbeat, because the server owns the pty. When the process exits, every lease of that session is released at once, the roster marks it as gone, and the orchestrator is told which paths it was holding and which task is unfinished. The expiry only ever covers the agent that is alive but stuck.
A person can break a lease from the canvas. The orchestrator deliberately cannot. Breaking the lease of a live agent that is genuinely writing is the most reliable way to manufacture the collision the whole mechanism exists to avoid.
There is also a ceiling on how often an agent may ask. Past thirty board calls in a rolling minute the answer is an explicit refusal carrying a retry time, returned as a value the agent can read rather than thrown as an error, because an agent cannot act on an exception.
Compliance is an optimisation, never a condition of correctness.
The rule is judged, not trusted
The server owns the filesystem, so it does not have to believe an agent that says it claimed something. A watcher sees every write and asks who it belongs to.
classifyWrite(rawPath: string, hintedAgentId?: string): WriteClassificationThe order is fixed. The agent’s own hook report first, which is ground truth wherever the CLI supports hooks. Then the live claim on that path. Then the single agent whose declared scope matches it. Then nobody, honestly recorded as nobody. A scope match is used only when exactly one agent claims that territory, because two overlapping scopes is an invalid plan and guessing between them would put a lie on the canvas.
That is the whole reason this works with models that do not cooperate perfectly. An agent that forgets to claim still gets its writes attributed and still gets caught writing into somebody else’s path.
When a write lands on somebody else’s path
The snapshot is committed before the collision reaches the screen, so the revert the canvas offers is always real. Ordering it the other way round would give people a button that sometimes has nothing behind it.
Then both sides are told. The offender is told to stop and to revert if it can, so it does not keep going. The holder is told that its file moved underneath it and that it should re-read the file before trusting it, which is the message that stops a good agent building on a corrupted assumption.
The third violation by the same agent stops being a message and becomes a question to the human: pause it, re-scope it, or let it continue. An agent that has broken the same rule three times is not going to be fixed by a fourth notification, and the loop diverges if nobody interrupts it.
Attribution comes free
Because the server knows who held which path at the moment it changed, every write is attributed. The commit message names the agent alongside the file. A human reviews one diff that can be coloured by author, and gets blame per agent with no branch per agent, which was the main thing a worktree each would have bought.
What the agents did with it
Measured on our own two-agent runs: 1.8 board calls per turn, two writes out of two covered by a claim, and two refusals to overwrite a peer. It is a small sample and our own observation rather than a benchmark, and it is stated as such.
The interesting figure is the first one. Nobody polled. Agents used the board roughly twice a turn, which is what a mechanism looks like when it is cheaper to use than to work around. The two refusals took insisting: we had to push before an agent would overwrite a peer at all.
If you are building the same thing
Put the enforcement where the filesystem is. A protocol the model is asked to follow is a suggestion. A watcher that attributes writes is a fact, and the difference shows up the first time a model is having a bad afternoon.
Make refusal cheaper than waiting. A refusal that arrives instantly with the holder, the remaining time and an alternative is something an agent can act on. A blocking lock is an invoice.
Snapshot before you notify, not after. The order is the difference between a revert button and a lie.