Synthesis Engineering·Synthesis Coding·Synthesis Writing·Synthesis Project Management

I told a sub-agent to check before committing. Minutes later, I didn't.

One night recently I dispatched a sub-agent, a separate agent instance handling one piece of a larger job, to work inside one of my repositories. Part of its brief, the instructions I write before sending an agent off to do something unsupervised, was a specific warning: check git status and git diff --cached before committing anything, because other agents might be touching this same repository at the same time. I’d seen the failure that warning exists to prevent, so I wrote it carefully.

Minutes after sending that brief, in that same repository, with two other agents actively mid-flight running deletions as part of work I’d dispatched earlier the same night, I added two files of my own and committed. I did not run the check I had just told someone else to run.

The commit swept up thirty-two files that weren’t mine: deletions the other two agents had already staged but hadn’t finished the work behind. git add doesn’t replace whatever’s sitting in the index. It adds to it. A plain git commit afterward commits everything currently staged, not just whatever you personally just typed git add on. I knew that. I’d written it into someone else’s instructions a few minutes earlier. Knowing a rule and having it fire at the moment you act turned out to be two different things.

It was recoverable. The content behind those deletions had already been checked as safely folded into its replacement, and the edits the other two agents were making were still sitting unstaged, waiting for them to reach their own commit step. Nothing was lost, just a commit boundary that mixed unrelated work under one message. It would have gone badly if either of those in-flight deletions had depended on a replacement that wasn’t finished yet.

Shared state nobody agreed to share

Here’s what’s new about this particular failure. Working alone, sequentially, there is nothing to check. If I’m the only process touching a repository, whatever’s staged is mine, by definition, because there’s no one else it could belong to. “Check what’s already staged before you commit” isn’t a rule worth having until more than one process can touch the same resource without telling you first.

That’s the shape of the whole category. A git index is one shared, mutable thing that several agents can touch without any of them announcing it. One agent working alone never produces this failure, because there’s no second party for its assumptions to collide with. Concurrency is what turns an assumption, whatever’s here is mine, into something that has to be verified instead of taken for granted. And the check has to be procedural, run first, every time, rather than a judgment call for whenever something feels risky enough to warrant it. The moment this one felt low-risk, two files, a documentation change, is exactly the moment I skipped it.

A brief doesn’t get a follow-up question

The second thing that changes with concurrency is the shape of the instruction itself.

Talking to one agent live, ambiguity is cheap. I say something under-specified, the agent guesses, the guess is visibly wrong, I correct it in the next message. That loop runs in seconds and I mostly don’t notice it happening. A dispatched brief doesn’t get that loop. Once it’s sent, especially as one of several running unattended and in parallel, whatever’s ambiguous in it resolves however the agent resolves it, and I find out only after the fact.

I ran into this the same night. One brief needed to tell an agent to leave a single, already-curated file completely alone while it processed several hundred others, a boundary I’d already settled, not something to reconsider. I wrote it the way I’d say it out loud: this file is out of scope. A guard built specifically to catch agents leaning on vague language to excuse skipped work flagged the phrase and stopped the dispatch. It had a point, even though it was wrong here: “out of scope” is the exact three words an agent, or a person, reaches for when the honest answer is “I didn’t want to do the harder part.” Read from the label alone, there’s no way to tell a genuine, already-decided boundary from an excuse. They’re the same words carrying opposite intentions.

The fix wasn’t arguing with the guard. It was writing what I meant instead of the shorthand for it: don’t touch this file, don’t merge it into anything, don’t delete it, it’s a separate, already-curated artifact. That’s not a workaround for an overzealous filter. It’s what a brief needs regardless of whether a guard exists, because a brief is a one-shot instruction with no next turn behind it. Say something ambiguous in a live conversation and there’s a next message to catch it in. Write it into a brief running unattended, maybe alongside other briefs running at the same time, and the label has to already be the concrete instruction, because there’s no second chance to notice it was vague.

The chain is only as honest as its weakest report

The third thing that’s different: the person deciding whether to trust a multi-agent result usually never touched the underlying work directly.

I’d authorized merging a branch automatically once its tests passed. The branch added a security control to a shared database: row-level isolation meant to keep one workspace’s data from being readable by another workspace’s queries. The report that came back read clean. 911 tests passed, 29 skipped, zero failed. Read as one sentence, that’s a green light.

It wasn’t, quite. The 29 skipped tests were exactly the ones gated behind a live database connection, and one of them was the only test that exercised the isolation the whole change existed to provide. When that test finally ran, it failed: the database role running the queries turned out to have superuser privileges, and Postgres hard-codes a bypass around row-level security for superusers regardless of how correct the policy itself is. A security control that looked like it worked, didn’t.

What made this catchable at all wasn’t my own review. I hadn’t written the migration and hadn’t run the live-database test myself. It was that the agent who built it said, plainly, inside its own report, that live-database verification hadn’t happened yet, and named the exact command that would run it. That one sentence is what turned “zero failed” from a false green light into a visible open question. Smooth that sentence away the way summaries tend to compress, tests pass, full stop, and the gap ships invisibly. I’d have found out from an actual breach instead of a report.

Working alone, I don’t have this version of the problem. If I do the work myself, I know what I skipped, because I was there. The moment work moves through a chain of agents, and I’m deciding whether to trust a result I never touched, the chain’s safety stops being about any single agent’s competence and becomes about whether every report in that chain says plainly what it didn’t check. One vague report anywhere in the chain, and whatever it’s hiding is invisible to everyone downstream of it, no matter how careful they are.

The space between agents

None of these three failures is about any individual agent being bad at its job. A shared git index, a brief instead of a live conversation, a chain of reports instead of one: these are properties of the space between agents, and that space doesn’t exist until there’s more than one of them working at once.

Running several agents in parallel doesn’t just get more done at the same time. It creates a new place for two agents’ assumptions to quietly disagree, invisible until checking for it becomes a habit rather than a judgment call: what’s staged, not what you assume is staged; what the brief says, not what you meant when you wrote it; what every report in the chain admits to skipping, not just the one you happened to read last.

Originally published on rajiv.com
multi-agent orchestrationAI agentssoftware engineeringsynthesis codingagent coordination