Back to Writing

Vibe Coding Is Gambling. Here’s How I Made It Engineering.

A component passed every test, cleared review, and never ran. What I learned about building software with AI agents, and the spec-first harness I now use on every project.

10 min readOriginally published on FutureLab

My SecretDetector was perfect.

Unit tests passing. Code review clean. It did exactly what it was built to do: scan messages for credentials and redact them before they hit storage. I built it, tested it, and moved on to the next feature with the quiet satisfaction of a job done well.

It was never wired into the production path.

The component sat in the codebase like a fire extinguisher behind a painted-over cabinet door. Every message flowing through my eleven-agent system was being stored with credentials intact, on an append-only audit bus that by design never deletes anything. The code meant to prevent that was three function calls away from where it needed to be, passing every test and doing nothing.

Nothing crashed. Nothing alerted.

I didn’t find it through testing, and I didn’t find it through review. I found it because a spec document, written before the code, said “SecretDetector must redact credentials before bus storage,” and a separate review agent compared that sentence against the actual execution path. The spec was the source of truth. The code was the liar.

The bug wasn’t in the code. It was in the wiring.

How I got here

I spent a decade as a software engineer before switching to education. These days I’m Director of Academics at Newton School of Technology in Bangalore. I think in systems: how to design processes that produce predictable outcomes at scale.

For most of the past year I’ve been building software with AI, and for most of that year I did what everyone does. Describe the feature. Watch code appear. Feel the small hit of seeing it work. Then spend three times as long debugging the thing that was supposed to work and didn’t.

The pattern repeated. The model would produce code that looked right, and it would even run. Somewhere in the gap between “this function works” and “this system works,” things broke quietly: features that tested fine in isolation but weren’t connected to anything, error handling in one module and missing from the one that needed it, authorization checks that were written, reviewed, approved, and never called.

I was vibe coding. And vibe coding, I learned, is gambling with a very convincing dealer.

The dead code wiring pattern

The SecretDetector wasn’t a one-off. When I sat down and compared my specifications against my code paths line by line, I found the same failure all over one project. Eight bugs, none catchable by unit tests, because every component worked perfectly on its own:

What the spec saidWhat the code didWhat would have shipped
SecretDetector must redact before bus storageImplemented, tested, never wired into Gateway.Route()Credentials flowing unredacted through the audit bus
Permissions enforced at executionToolRegistry.Execute() existed; permission check never calledAny agent can execute any tool
System budget: $25 daily, $500 monthlyConfig values existed; WithSystemCaps() never called at init11 agents with no spending limit
Circuit breaker: 5-minute recoveryCooldown set to 30 minutes6x longer downtime after failures
Cost-history SQL queryString concatenationSQL injection in cost queries
Privilege check for the coordinating agentSubstring matchPrivilege escalation via a crafted agent name
Integrity checks on every agent’s configPath computed, never passed to the daemonProtection disabled for all 11 agents
W3C-compliant trace IDsWrong ID formatBroken distributed tracing

Fully coded, tested, passing, and not wired. I call this the dead code wiring pattern: components that are built and green in isolation but never connected to the live paths where they have to operate. Traditional QA doesn’t see it because every piece works. The failure lives in the connections.

This is the specific failure mode of AI-assisted development. Models are very good at generating correct components. They are bad at making sure those components are wired into a coherent system, and the gap widens as the system grows. Unit tests verify components. Spec reviews verify wiring. They catch different categories of bugs, and vibe coding only gives you the first.

The fix wasn’t “prompt better.” It wasn’t “review harder.” It was putting a structural harness around the whole process.

Build the harness, not the software

The core idea isn’t mine. In his talk “No Vibes Allowed,” Dex Horthy of HumanLayer lays out what he calls context engineering and harness engineering.1 It is the mental model I’ve adopted:

In the AI era, you don’t build the software directly. You build the structural harness (specifications, architectural constraints, adversarial validation gates) that forces AI systems to produce production-grade outcomes.

The tools matter less than the rigor. The implementation I use is BMAD, the Breakthrough Method of Agile AI Driven Development,2 an open-source set of structured agents and workflows. It is close to a summary of the engineering practices we followed running teams, adapted for a world where the AI is now the one holding the context.

Here is what the harness looks like on every project.

It starts with structured brainstorming, not “give me ideas.” BMAD ships dozens of named techniques (Cross-Pollination, SCAMPER, Failure Analysis, First Principles), and the agent acts as a facilitator that pulls ideas out of you instead of inventing them for you. One session produced 47 numbered ideas across four techniques. Most were bad. That’s the point: you explore the space, then converge, and whatever survives into the requirements has been tested against alternatives.

Then comes a PRD, written for two audiences. Humans read it for vision. Downstream agents consume it to produce architecture, stories, and tests. My largest has 88 functional requirements and 32 non-functional ones, each stated as a capability, not an implementation. “Users can reset their password via email link” is a requirement. “System sends a JWT via email and validates it against the database” is a design decision that snuck in early.

Then the PRD gets attacked. A different model from the one that wrote it runs twelve adversarial checks: measurability, traceability to a user journey, implementation leakage, and more. On one project it found 26 functional requirements, almost 30% of the total, with no user journey behind them. On my agent system it flagged that I had written specific vendor model names into the tiering strategy. That would have baked vendor lock-in into the document every downstream agent reads. The fix was capability tiers (premium, standard, economy) that work across providers.

