What Went Wrong
The Potluck post-mortem: every failure, traced to its root cause
The Potluck build shipped a working app the family uses every week. It also produced this verdict from the person paying for it: "I consider the multi-agent my first breakdown. I do not consider it a success." Both are true. This page is the honest accounting — every failure from the field notes, sorted by root cause, because a pile of anecdotes teaches nothing and six root causes teach almost everything.
The scoreboard first
| Multi-agent build (actual) | One agent (estimated counterfactual) | |
|---|---|---|
| Wall clock to MVP | ~17 hours | 4–6 hours (one evening) |
| Actual agent work inside that | ~2 hours | 3–4 hours, sequential |
| Session tokens | ~4M+ across build and fix rounds | ~1–1.5M |
| Tommy's attended time | 10–12 hours, "hard to follow" | 4–6 hours, one thread |
| API spend (Claude extraction) | ≈ $6 | same |
| Code conflicts across 5 parallel agents | 0 | n/a |
Read the last row before the rest: the method's core promise — contracts-first parallelism with no collisions — was kept. Five agents wrote ~7,000 lines including tests and not one line of TypeScript conflicted. The breakdown was economic and operational, not architectural. That distinction is the whole post-mortem.
Root cause 1 — The economics of fan-out on a shared budget
What happened. The five lanes shared one account's session rate limit. During the build, agents sat parked behind it for ~13 of the 17 wall-clock hours. During the tuning loop — Tommy testing on his phone, findings going back to the lanes — every agent resume reloaded 150–300K tokens of context. 92% of a session allowance disappeared in roughly ten minutes of fan-out. The night ended with: "your agents are fighting more than we are accomplishing."
Why it happened. Parallel agents multiply token consumption but the constraint wasn't wall-clock — it was one shared budget. Parallelism spends N× tokens to buy time you only receive if nothing else is the bottleneck. Here the rate limit was, so the build paid N× cost for 0× speedup. And per-finding dispatch during iteration is the worst case: a one-line fix costs a full context reload, so the fixed overhead dwarfs the work every single time.
The rule that comes out of it. Multi-agent is a build tool, not an iteration tool. Fan out when lanes are big, independent, and written against contracts; fold back to one agent the moment a human is testing and reacting. Before any dispatch, ask what the resume costs — if the context reload outweighs the change by 100×, type the fix yourself.
This scales past the hobby build, and the enterprise world is already reacting. What burned one person's session allowance in ten minutes here is exactly what corporations are now seeing multiplied across whole engineering organizations — and they're responding the way they respond to any runaway line item: becoming token-sensitive and cutting back. AI spend is being pulled into the same governance as cloud spend — budgets per team, usage dashboards, questions from finance about what the tokens actually bought. In that climate, "we ran five agents in parallel" is not a flex; it's a line on a bill that has to justify itself against "one agent would have shipped the same code overnight for a fifth of the tokens."
Which points at who actually gets to keep working this way. Multi-agent development may end up reserved for two kinds of people: the ones who work at the AI companies themselves — the pattern this series follows came from inside the Claude Code team, where the tokens are the house's own — and the rare outfit that genuinely wants five people doing the work of twenty-five and accepts the token bill as the price of that leverage, eyes open. A large IT organization optimizes the budget, not the leverage; when finance sees the fan-out line, the fan-out is what gets cut. The fan-out math in this post-mortem is the small-scale version of a calculation every platform team is about to do — and the teams that learn to spend tokens where parallelism actually pays, instead of everywhere, are the ones whose budgets will survive the cutbacks.
Root cause 2 — Dispatching before checking whether a human did it
What happened. Two "bugs" from a test round — recipes that had vanished — got three agents dispatched to chase them. The recipes were ones Tommy had deleted on purpose, in the admin UI, minutes earlier.
Why it happened. The Orchestrator treated every finding as a defect and every defect as a dispatch. There was no cheap triage step — a ten-second database query, or simply asking "did you delete these?" — between observation and mobilization. In a single-agent flow this mistake costs one wrong glance; in a fan-out flow it costs three context reloads and the operator's trust.
The rule. A finding is a hypothesis, not a work order. Check for a user action first — the cheapest possible query — before anything with a price tag moves. (This one later grew a happy tail: the deleted recipes exposed a real modeling question — a done queue row with a null recipe is indistinguishable from one whose recipe was deleted — which the tests now account for.)
Root cause 3 — Plans encode assumptions no one has verified
What happened, repeatedly. The plan said the Instagram export was JSON with post links; it's HTML, with full captions — which inverted the entire backfill design (the polite scraper became the exception path for 54 of 2,143 posts). The plan said "your saved posts"; the real unit is collections, and only one of twenty was recipes — without a preview step, 2,089 Claude calls would have chased a few dozen recipes. The plan said "temperature 0"; Sonnet 5 rejects sampling parameters outright. The plan said int4range; Drizzle has no range type. The first real household upload — "Moms Recipes.pdf" — wasn't a PDF at all by its magic bytes: macOS TextEdit had wrapped it in an RTFD container with %PDF- at byte 16,384.
Why it happened. None of these are careless errors — they're the normal state of a plan written before touching the external systems it depends on. Export formats, API surfaces, ORM capabilities, and what users actually upload are all facts you rent, not own. The failure mode isn't having assumptions; it's not knowing which parts of the plan are assumptions.
The rule. Put the seams where the assumptions are. Potluck survived every correction cheaply because the design isolated them: one queue everything feeds (the format flip changed a parser, nothing else), raw text stored everywhere (re-extraction is free), and a decision log in the plan (PLAN.md §0) so every correction was recorded the moment it was made. And validate the riskiest assumption with real data first — one real export file inspected on day one would have deleted the scraper before it was designed.
Root cause 4 — Tools that report success while doing the wrong thing
What happened. The best story of the build: Mom's 63-page scanned cookbook rendered as 63 blank white pages, with zero errors, and vision dutifully reported "no recipes in this document" — the honest answer to the wrong question. The scan was JBIG2-encoded; pdfjs decodes that with WebAssembly it must be explicitly told how to locate under Node, and its only complaint was a console.log warning nobody reads in a background job. Same family: drizzle-kit generated a migration with bound placeholders (CHECK (source_type IN ($1, $2))) that Postgres would reject — and reported success; the eval script was wired so it could never call Claude, making the 90% merge gate unreachable while looking like a working command; cache_control on the extraction prompt silently did nothing (below the model's minimum cacheable prefix); and Vitest's default reporter hides console output from passing tests, so "your test printed nothing" looked like a bug and wasn't.
Why it happened. Exit code zero measures "the tool ran," not "the thing you meant happened." Every one of these tools was working as documented; the gap was between what success signals claim and what correctness requires. The JBIG2 case is the purest form: every component behaved — the renderer rendered, vision answered, the queue recorded — and the composed system confidently produced garbage.
The rules. Read generated artifacts before running them (the migration bug was caught exactly that way). Turn library warnings into hard failures at the seam — the fix added a blank-render detector in front of the vision call, so a page that paints images yet renders one flat color now fails the row with a codec hint instead of asking Claude about a white rectangle. And prove your gate numbers are reachable: an eval that can't fail is not a gate, and neither is one that can't run.
Root cause 5 — Two versions of the code, one shared state
What happened. Hours after the JBIG2 fix merged, nine perfectly legible recipe cards came back no_recipe at confidence 0.00 — the blank-render signature again. The fix wasn't broken: a long-running npm run drain worker had been started before the fix landed. tsx loads modules once; the old code stayed in memory, saw fresh queue rows, claimed nine, rendered them white. The hot-reloading dev server processed the other 46 with the new code. Same queue, two processes, two versions. Smaller cousins: a dev server started before a schema change kept serving the old validator ("Unrecognized keys" on a valid request), and — after the platform fold-in — a dev server served a stale security-header bake until restarted.
Why it happened. Hot reload creates the illusion that "the code" is one thing. Every long-lived process is a snapshot; shared state (a queue, a database) lets snapshots from different eras interleave. Note what didn't fail: the queue's claim semantics prevented double-processing exactly as designed — the design held while the deployment story didn't.
The rule. Restart every long-running worker after any merge that touches its code path — or have workers record the git HEAD they started from so skew is at least visible. In production this is why deploys restart everything; dev needs the same discipline manually.
Root cause 6 — Trusting the measuring instrument
What happened. "It kind of sucks" → screenshots showed the family link clipped on the right at phone width. Two CSS fixes later it was still clipped — because headless Chrome on macOS won't open a window narrower than ~500px, so every 414px screenshot was cropped by the tool. An hour went into a bug that did not exist. Same family: curl -w '%{http_code}' appended a status code into a captured JSON body, breaking a smoke test's parser with redirects that looked like an auth bug; a wait loop that pgreped for its own command line matched itself and waited forever; and the Orchestrator misread the export's collections file by grepping it wrong ("collections are people") while the shipped parser had it right — recorded in the notes because the operator being wrong belongs in the log too.
The rule. Before debugging what the instrument shows, verify the instrument. One direct check — is the DOM actually wider than the viewport? — beats a speculative fix every time. If a report and the code disagree, suspect the report's provenance first.
Root cause 7 — Context is not memory
What happened. Between the scaffold commit and the contracts, the Orchestrator window lost its conversation. Everything decided in chat and not yet written down was simply gone; work restarted from the plan file.
The rule, verbatim from the notes: "the plan file and STATUS.md are the memory; anything decided in chat and not written down did not happen." From that moment every decision landed in the plan's decision log as it was made — which is also what made the later reality corrections cheap to absorb. Durable state lives on disk. Chat is a scratchpad.
What the method still earned
An honest post-mortem cuts both ways. The field notes defend, with evidence: contracts-first meant five agents produced zero code conflicts; two independently written cookie helpers were byte-compatible on first contact; the eval harness caught real prompt behavior no one would have eyeballed; the queue invariants meant every failure along the way — dead API key mid-backfill, login walls, blank scans, version skew — left rows with reasons instead of silent loss; and "never silently drop a save" converted even the misses into a measured v2 backlog. The single-agent day that followed shipped fast because it stood on those rails.
The whole page in one line — from the field notes, the night of the pivot: "multi-agent is a build tool, not an iteration tool; the moment a human is testing and reacting, fold back to one agent." Use fan-out when lanes are large, independent, and contract-bound, and no shared rate limit sits under them. Use one agent for everything after first contact with a human tester. Write every decision down the moment it's made. Read what your tools generate. Restart your workers. And check whether a human deleted it before you dispatch anyone.