My repo took 66 commits in the last seven days. Seventeen distinct agent branches merged in the last thirty. I wrote maybe a third of it by hand.
The agents are Claude Code instances, coordinated through Multica, a task board built for teams of humans and coding agents. You'll find plenty of posts about running one coding agent. This is about what happens when you run several against the same codebase at the same time, which is a different problem with a different failure list. Almost none of it is the failure people warn you about. The agents write fine code. What breaks is everything around the code.
The setup
Agents get assigned issues in Multica and work them independently, each opening its own pull request. One of them, Scout, does nothing but find AI tools worth adding to the directory, verify them against their own sources, and prepare a reviewed migration. Its brief ends with a line I deliberately added: never add anything without explicit approval.
Last 30 days: 33 runs, 94% succeeded, 18 minutes 44 seconds average duration, concurrency 6. Two failed with agent execution errors. Every run is on Claude Code with Opus.
Others build features. Looking at merged pull requests from the last month, the branch names tell you how varied it gets: feat/tool-screenshot-carousel, fix/carousel-arrow-styling, feat/dark-logo-guard, chore/retire-applied-2026-08-30-batch, feature/tool-comparison, blog/what-happened-to-codeium.
All of them run on one machine, against one checkout.
The working tree is the shared resource nobody warns you about
Git branches are cheap. The working directory is not, and there is exactly one of it.
In a single afternoon, my tree was on feat/dark-logo-guard, then fix/tool-og-image-own-domain, then back on main. I did not do any of that. Another agent checked out its branch to do its work, which is the correct thing for it to do, and my uncommitted changes came along for the ride because that is how git works.
The first time it bit me, I had staged a set of file deletions, run the gate, and was about to commit. The tree had moved to someone else's feature branch with an unpushed commit. Committing there would have buried my cleanup inside their pull request.
The fix is unglamorous and works completely:
git worktree add /tmp/wt-cleanup main
cp <my files> /tmp/wt-cleanup/
cd /tmp/wt-cleanup && git commit && git push origin main
git worktree remove /tmp/wt-cleanup --force
A second working directory on main, used for the commit, then thrown away. The other agent's checkout is never touched. It costs about thirty seconds, and it is now the only way I push from this repo.
The habit that matters more than the technique: check git branch --show-current and git fetch before every single commit. Not once per session. Every time. The branch that was correct when you started the task is regularly not the branch you are on when you finish it.
Another agent shipped my unfinished work
I built a pricing index page over an afternoon and left it uncommitted while waiting on feedback about the design.
While I was doing something else, another agent found those untracked files, decided the page needed the same header treatment as the rest of the site, wrote a wrapper component, generalized that wrapper to serve a second route as well, and merged the whole thing in pull request #22.
Nothing about that is wrong. The work was better for it. But it is a genuinely strange experience to look for your uncommitted changes and find them already in production, in a component you didn't write.
The lesson is that "uncommitted" is not a signal any agent can read. If work isn't ready to ship, it needs to be on its own branch, not sitting in the tree as an implicit do-not-touch.
Tools collide in ways a single agent never shows you
Some of this is embarrassingly mundane and cost me more time than any of the interesting problems.
That is three Claude Code sessions in one window, each on its own task, all pointed at the same checkout. It looks productive. It is also the exact condition in which the next two problems happen.
npm run verify kills npm run dev. Both own the .next directory. The gate's build wipes the manifest the dev server is reading, and the dev server dies on a missing _buildManifest.js.tmp. Worse, it sometimes survives as a process still bound to the port while serving 500s on every route, so the next thing you check looks catastrophically broken when it is just a corpse holding a socket.
The tell is that every route fails, including ones you didn't touch. When / is throwing 500s and you only changed one component, stop debugging your change:
lsof -ti:3100 | xargs -r kill -9; pkill -9 -f "next dev"; rm -rf .next
With one agent, you notice this once and remember. With several, one agent runs the gate while another reads localhost, and the second agent starts debugging a fault that doesn't exist.
Agents invent shapes that type-check perfectly
Here is the one that reached production.
I needed to write a pricing record into a JSONB column. I knew roughly what it looked like from having read other rows, so I wrote it from memory: a tiers array of objects with name and monthly fields.
The real contract, defined in a TypeScript file one away, uses monthly_usd. JSONB has no schema, so the database accepted it happily. The page component then did this:
const hasAnyPricedTier = tiers.some((t) => t.monthly_usd !== null)
My objects had no monthly_usd at all. undefined !== null is true, so the component decided every tier was priced, tried to format undefined as currency, and threw. The tool page returned 500 for about two minutes until I checked the live URL and reverted.
Nothing caught it. Not the type checker, because the write went through a plain object. Not the build, because the gate points the database at an unreachable address on purpose and prerenders zero tool pages. Only loading the actual page found it.
What I should have done, and now do, is read the interface before writing to the column it describes, and check the shape against existing rows rather than my memory of them. An agent that has seen a hundred JSON objects is extremely confident about what the hundred-and-first looks like.
Parse success is not identity success
Five tools in the directory were showing identical pricing: free, then four dollars, then twenty-one dollars a month. GitHub Copilot was one of them, which is wrong, because Copilot costs ten dollars.
All five were open-source projects whose listings included a GitHub URL. The pricing crawler followed that URL, landed on github.com/pricing, and parsed GitHub's own platform pricing flawlessly. Correct data, wrong company, high confidence, recorded as verified.
It sat there invisibly for weeks because a wrong price on one tool page among hundreds looks like a price. It only became obvious when I aggregated every price onto a single page sorted cheapest first, and five unrelated tools appeared in a block with identical numbers.
The general form: a crawler keys on a URL, and a URL is not an identity. Any automated enrichment step needs a check that the page it parsed is about the thing it thinks it is about.
The guardrails that actually hold
After all of that, the things that work are boring and few.
One command decides whether work is done. npm run verify runs lint, typecheck, a packaged-assets check, and a real production build. It takes about forty seconds. The exit code is the entire rule. No agent hands back work on a red gate, and no agent gets to argue that the failure is unrelated.
The ways around the gate are written down and forbidden by name. This matters more than the gate itself. An agent under pressure to make a check pass is inventive, so the rules are explicit: no eslint-disable comments, no any casts to silence a type error, no @ts-expect-error on the failing line, no deleting the call site, no quietly dropping part of the change so the rest gets through. Writing these down converted a recurring argument into a lookup.
A warning ratchet that only moves one way. The lint baseline is a number in a JSON file, currently 26. New warnings above it fail the gate. Lowering it is fine; raising it requires a stated reason. Without this, warning counts drift one agent upward at a time, and nobody is responsible.
Everything that writes to production is dry run by default. The migration tool reads only, and prints exactly which rows it would write, unless you pass --execute. The media scripts download and quality-gate images into a temp directory and upload nothing unless you use --upload.
Approval is a separate, named step. Generating a migration is not approval to run it. The sequence is generate, validate, dry run, show the output, wait for an explicit yes, then execute. Approval covers one batch and expires with it. This is the rule I would keep if I could only keep one, because it is the difference between an agent that proposes and an agent that acts.
Read results back with the least privileged key you have. After a write, the row gets read again with the anonymous key rather than the service key, because the question is not "did the write succeed" but "can a visitor see this."
What I would tell someone starting
Put the rules where the agents read them. Mine live in a CLAUDE.md at the repo root, and the ones that get followed are the ones written as commands with named forbidden alternatives, not as principles.
Assume the working tree is contended. Check the branch before every commit and use a worktree to push.
Verify content, not metadata. While cleaning up applied migrations, I nearly deleted two as unapplied, because the updated_at column on their rows said January and the migration files said August. Comparing the migration text to the live content showed both had, in fact, run. The timestamp column simply was not maintained on that write path. Timestamps lie; content does not.
And the honest limitation: this repo has no test suite yet. The gate proves the code builds and type-checks. It does not prove the code behaves. Everything above is scaffolding around that gap, and the scaffolding is why several agents can work in one repo without breaking it, not a substitute for tests I still owe myself.
Nobody has figured this out yet
I want to be clear that none of the above is advice from someone who has solved it. Every rule in this post exists because something went wrong first, and I expect half of them to look naive in a year.
Writing code is arguably the solved part now. Across those 66 commits, almost nothing failed because an agent could not write the function. What failed was two agents wanting the same working tree, an agent confidently inventing a data shape, a crawler parsing the right page for the wrong company, and a dev server dying because two processes assumed they owned a directory. Those are coordination problems, and coordination is where the tooling is thinnest.
Humans have decades of accumulated practice working together in one codebase. Version control, code review, CI, branch protection, on-call rotations. All of it assumes participants who get tired, remember yesterday, and ask before doing something irreversible. Agents have none of those properties. They are fast, tireless, extremely confident, and have no memory of what they broke last Tuesday unless you wrote it down somewhere they can read.
So we are all improvising the equivalent layer, in public, right now. Some of what I do will turn out to be the right pattern. More of it will turn out to be a workaround for a gap that gets closed properly. The git worktree dance is almost certainly the second kind. It works, and it is obviously a symptom of tools that assume one operator per checkout.
If you are running more than one agent against real code, you are doing original work whether you meant to or not. Write down what breaks. That is the actual contribution at this stage, and it is worth more than another post about how fast the code got written.


Top comments (0)