Raw capture â raw/spec-driven-development.md
url: https://www.dbreunig.com/2026/03/04/the-spec-driven-development-triangle.html date_fetched: 2026-07-05 backfilled: true
The following is a write up of a talk I delivered at MLOps Communityâs âCoding Agentsâ conference, on March 3rd. Thereâs a video version of the talk available on YouTube.
I share what I learned building a no-code library, why spec-driven development is a feedback loop not a straight line, historical parallels for our current moment, and a PoC tool for keeping specs/tests/code in sync.
Finally, we consider what GitHub should look like in the era of coding agents.
I was invited here today to talk about a project I launched â a software library with no code â which got a lot of really interesting feedback. Iâm going to tell you the whole story, how I got it wrong, explore a bit of historical context, then propose a path forward.
Last Fall, Opus 4.5 launched and surprised everybody with the quality of the code it was produced and the problems it could solve. Opus 4.5 was good enough that we started to ask some really big questions.
I wondered: if the agents are good enough, why do we need to share code?
Whenever I have a big question like this, one that requires lots of thought, I like to go for a long bike ride. So I did, and while I was riding I came up with the idea to ship a software library with no code.
And so we have whenwords.
Open source, freely licensed. Itâs a GitHub repository with a markdown file describing what the library is supposed to do. Itâs a library that takes a Unix timestamp and converts it into something human-readable â âabout 12 oâclock,â âfive hours ago,â things like that.
I also generated about 750 conformance tests in YAML: given this input, I expect this output. And one more file called install.md â a single paragraph youâd paste into the agent of your choice, with all the instructions for building the code. Youâd drop in what language you wanted and where to save it.
whenwords kicked off a lot of conversation about spec-driven development. Itâs something more and more people are thinking about: the idea that if you bring specs, which define the what, why, and sometimes how, and tests, which measure and validate behavior, the code will just flow from that. Give it to an agent, get code out.
whenwords kind of blew up. Karpathy was a fan. whenwords has over 1,000 stars on Github.
What was even crazier was that I started getting normal GitHub interactions. People submitted issues. They submitted pull requests. And the pull requests were good, things like: âIn this test, youâre expecting this result, but that violates the rounding rule you detail in the spec. You need to true these up.â
But I wasnât the only one with this idea. Larger teams started shipping larger projects. whenwords was a toy; small, constrained, 750 tests. But then:
Vercel released just-bash, a simulated bash environment with an in-memory virtual filesystem, written in TypeScript. Basically re-implementing Bash in TypeScript. Theyâre running shell scripts against it to verify behavior.
Pydantic released Monty, a Python interpreter written in Rust. Fast, safe, ideal for agent REPLs and code use. Same approach: a pile of Python tests, throw it at the model, make it pass.
Anthropic famously threw 16 Claudes and $20,000 at a spec suite to build a Rust-based C compiler. It didnât really work. But it was pretty cool.
I couldnât stop thinking about Spec Driven Development and how far we might push it.
I think thereâs a few learnings from this first wave of Spec Driven Development.
Tests and specs arenât free or easy. All the projects we surveyed used large existing testing libraries from existing projects: the Bash tests, the Python tests, the C tests. Those are the low-hanging fruit. I joked online (and Iâm not the first) that pretty soon anyone who wants to protect themselves is going to be like SQLite, where the code is free but youâve got to pay for the tests. Tests are precious.
Implementation is fast, but not instant. You go fast at first, but none of these projects are complete. just-bash is still being worked on. Monty is missing JSON and other standard libraries. Anthropicâs C compiler stalled out. It gets hard. Itâs not perfect and itâs not free.
As complexity grows, structural choices become more important. This was especially clear in the Anthropic project. They got incredibly far, down to 1% of failing tests. But every time they fixed a new bug, it broke something else. Systemic changes required systemic thinking, not just local fixes.
Architectures that allow parallel development are incredibly valuable. What it allows you to do is move fast with multiple agents. And, this is something I havenât seen explored yet, it allows for open source contribution. Rather than spending $20,000 to build a C compiler, what if you structured it so everyone knows what chunk they can work on? Itâs like SETI@home, except Iâm not using your engineering expertise. Iâm using your Claude Code subscription. Which I think is wild.
But the biggest learning â and the one weâre going to spend the most time on today â is that sometimes the spec and tests arenât sufficient.
One of my favorite things to do is look at the PRs and issues for all of these libraries. Even with a great spec â âmake it run Python perfectly in Rust, here are all the tests, just make them passâ â there are still 20-comment threads about what the right way to implement something is.
Because no spec is perfect. And this is probably my biggest takeaway today:
Implementing the code helps us improve our spec.
Letâs take a digression. Weâre at the Computer History Museum, so letâs go back into history. Specifically the history of code and managing code.
One of my favorite jokes about AI development is one I stole from Matt Levine, who writes the finance newsletter Money Stuff. In it, he has a running joke about crypto people speed-running financial history, from first principles, as they attempt to build new financial infrastructure. We are doing that with software engineering and AI coding.
Iâm lucky: one of my co-founders, Heather Miller, is a professor at CMU and a programming languages expert. I can call her up, share my theories, and ask: âHeather, tell me this has already been dealt with. Who should I be talking to and what shoud I read?â This time, she said, âOf course it has, Drew,â and introduced me to her office neighbor, Professor Claire Le Goues. Claire then who walked me through the relevant software engineering history, which Iâm going to share today because it is incredibly relevant to our current situation.
In 1963, Margaret Hamilton was writing and managing software effort for NASAâs Apollo missions. She coined the term âsoftware engineeringâ because, running this giant, complicated project that couldnât have errors in it, she realized: this is engineering. Itâs systems design, we have to worry about errors, we have to worry about unexpected inputs like astronauts pushing the wrong button.
And also: we now have enough code that no one person can hold it in their head. Which is a problem, because then you canât reason about it effectively. And it gets even worse when a team is working on it.
By the way: this is her code. This is what she was managing. This is her VS Code.
And this is her Git.
Iâm a dad, which means dad jokes come naturally. So Iâm going to retroactively coin Hamiltonâs Law: when you canât see over your code, you canât oversee your code.
(Sorry.)
After Hamilton dealt with this problem, others realized it was a problem too.
NATO held a conference in Berlin and identified the âSoftware Crisisâ: computer hardware now allowed programs so complex they couldnât be managed adequately. A single engineer couldnât hold all the code in their head. If they were going to continue delivering on what software could promise, they needed process.
Dijkstra popularized this in his 1972 Turing Award lecture. He said:
As long as there were no machines, programming was no problem at all. When we had a few weak computers, programming became a mild problem. And now we have gigantic computers, programming has become an equally gigantic problem.
He said this in 1972. Maybe later, walk around the museum weâre in and look at what he was working with then. Then consider what weâre working with today.
So after the Software Crisis emerged, we wandered through the desert of processes, searching for one to borrow. We looked at manufacturing engineering. In 1975, Brooks published The Mythical Man-Month. And finally, Waterfall was adopted as a DoD standard. We learned how to engineer complex software. Progress.
But these things move in cycles. In 2001, we published the Agile Manifesto. Zuckerberg said itâs time to move fast and break things. We embraced Agile, and Agile was finally realized by the cloud and GitHub â which enabled continuous CI/CD and let us offload enough of the error-checking that we didnât break things too often, even when moving fast.
Which brings us to today.
I added this slide right at the last minute, because I logged into Twitter to check something and saw todayâs trending news: âAI Coding Boosts Output But Overwhelms Human Reviews.â And itâs paired with that last headline: âOpenAI Codex Leaks Hint at GPT-5.4 Amid Speedy Updates.â So not only is it overwhelming us, itâs accelerating.
So what do we learn from this history rabbit hole?
Being overwhelmed by the volume of code isnât a new problem. Itâs what birthed software engineering.
The initial Software Crisis was our inability to manage complex codebases new computers allowed. Our current Software Crisis is our inability to manage complex codebases new models allow.
Our problem used to be that we couldnât hold an entire codebase in our head. Now we canât even read our entire codebase.
Agentic engineering enables waterfall volume at the cadence of agile. And even that undersells it: itâs waterfall times ~two at the cadence of agile times ~seven.
We keep oscillating, historically, between unhindered velocity and managed process. We could use some process right about now. Perhaps AI can helpâŚ
Iâm not the only one asking this question.
For the last couple of quarters, people have been trying to figure out how to deal with this onslaught of code. The most dramatic example is Gas Town â youâre all familiar with it â an infrastructure for managing a coding process that grew beyond one personâs ability to manage.
But Gas Town just moves the problem. It doesnât solve it. Steve Yegge even admits this in the Gas Town blog post:
Gas Town is complicated. Not because I wanted it to be, but because I had to keep adding components until it was a self-sustaining machine. And the parts that it now has, well, they look a lot like Kubernetes mated with Temporal and they had a very ugly baby together.
If the process is complex, weâre just moving the problem.
So letâs go back to what we defined spec-driven development to be. This idea that itâs an equation: bring specs, maybe add some tests, add an agent, get code out.
I got this wrong. This is the wrong way to think about it. Because this isnât a one-way equation. Itâs a feedback loop. The act of writing code improves the spec, and it improves the tests. Just like software doesnât really work until it meets the real world, a spec doesnât really work until itâs implemented.
So instead of an equation, I propose a triangle. The spec defines what tests need to be written, and what code needs to be written. Tests validate the code. Thatâs essentially what we had before, just in a different shape.
But the act of implementing code generates new decisions. Those decisions inform the spec. And when the spec updates, new tests need to be written. And sometimes itâs not new decisions â itâs just dependencies or subtle choices. New code surfaces new behaviors that need to be tested.
I call this: the Spec-Driven Development Triangle.
As each node moves forward, our job â and our toolingâs job â is to keep those nodes in sync. Thatâs the job. If we improve the code, we must improve the spec.
But keeping the nodes in sync is hard.
Writing tests is hard. Even before agents, we couldnât write tests. We donât like writing tests and weâd prefer not to.
Writing specs is hard. They can never be exhaustive, leave room for interpretation, and are written before the software meets the real world. The spec gets written, it gets implemented, it gets released. Is the spec updated? No.
Specs are written at a different cadence than code, in a different medium. If only we had something that could read natural language.
Updating specs and tests feels like overhead, especially when youâre moving fast. And the entire point of using agents is to move fast. Any system we design has to respect that.
Implementation is messy, and often humans and LLMs take shortcuts. Humans say âIâm not going to implement that right nowâ or âIâll come back and fix this.â LLMs certainly do this.
And so regular reconciliation of tests, spec, and code is not part of the process.
But thankfully, there are signals we can work with.
Code changes are tracked by Git, and we can compare them against the spec to find gaps.
Test coverage tools tell us what code is tested â but not whether the tests reflect the spec. Itâs not just about covering the code. The tests have to cover the spec.
Updates to the spec â if a product manager logs in and changes something â are also tracked by Git. Is the rest of the system changing with it?
Bug reports and hotfixes that go straight into code or tests need to be captured and rolled into the spec.
And most importantly: implementing the code with an agent generates decisions â from both the humans and the agent. Those decisions exist in the traces. We can look at the traces from our coding agents and find where decisions were made. Thatâs the signal we need to keep everything in sync.
So we have tangible things we can analyze. And a goal to aim towardsâŚ
One of the the nice thing about having a thought experiment during the era of great coding agents is that you can just try building it. And as you implement it, you improve it.
This is my tool. I call it Plumb, after a plumb bob, because it keeps things true. A plumb bob hangs from a line and helps a carpenter keep things straight. Even better, they used to be held on tripods, which echoes the triangle.
You can install it right now: pip install plumb-dev or uv add plumb.
Itâs not perfect. Itâs a proof of concept. A thought experiment as code. But Iâve been using it, and itâs pretty great.
Hereâs how Plumb works.
Plumb is a command line tool. Every time youâre working with an agent and you run git commit, it identifies decisions made by evaluating the code diff from the last commit and by reading the agent traces (all the conversations since that last commit). It extracts the decisions, dedupes them, and presents them to you: here are all the decisions you made, do you agree?
Once youâve approved, it updates the spec to reflect those decisions. It runs sync and reports on coverage gaps between the spec and the tests, and the spec-to-code coverage. Is the code actually reflecting what the spec defines?
As it does this, it generates files that become artifacts you can track. My favorite is a big JSONL file of decisions.
Hereâs one example: âShould spec updates be batched across all decisions, or run individually for each decision?â My decision â batch them. It says it was made by the user, not the LLM. I have blame. And you can see how we can enrich this over time: tie it to code, to branch, to whether it was informed by the conversation, when it was approved, when it was synced. This is not just the code changes. Itâs the intent.
To set up Plumb in your project: install it, go to your project directory, run plumb init. Itâll ask you to specify your spec markdown file or folder and show it where your tests are. It creates a .plumbignore to tell it when to skip decision generation â changing the README, for example, doesnât need to generate decisions. It creates a .plumb folder to store state and config. Very similar to .git.
Most importantly: it adds hooks to Git. When you run git commit, it extracts the decisions. If there are decisions to review, the commit fails. It exits and tells you to review your decisions and approve, reject, or edit them. Thatâs what makes this work anywhere: command line, CI pipeline, inside your coding environment. It just works. And thatâs a hard requirement.
The other thing Plumb generates is a breakdown of your spec into individual requirements â the atomic statements that make up what your spec defines. Ambiguous or not, what source file it came from, eventually linked directly to the code. Right now I use a commenting format to link tests back to the requirement theyâre testing, so coverage mapping can show which requirements have tests and how many.
Our aim is link spec to requirements, requirements to code, requirements to tests, decisions to requirements. Weâre building a new object graph extending off the code diffs. And eventually â edit the spec, the tests, or the code, pick your poison, and everything else gets brought along.
Now, as you design this, the interesting design choices start to emerge.
Canât this just be a skill? There are already code review skills, superpowers, things like that. Why not just use those?
I donât think it can be a skill. Whatever tool we end up using for tracking decisions and intent, it cannot live only inside the agent. It needs to run outside. It needs to handle small commits, triggers, anythingâŚeven if you never touch the agent.
A skill is a suggestion. A tool needs to be a checkpoint. That commit-fail mode is essential. Otherwise it gets ignored. Weâve all had this happen with Claude Code.
And the system needs to be canonical. It canât be optional. Agents wander. Validation needs to be more deterministic. When we can use code, we will. This is a validation and verification step. Fuzzy LLM calls are a last resort.
When we do use LLMs â parsing the spec, extracting decisions â we use DSPy. It lets us structure LLM calls with tight inputs and outputs. It lets us optimize, test, and choose which models to route to. Speed matters enormously here. For decision deduplication, Iâm routing to GPT, because itâs faster than anything Anthropic offers for that task. And the whole thing has to be simple enough for the developer to hold in their head.
Of course, there are real limitations.
Plumb only supports pytest. I want it to support any test framework and conformance tests, such as language-agnostic tests like whenwords used.
Decisions might interrupt your flow on long-running tasks. If I make a quick fix and generate five decisions, I have to sit through a review. That needs to be tunable. Maybe you donât want it to bother you for lightweight decisions, only surface things that are vague or contradict previous decisions. I suspect this is something that will be dictated by the type of project youâre working on.
Deduplication isnât perfect. Decision identification is fuzzy and will likely need to be project-specific.
Code reversals on decision rejection arenât working yet. Iâd like it so that when you reject a decision the LLM made, it goes back and undoes it. The reason itâs not implemented is that the flow needs to be right: if you reject from the command line, nothing automatic should happen. If you reject from inside the agent, the agent should act on it.
It needs better tools for managing the spec. Mine has grown long and probably should be sharded into sections. Thankfully, this is something an LLM can and should do. Though, we have to be careful when doing it. Perhaps we can perform dry runs, regenerate requirements from the shards, then confirm they match the original specâŚ
Plumb should be tunable for âjust enoughâ structure. Can I run with --dangerously-approve-all-decisions? Sometimes I want to.
And itâs untested on large projects. Hell, itâs untested in general.
But hereâs the fun part: Iâve been testing this by using it to manage the project itself. Using Plumb to build Plumb. And itâs been genuinely useful.
Claude can refer to the spec for implementation understanding without searching the entire codebase. The decision log has proven valuable for answering âwhy does this code exist?â â I can ask the agent, âis there a decision we made that explains why this is implemented this way?â And it can find it.
Itâs code review, but code review where we capture intent. When I hit commit in Claude Code, I get a list of decisions and I step through them. Sometimes I hit one I donât like and I stop right there. I reject it, go back, redo it. I like that better than pure code review.
It actually spots and controls weird silent LLM behavior. Weâve all let an agent run while we answer email and come back to something insane. Now I get a decision and I can say âdonât do that, letâs roll that back.â
And hacks get documented. Iâve taken shortcuts in this app. Now I know they exist. I can search back for all the shortcuts and then go fix them. The decision log becomes an artifact â not just of code changes, but of intent.
So letâs take this question further. Say Plumb exists and does exactly what I want. How could GitHub be better with this kind of information?
Right now, the main way we interact with code is with Markdown and chat. And GitHub has not changed anything about how we interact with Markdown and text on their site. Could my Markdown diffs have decisions linked to them, so I can see how intent manifests in the code?
I think any version of GitHub that takes the agentic era seriously needs to do four things:
Spec, tests, and code have to be first-class citizens. Code is already. Tests are close â GitHub Actions gets you there. But Markdown is not. Microsoft is probably leaving a lot of inference revenue on the table by not treating it seriously.
Markdown has to be a first-class citizen. This is the gap.
We need to see the linkages. Users need to follow connections between decisions, requirements, code, tests, and spec. Spec-driven development right now is treated as a one-shot thing: write the spec, hit go, youâre done. Itâs not. Itâs a process. You need to track all of it over time.
Users should be able to ask questions of the system. Not just read it â query it, to get closer to understanding intent. Thatâs how you actually understand a codebase thatâs too large to read.
So here are my takeaways from the journey from whenwords to Plumb.
Code implementation clarifies and communicates intent. I could stop there and walk out of the room. I missed this with whenwords.
The job is to keep specs, code, and tests in sync as they move forward. The system for managing that has to stay simple. If it creates developer mental overhead, it just moves the problem somewhere else.
The act of writing code improves the spec and the tests. Just like software doesnât truly work until it meets the real world, a spec doesnât truly work until itâs implemented.
No-code libraries are toys because they are unproven.
Even if you arenât the one making decisions during implementation, decisions are being made. We should leverage LLMs to extract and structure those decisions.
And finally: weâve been here before. The answer then was process. The answer now is also process. And just as we leverage cloud compute to enable CI/CD for agile, we should leverage LLMs to build something lightweight enough that we can fit in our heads, doesnât slow us down, and helps us make sense of our software.
Again: thank you very much to Professor Claire Le Goues, who helpfully walked me through the history of computer science. The history section of this talk is entirely thanks to her. And she has a book coming out, aimed at a wider audience, later this year. Do check it out.