Only then do architecture and epics follow, with requirement coverage checked in the frontmatter so no requirement is orphaned. Then comes a readiness gate that actually blocks. One project passed six of seven checks and was held on a P0 security condition, credential exposure, until it was fixed before Sprint 1. A rubber-stamp gate is overhead. A gate that can say no is engineering.

Then, and only then, code.

Every artifact in that chain lists its inputs in its frontmatter. When an architecture doc declares that it was built from the PRD, the UX spec, and the brainstorming session, any fresh agent that reads it inherits the whole decision chain without the original conversation. Context flows forward through artifacts, not through memory.

Keep every agent in its smart zone

Planning is half of it. The other half is how the code gets written, and that is governed by the physics of context windows.

Horthy draws a line between a “smart zone” and a “dumb zone.” A fresh context window is sharp and precise. As it fills with history, dead ends, and loaded files, quality degrades, and the agent starts producing what he calls slop: plausible-looking code that quietly damages the architecture. The most naive way to build with AI is to keep prompting the same session until the window is full.

So I separate the intelligence of the plan from the mechanics of execution. BMAD tells me what to build. Execution frameworks like GSD3 and Superpowers4 break that plan into small units sized to keep the coding agent in the smart zone: two or three tasks per plan, targeting about half the context window. When a task finishes, the session ends, state is saved, and a fresh agent picks up the next one. The plan carries the intelligence. The execution layer makes sure no agent lives long enough to get dumb.

On larger projects I run several coding agents in parallel, each in its own git worktree with exclusive file ownership, coordinated through shared state (artifact frontmatter, status files, an issue tracker) rather than by talking to each other directly. Indirect communication is slower. It is also far more reliable.

The review layer is hostile by design. The code-review directive asks for a minimum of three to ten specific issues. The review agent cross-checks the git diff against what the story claims, looks for evidence behind every “done,” and if it finds fewer than three problems it has to look again. It works because the reviewer has no motivated reasoning. It didn’t write the code, so it has no stake in the code being right.

The numbers

Across five projects built this way: roughly 80,000 lines of production code (74,000 of them Go, in my personal agent system), more than 2,000 tests, and 193 functional requirements traced from spec to implementation.

My most complete project carries 12,341 lines of test code against 4,332 lines of production code: 456 tests across 44 suites, 82% coverage. That isn’t typical for a solo developer. It isn’t typical for a team. It is what happens when tests are generated from specs instead of written afterwards.

The before-and-after on a single project makes the case best. It started as ordinary vibe coding: 40-plus API endpoints with 12 documented, two competing data-access patterns in the same codebase, no trace between what was planned and what was built. After BMAD: 54 requirements traced through 8 epics and 43 stories, every artifact recording its inputs, and a readiness report that blocked deployment until the security issues were closed.

It turned vibe coding into auditable coding.

What I got wrong about overhead

Not everything the method produces earns its keep.

The first run on one project generated 36 UX screen specifications from a template. They were near-identical boilerplate. A later audit scored them 1 out of 10 for test readiness, against 7.5 for the architecture specs, and they had to be rewritten from scratch. The artifacts existed, and their existence created false confidence. Having a spec and having a good spec are different things, and you only learn which one you have by attacking it.

Having a spec is also different from the spec being enforced. My agent system had 21 specification documents and still needed a dedicated audit pass to find the eight wiring bugs above. Specs are necessary. They are not sufficient without something other than the builder checking the code against them.

Some steps are ceremony for a solo builder. Three deep research documents before a personal tool. The ninth, tenth, and eleventh polish passes on a PRD. Emotional journey mapping for software where I am the only user; “operator calm” does not need to be a documented UX state when I am the operator.

Here is the asymmetry that keeps me using it anyway. A boilerplate screen spec costs an hour of rewriting. A missing spec lets a SQL injection reach production. The overhead of over-specification is recoverable. The overhead of under-specification is catastrophic.

When in doubt, specify.

The principle underneath

The spec defines what the system should be able to do when it’s finished. The tests verify that capability exists. The code is the thing you build last, in service of outcomes you defined first.

I’m not against vibe coding. I vibe code every day. The difference is that I do it inside a structure that catches the failures my intuition can’t see. The AI writes the code. The spec says whether the code is right. A separate, adversarial agent says whether the spec is being followed.

If you want to try it, BMAD installs with npx bmad-method install; open your agent of choice and run /bmad-help to see where to start. But the framework is not the point. Define what you’re building before you build it, verify what you built against what you defined, and make sure the definition and the verification are done by something other than the builder.

Vibe coding without specs is gambling. With them, it’s engineering.

And the house doesn’t always win.

Footnotes

  1. Dex Horthy, “No Vibes Allowed,” HumanLayer. youtube.com/watch?v=rmvDxxNubIg (opens in a new tab) ↩

  2. BMAD Method. github.com/bmad-code-org/BMAD-METHOD (opens in a new tab) ↩

  3. GSD. github.com/glittercowboy/get-shit-done (opens in a new tab) ↩

  4. Superpowers. github.com/obra/superpowers (opens in a new tab) ↩

Chapters