pickuma.
AI & Dev Tools

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.

6 min read

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 modeWhat the agent doesSpec line that prevents it
Invented abstractionAdds 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 creepRefactors 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 theaterWrites 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-deletionMakes 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

Try opencode

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?
Short enough that you'd write one for a two-hour task. In practice ours land between 30 and 80 lines. If a spec is running past a page, the task usually has more than one acceptance command in it and should be split into sequential specs, each with its own checkpoint.
Isn't this just writing the code twice?
No — a spec names signatures, files, and checks, not logic. The implementation work (edge cases, error handling, wiring, tests) is still the bulk of it. What the spec eliminates is the agent's freedom to choose names, locations, and abstractions differently from the rest of your codebase, which is where most review time goes.
Does this work for exploratory work where you don't know the answer yet?
Not well. Spec-driven runs assume you can state what 'done' looks like before starting. For exploration, run a short interactive session first to find the shape of the solution, then write the spec for the actual implementation and restart with a clean context.

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

See all AI & Dev Tools articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.