← All posts
Zerolve Team

Five Agents, One App, Twelve Days to the App Store

How five parallel agents took Bell Club from fork to App Review in twelve days: 288 merged PRs, 4,297 tests, and the five rules that made it work. Plus everything that broke.

ai-agentsmulti-agentbellclubproductionarchitecture

On October 10 we forked a new app out of an old one. On October 22, Bell Club 1.0.0 went to App Review: a kettlebell training app for iPhone, iPad and Apple Watch, with small private crews, a Lock Screen Live Activity, subscriptions and a landing page with a film made in code.

In between: 288 merged pull requests, 307 commits on main, 4,297 unit tests and 15 end-to-end flows. Most of the code was written by agents. On busy days, five of them were working at once on the same repository.

This post covers how that worked and the five rules that made it possible. It also covers what broke, which is the more useful part.

The setup

One human owns the product. One Claude session acts as the integrator. Everything else is an agent with a narrow job and a written brief.

A typical afternoon on October 19 looked like this:

  • Legal: move the privacy policy and terms out of the app and onto the website
  • App fixes: five reported bugs in Progress, records and the workout player
  • Watch: two rest-day bugs on the Apple Watch
  • Tooling: release scripts and leftover docs from the app we forked
  • iPad: make the whole app work on iPad, take the App Store screenshots, run the test suite

All five ran in parallel. None of them merged anything. They opened pull requests, and the integrator rebased, ran the gates and merged.

Rule 1: every agent gets its own worktree and a do-not-touch list

Each agent works in its own git worktree, on its own branch, based on a fresh origin/main. The brief names the files it owns and the files owned by the others.

Four files nobody touches: the lint ratchet in package.json, the feature coverage ledger, CLAUDE.md and the Jest config. When an agent needs one of them changed, it says so in its report and the integrator does it at merge time. Five agents each lowering a warning count would give you five merge conflicts over one number.

Disjoint ownership is the whole reason this works. With it, the merges were boring. When two PRs did touch the same lines, the integrator resolved it in a minute.

Rule 2: agents don't merge

This sounds like bureaucracy. It isn't. Every agent report is treated as evidence, not as a result.

That paid off more than we expected. Agents told to fix a list of bugs came back with "already fixed on main" more often than with fixes. Of the five app bugs on October 19, three had been fixed by earlier PRs, and both Watch bugs had been too. An agent that is allowed to merge would have "fixed" them again.

The other direction mattered too. The legal agent mentioned, as a side note, that a worker test still read a file it had just deleted. Worker tests only run when worker code changes, so nothing had gone red. That test would have failed the production deploy on release day. One line in a report, checked by someone else, saved a release.

Rule 3: write the failing test first, then try to break it

Every brief says the same thing: write the test that fails, make it pass, then put the bug back and watch the test go red. A test that never failed is a claim, not evidence.

Agents are good at this when you ask, and bad at it when you don't. Roughly one test in four that looked right on the first try passed for the wrong reason:

  • A privacy test checked that a Garmin heart rate of "137" never appears in a crew post. It searched the whole JSON, including a random UUID. On release day the UUID was …-4137-… and the release build failed its checks.
  • A CSV privacy check looked for the rounded value "614". The leak it was meant to catch wrote "613.7".
  • A test for the rest-screen headline passed with the bug in place, because the test data's exercise happened to be called "Rest".

Every one of these was caught by deliberately breaking the code and seeing nothing go red.

Rule 4: the dangerous rules are hooks, not prose

Some mistakes are cheap. Some rewind your main branch. The expensive ones are blocked by a shell hook that runs before every command an agent issues:

  • force-pushing main
  • pushing a release tag
  • git stash (the stash is shared across every worktree, so one agent's stash lands on another's)
  • setting a server secret
  • printing or editing an .env file

The hook blocked the integrator three times this build, on a reflexive git stash list, and it was right each time. The release tag block did its job too. Shipping to the App Store stayed an explicit human decision, said out loud in the chat.

Rule 5: one simulator, one owner

Maestro, Metro and the iOS Simulator are a single shared resource. Exactly one agent is allowed to touch them, and everyone else is told so in writing. Visual checks are queued for that agent.

We still broke this once. An end-to-end run was restarted while the previous one was still alive. Two runners fought over one device, and the first one never connected. It cost a re-run and nothing else, but it is the kind of thing that eats an afternoon if you don't know to look for it.

What broke (honest)

A day of updates that reached nobody. Our build pipeline decided "native build or over-the-air update" by looking at which files changed. Two harmless-looking commits (a comment in .gitignore and a removed npm script) each changed the app's runtime fingerprint. The pipeline called both "JS only", published updates for a runtime no installed build had, and reported success. Testers just saw "built-in" next to the version number for a day. The fix: decide by comparing fingerprints with the last finished build, and refuse to publish an update when no build matches. A red pipeline beats a silent one.

CI stopped, and it looked like our fault. On October 19 every workflow failed. No logs, no failed step. The cause was a GitHub billing problem: jobs were refused before they started. It took a few minutes to find, because the instinct is to look at the last merge. Now we read the job annotation first.

Agents share a temp directory. One agent restored a file from a backup in the shared $TMPDIR. The backup belonged to a different agent, and it overwrote a file mid-task. The agent noticed and put its own version back, but it shouldn't have been possible. Scratch files now carry the agent's name.

The integrator's own tools lived in a temp folder. The script that rebases, gates and merges every PR sat in a session scratch directory. On release day, macOS cleaned it up. It was rebuilt from memory in a few minutes, but it should never have lived there.

Rules get broken under pressure. One agent used --no-verify to skip the pre-commit hook on a throwaway local commit. It was never pushed, and it was reported. It's still the exact habit the gates exist to stop.

Not everything was the code. A performance report on our landing page flagged a 127 kB render-blocking script we didn't write. It turned out to be a VPN's "threat protection" injecting itself into every page on the owner's Mac. The Lighthouse agent found it by noticing the script's URL changed on every request and that headless Chrome never saw it.

What we'd do differently

  • Put the integrator's tooling in the repo from day one. Scripts that merge things are infrastructure.
  • Give every agent its own temp directory. Shared scratch space is shared state.
  • Make the pipeline's decisions measurable. "Which files changed" is a guess. A fingerprint is a fact.
  • Ask for "already fixed?" first. Half the bug reports we handed out were stale. A five-minute check against main before starting would have saved hours of agent time.

The numbers

Fork to App Review 12 days
Merged pull requests 288
Commits on main 307
Unit and render tests 4,297
End-to-end flows 15 (11 in default suite)
Agents at once, peak 5
Lint warnings ratcheted, only go down

The core insight

Parallel agents don't fail because they write bad code. They fail at the seams: shared files, shared temp folders, shared simulators, a pipeline that guesses. Almost everything that went wrong in these twelve days was a seam, not a bug.

So the work is mostly drawing boundaries. Who owns which file. Who may touch the device. Which moves need a human. Which claims need a second look. Get those right and five agents feel like a team. Get them wrong and they feel like five people editing the same Google Doc.

Bell Club made it through review. It's live at bellclub.app, and the launch post is here.

Part of our Agent Squad series. Questions or building something similar? hello@zerolve.io