Spec-Driven Development With AI Agents: Writing a Spec an Agent Can Actually Execute
How to structure a spec so a coding agent can run it end to end without babysitting: ground truth files, an interface contract, one acceptance command, and explicit out-of-bounds rules.
Most “specs” handed to a coding agent are wishes. “Add rate limiting to the API” is a wish. The agent will produce something — a middleware file, probably in-memory, probably with a test that asserts the middleware exists — and you will spend longer reviewing it than you would have spent writing it yourself.
A spec an agent can execute end to end is a different artifact. It names the files that already exist, states one acceptance check, and closes every decision the agent would otherwise make on your behalf. We rewrote our own workflow around this on a TypeScript codebase (Astro front end, Supabase backend) after too many twenty-minute unattended runs came back with plausible code and no working feature. What follows is the structure that survived.
The five parts of an executable spec
1. Goal and non-goal, one line each. The goal is the behavior change, stated from the outside: “A client that sends more than 60 requests per minute to /api/track gets a 429 with a Retry-After header.” The non-goal is the adjacent work you do not want touched: “Do not add rate limiting to any other route. Do not change the response shape of successful requests.” Agents expand scope when the boundary is implicit; the non-goal line is cheap and it holds.
2. Ground truth: the files that already exist. List the three to six files the agent should read before writing anything, and say what each one is for. This is the single highest-leverage part of the spec. Without it, the agent greps, finds a pattern from a file you abandoned six months ago, and copies it. With it, you get code that looks like the rest of your codebase because it was told which code to look like.
3. The interface contract. Function signatures, table columns, env var names, error shapes — written out, not described. If the agent has to invent a name, it will invent a different one in the implementation than in the test, then spend three tool calls reconciling them. Write rateLimit(key: string, limit: number, windowMs: number): Promise<{ allowed: boolean; retryAfter: number }> and that whole class of thrash disappears.
4. Exactly one acceptance command. Not “make sure tests pass.” A literal string the agent can paste into a shell: bun test src/lib/rate-limit.test.ts. The agent needs a signal it can produce itself, on demand, without asking you. If the work can’t be reduced to one command, the spec is too big — split it.
5. Out of bounds. Files or directories that must not change: migrations already applied, generated files, anything with a hand-tuned config. Agents treat a failing build as a puzzle, and deleting your tsconfig strictness flag is a valid solution to that puzzle.
Four failure modes and the spec line that fixes each
These are the patterns we saw repeatedly across runs, and the specific sentence that stopped each one.
| Failure mode | What the agent does | Spec line that prevents it |
|---|---|---|
| Invented abstraction | Adds a RateLimiterFactory, a strategy interface, and a config object for one call site | ”Single exported function. No new classes, no config objects, no new directories.” |
| Silent scope creep | Refactors the surrounding module “while it was in there" | "Only these files may change: <list>. Report anything else you believe needs changing; do not change it.” |
| Test theater | Writes a test that asserts the function is defined and returns an object | ”The test must fail if the limit is off by one. Include a case at limit-1, at limit, and at limit+1.” |
| Green-by-deletion | Makes the build pass by loosening a type, skipping a test, or removing an assertion | ”Do not modify existing tests, tsconfig.json, or lint config. If an existing test blocks you, stop and report it.” |
The last one deserves emphasis. An agent optimizing for a green acceptance command has two paths: fix the code, or weaken the check. It will take whichever is shorter. Your spec is the only thing that closes the second path.
Running it: checkpoints, and what to do when it stalls
Hand the spec over as a file in the repo, not as a chat message. A file survives context compaction, gets read again when the agent re-orients mid-run, and — importantly — can be diffed. When a run goes wrong, you want to compare the spec you thought you wrote against the one on disk.
Structure long specs as checkpoints rather than one blob: after each numbered step, state the observable result. “Step 2 done means bun test src/lib/rate-limit.test.ts passes and no other file has changed.” The agent gets intermediate signal, and you get a resume point that isn’t “start over.”
When you review, read the diff, not the transcript. The transcript is the agent’s account of its own work and it is uniformly optimistic. The diff is what shipped. This sounds obvious and it is still the discipline people drop first when a run looks like it went well.
The rule that saved us the most time: if you’ve corrected the agent twice in chat, stop and rewrite the spec. Two corrections means the spec was ambiguous, and a third chat message patches this run while leaving the ambiguity in place for the next one. Rewriting the spec and restarting from a clean context is almost always faster than steering — the run that went sideways is carrying a context full of its own wrong turns.
One practical note on tooling: this workflow works better with agents that read project-level instruction files (AGENTS.md, CLAUDE.md) automatically, because your standing rules — package manager, test runner, forbidden patterns — live there instead of being restated in every spec. The spec then covers only what’s specific to this task, which is how it stays short enough that you actually write one.
opencode
Open-source terminal coding agent. Reads AGENTS.md for project-level rules, runs your acceptance commands directly in the shell, and works with whichever model provider you already pay for — useful when you want spec files and standing instructions living in the repo rather than in a vendor's chat history.
Free and open source; you bring your own model API key or subscription
Affiliate link · We earn a commission at no cost to you.
Spec-driven development isn’t a productivity trick, and it doesn’t make agents smarter. It moves the thinking earlier: the ambiguity you don’t resolve in the spec gets resolved by the agent, at random, in code you then have to read. Writing the spec is the same work either way — you just get to decide whether you do it before or after the diff exists.
FAQ
How long should a spec be?
Isn't this just writing the code twice?
Does this work for exploratory work where you don't know the answer yet?
Tools used in this review
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-05-26
Macchiato Day 2: Live Token Metrics and Parallel AI Terminals Reviewed
Macchiato's day-2 build adds a live token/cost sidebar and keyboard shortcuts for swapping between Claude Code and OpenCode in one terminal. Here's what shipped and what it means.
2026-05-21
Agnt Review: An Open-Source CLI for Running Public and MIT-Licensed AI Agents
Agnt is a free, open-source CLI for running any public or MIT-licensed AI agent from one interface. What it does, how it compares to other agent runners, and whether to install it.
2026-05-21
How to Measure AI Coding Agents Beyond Lines of Code and PR Acceptance Rates
Lines of code and PR acceptance rates look like productivity signals but reward verbosity and rubber-stamping. Here is what engineering managers should track instead when adopting Copilot, Cursor, and Claude Code.
2026-05-21
Amp's Neo CLI: Why AI Coding Agents Still Live in the Terminal
Sourcegraph's Amp is reworking the command line around autonomous AI coding agents. Here's why the terminal remains core infrastructure for agentic development — and what changes when software, not a person, is the operator.
2026-05-18
GitHub Copilot Desktop vs Claude Code vs Codex CLI: Picking Your Agent
GitHub's standalone Copilot desktop app puts it head-to-head with Claude Code and Codex CLI. We compare workflow surface, approval semantics, and model neutrality so you can pick the right one.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.