Back to blogThe Spec Was the Bug

Three weeks ago, an agent on our team at PhoenixDX shipped a billing bug no human engineer would ever have written — and it came from a spec a human had already signed off on. The ticket said: "When a user upgrades their plan, prorate the difference and charge the card on file." One sentence. Our senior engineer read it, nodded, approved the PR. The agent did exactly what the sentence said: on every upgrade event, charge the card. Nobody had written down that Stripe retries webhook deliveries when it doesn't get a fast 2xx back, and that "charge on upgrade" carries an unspoken second half every backend engineer here just knows — once per upgrade, keyed to an idempotency token. The agent's code passed every test we had, because none of our tests simulated a retried webhook. It shipped on a Friday. By Monday, nineteen customers had been charged twice.

Here's the uncomfortable part. That spec wasn't bad — not by any standard we normally judge specs against. It would've sailed through a design review, a ticket audit, a new-hire onboarding doc. It was fatal only to an agent, because a human filling a gap in an implementation reaches for tribal knowledge — the thing you know about webhook retries after they've bitten you once — and an agent reaches for the literal words on the page. The gap wasn't a flaw in the spec. The spec was the bug.

The bottleneck moved, and most teams haven't noticed

For two decades, "write the code" was the scarce skill. What separated a senior engineer from everyone else was speed and correctness turning intent into working software. That's over — not because agents write better code than your seniors do (often they don't), but because implementation speed quietly stopped being the constraint. An agent will hand you the upgrade-billing service in four minutes. Those four minutes were never the bottleneck. The bottleneck was that nobody had turned "charge the card" into "charge the card exactly once, keyed to an idempotency token derived from the Stripe event ID, only if the resulting amount is non-negative." That sentence — not the code — is the real unit of engineering work now. Writing code became writing specs an agent can't misread. The bottleneck moved from implementation to disambiguation.

This isn't a programming skill

What I'm describing looks a lot more like contract drafting than the software engineering we were taught. A contract lawyer's entire job is closing the gaps a literal-minded or bad-faith counterparty could exploit. An agent isn't acting in bad faith, but it's exactly as literal as the worst-case counterparty a lawyer drafts against — it takes the sentence at face value and builds whatever that sentence, read strictly, implies. Most of us were never trained for this. We were trained to fill gaps ourselves, silently, with judgment — which is precisely what made us bad at noticing the gaps existed, because we closed them without ever writing anything down.

  • A spec that survives names the boundary conditions out loud — not "charge the card" but "charge exactly once per upgrade event, idempotent on event ID, skip if amount ≤ 0"
  • A spec that survives says what happens on failure and retry, not just the happy path — an agent has no gut instinct that webhooks retry, networks partition, or jobs run twice
  • A spec that fails reads like a ticket your teammate would approve without blinking — a human nodding along isn't proof the spec is precise, it's often proof the reader silently patched the holes with experience the doc never captured

So review becomes the bottleneck next — except the fix isn't reading faster

Obvious objection: if specs are cheap to write and agents implement in minutes, every PR queues up for human review, and review becomes the next wall. True — but the fix people reach for first is the wrong one. "Read diffs faster" doesn't scale, because the failure mode here isn't a bug you'd catch with sharper eyes — it's a correct-looking implementation of an underspecified sentence, exactly what happened to us. Reading harder doesn't surface the gap; the gap was never in the diff, it was in what the spec left unsaid. What actually works is deciding, ahead of time, which parts of the system an agent gets to touch without a human in the loop at all — drawing trust boundaries around modules by blast radius, not by how good the agent's code has looked so far.

That tiering is an org-design call, not a code-review trick, and it's not in anyone's job description yet. Someone — usually an EM or staff engineer — has to walk the codebase and decide, module by module, what "unsupervised" is allowed to mean, then write that down as policy instead of leaving it as a vibe. We didn't have this before the billing incident. Payments should've been tier three from day one. It wasn't, because we hadn't yet built the habit of asking "what's the blast radius if this spec has a gap" before letting an agent near a module.

What doesn't change — and why it gets more valuable

Here's what agents don't touch, no matter how good they get at turning a precise spec into correct code: someone still has to carry the real mental model of the system, for the incident that doesn't match any spec anyone wrote. When the billing service double-charged customers, the agent that built it had no idea why — by its own lights, it had implemented the sentence correctly. The engineer who found the idempotency gap in eleven minutes at 1am wasn't reading the agent's code line by line. She already knew, from two years of Stripe-integration scar tissue, exactly where a webhook-retry bug hides. That kind of knowledge — the shape of the whole system, the failure modes that live between services rather than inside any one file — doesn't come from reviewing diffs faster. It comes from having built or run the thing long enough to hold a model of it that no document and no agent has. As agents write more of the actual code, that mental model gets harder to build — because the way people used to build it, by writing every module themselves, is exactly the part that's disappearing — and it gets more valuable to have.

So start now, before your team has its own billing incident: write specs the way a contract lawyer drafts a clause — closing the gap a literal reader could exploit, not the gap a generous human reviewer will quietly fill in. Decide your trust zones on purpose, in writing, before an agent touches a module — don't let "it looked fine to me" be the policy. And protect, deliberately, the time it takes someone on your team to keep holding the whole system in their head, because that's the one job an agent's speed can't buy you out of. Flip the old logic: the engineer who used to be valuable for how fast they could write the implementation is now valuable for how precisely they can refuse to — until the spec has no gap left to misread.

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “The Spec Was the Bug” — trade-offs, decisions, or the story behind it.