All writing

Leading AI-assisted teams · 1 of 5

Twelve ways a multi-agent team broke, and the rule each one produced

Twelve real incidents from a five-person, five-agent engineering team, and the written rule each one produced.

Fortan Pireva 6 min read ai-engineeringagentsprocess

For about two months this year, five engineers built a procurement backend for a global CPG manufacturer with each of us driving our own Claude Code session. It worked. We also broke things in ways I had not seen a human team break them. The useful part is that every incident was written down in the repo next to the rule it produced. Here are twelve of them.

1. git stash is repo-global

Two Sonnet agents were working in separate worktrees of the same clone. Each ran git stash and git stash pop. The stash stack belongs to the repository, not the worktree, so each agent popped the other’s work in progress into the wrong tree. Nothing was lost because both agents noticed, backed the foreign changes up outside git, and re-stashed them with the label RECOVERED-NOT-MINE-DO-NOT-DROP.

Rule: agents in worktrees never run bare git stash. Commit to your own branch instead. Never drop, pop or clear a stash you did not create, because it may be another agent’s only copy and that agent may still be running.

2. The shared clone’s HEAD is also repo-global

Two sessions ran in the same clone on the integrator’s machine. A build session checked the clone out onto its feature branch. The orchestration session’s file reads broke twice, because files it needed were on the other branch. The two sessions detected it and negotiated a fix by cross-session message.

Rule: feature-branch work in a shared clone happens in a worktree. The shared clone stays on the coordination branch. A session that finds the clone on an unexpected branch fixes it forward and says so.

3. The invisible claim

This one was mine. I committed my board claim for a batch of work to my feature branch. On the coordination branch, the package row read ”, ” for three days. The integrator raised the routing, stating I had never claimed it. The package was re-routed to another machine, which was dispatched to build four batches. Only that machine noticing the first batch already existed stopped it from being built twice. Roughly 477 lines of working, tested code came within one machine’s diligence of being duplicated.

The dispatch that recorded it also recorded the integrator’s share: “M1 never ran git log --all --author across the branches, which is a single command and would have found it.”

Rule: the claim belongs on the shared branch, pushed. A push on a feature branch is not a claim, because nobody reads other machines’ branches.

4. The stale State column

A session reported packages to the sponsor as unmerged when they had merged. The board’s State column had not been updated when the merge happened. The board records two lessons from it:

the git history is the authority on what merged, and the board is a summary of it. git log --merges would have answered this in one command.

The board goes on to note that the same class of mistake, “a check narrower than the claim it supports,” was by then recorded five times.

5. Merged without its migration, and the tests stayed green

A package merged without its database migration. The test suite builds the schema from the ORM, so the suite stayed green “while real Postgres would have failed for every caller.” The fix was organisational as much as technical: all migrations are authored by one machine, every other machine commits a migration request file, and the first full run against real Postgres became a milestone in its own right.

6. Fail-open checks

A compliance check took a data parameter that had zero call sites anywhere, defaulted it to an empty dict, and was written if coa_data.get("gl_accounts"):. So it recorded a CRITICAL check as PASSED when it had no data. The board calls it “the third instance of that pattern found this cycle.” Fail-open became a named defect class we grepped for in review.

7. commit() inside tests

One package called commit() inside tests, which broke the test engine for everything that ran after it. The suite went from 11 to 120 failures on a single day and cost a rework round. The onboarding checklist for the next machine says simply: use flush().

8. Single-sample blame

We blamed a package for test failures based on one run. A second run on the unchanged commit showed a different count. The bare coordination branch ranged from 11 to 33 failures on the same commit, fully explained by a handful of flaky files. The test bar was rewritten: a failure counts only if it reproduces with its file run alone and is absent from the merge base, and you take two samples.

9. The reminder loop that died quietly

In September the integrator’s machine ran an hourly loop that re-sent reminders for open asks. In the measured window, 85 of 124 coordination commits on that side were reminders, re-sent for items whose state had not changed. The loop also “died silently once (context reset, 28→29 Sep) and cost 117 scheduler turns.” The ruling that retired it has the principle in its title: state is pull on demand.

10. Rules that create impossible obligations

Two of our own rules, R4 and R6, each mandated an artifact that a legitimate class of edit could not produce. R4 required a commit trailer from a closed vocabulary, and a maintenance commit fit none of the values. R6 required a regenerated head file in the same commit, and a no-op projection produces no change. The machine that hit the first one refused to invent a value, and wrote up the gap instead. The ruling that followed was the same both times: “write down that the absence IS compliance rather than to invent a value.”

11. The token-limited branch

One machine ran out of tokens mid-build with its branch unmerged. By the time anyone could pick it up, the branch was 802 insertions against 6,974 deletions relative to the coordination branch. Merging it as-is “would revert WS-1A.1/1A.2/1B.5 and delete all four of M1’s migrations.” A three-dot diff hid this behind a “multiple merge bases” warning and showed a plausible changeset. The kickoff prompt for the machine that inherited it opens with a stop sign and the instruction to use a two-dot diff.

Rule: a token-limited machine is a distinct state from offline. Its branches do not transfer automatically. Someone re-owns them explicitly, with a two-dot diff first.

12. The scope was built on a summary of a transcript with a hole in it

Before the build started, we red-teamed our own scope documents. The first finding: the meeting transcript we had contained timestamps from 0:07 to 0:29, then jumped to 26:52. The entire substantive middle was missing. Everything we “knew” about the meeting came from an AI-generated summary whose own first line read “Generated by AI. Make sure to check for accuracy.” We had quoted it as a primary source throughout two planning documents.

Rule: a summary is not a source. If the primary is missing, say so, and route the questions to the people who were in the room.

What I’d tell you to do

  • Treat every piece of repo-global state (stash, HEAD, reflog, index lock) as shared memory between agents. Worktrees and branches are the only safe scratch space.
  • When a report turns out wrong, record the scope of the check that produced it. Most of ours were “a check narrower than the claim.”
  • Grep for fail-open shapes in review: a parameter with no callers, a default that makes the guard pass.
  • Never blame on one sample. Take two, compare by test id, prefer a grep for call sites over a third full run.
  • When a rule cannot be satisfied, write down that the absence is compliance. Do not let the machine invent a value.
  • Retire any loop that pushes state to humans who are not reading it. State is pull on demand.