Spec-Driven Development with AI Agents: a 5-Step Workflow

Coding agents write code in minutes. Someone still has to check it.

Spec-driven development promises that the spec carries the weight. In practice the weight lands on review. Output arrives faster than anyone can read it, and developers spend their days checking results instead of building. The agent also makes decisions nobody asked it to make: how to treat an edge case, which rule wins when two collide. They land in the code without a label, and review rarely finds them because the code looks reasonable.

Tests should help, but the same agent writes them and tends to confirm its own code.

The obvious fix is more discipline: review every pull request, write more tests, write longer prompts. It doesn't hold. Reading everything doesn't scale with an agent's output.

The better question is which code a human has to read at all. A team can leave code to an agent once it has fixed that code beforehand in a contract: scenarios, invariants and measurable non-functional requirements (NFRs), the requirements about how well a system works, such as speed or security. Everything such a contract doesn't capture, humans read.

Leaving code unread sounds reckless. But you already do it every day. You don't read your database engine, your HTTP library or your compiler. You rely on what they promise. What does an agent have to promise before you can do the same?

The idea rests on one of the oldest principles in software engineering: the black box.

A black box is only safe behind a contract

In 1972 David Parnas described how to split a system into modules. Each module hides a design decision, and its interface is "chosen to reveal as little as possible about its inner workings." Callers rely on the interface and never look inside. That is the black box.

From 1986 on, Bertrand Meyer sharpened the interface into a contract and called it Design by Contract. His 1992 article lays out the idea: the conditions a caller must meet, the results the module guarantees in return, and the properties that always hold. In his words, "what is an obligation for one party is usually a benefit for the other."

Agents make the old principle urgent, because code is now cheap and attention is not. A block with a precise contract lets a team spend its attention on the contract and the tests and treat what is inside as replaceable. João Queiros puts it in one line: "Disposable code can be abundant; accepted risk cannot."

A block as a black box. The next block depends only on the contract, so the contract is the one thing humans must get right.

The catch is the contract. A black box with a vague promise is just code nobody reads. Without a contract, three things go wrong, and each has been measured or reported:

  • Silent decisions. Take a user story: a user can correct a booking. It doesn't say what happens when a correction collides with an automatic update that arrives a moment later. Where the spec is silent, the agent decides. We call these silent decisions, and How to Write a PRD for Claude Code and Cursor shows how they get into code.
  • Tests that confirm instead of check. An agent that writes tests for its own code asserts what it just decided. ImpossibleBench builds coding tasks in which the tests deliberately contradict the specification, so any pass is a shortcut. GPT-5 found a way through in 76% of the tasks in one variant and in 54% in another. That is a stress test, not an everyday rate. Simon Willison, writing about StrongDM's agent-built software, names the condition for agent-written tests: they help only if they don't cheat.
  • Code that needs reading anyway. In CodeRabbit's analysis of 470 open-source pull requests, those co-authored by AI averaged 10.83 findings, against 6.45 for human-only ones. It is a vendor study and authorship was inferred from AI signals, so take it as a direction, not a measurement.

So the work moves in front of the code. A team decides what the blocks are, writes down what each block promises, lets one agent write the tests and another one pass them, and then decides per block how much it reads. Five steps:

Five steps from decision to gate. Step 2 is where requirements become a contract. Steps 3 and 4 run in separate agent sessions so that the code never marks its own tests.

Step 1: Decide the blocks and the seams

Humans decide what the building blocks are and where they touch. To make this concrete we use a constructed example for the rest of the guide: accounting software built with CQRS, which separates writes from reads. Names and numbers are invented. It has two blocks and one seam:

  • The write block takes automatic bookings from the product's AI and manual corrections from the UI. One block, two inputs. It keeps the list of all changes in order.
  • The read projection builds what the UI shows from those changes.
  • The seam is how changes travel from the write block to the projection, asynchronously.

The code inside each block is the agent's job. The blocks, the seam and the rules for how they fit are the team's. Matt Pocock describes the same split for modules with a small interface and a deep implementation: the human designs the interface, the AI fills it in, tests at the module's edge keep it honest.

One sheet per block is enough. This is the one for the write block:

Block        Write block
Owns         The ordered list of all changes to a booking
Inputs       Automatic bookings (product AI), manual corrections (UI)
Promises     Every change is kept, in order, and never edited
Seam to      Read projection: changes travel asynchronously

The Promises line is the test of the cut. Our rule of thumb: if you can't state what a block promises in one sentence, the block is cut in the wrong place.

Now the collision from the user story, with the blocks in place. A user corrects the account of booking B-1042 at 10:00:05. An automatic change, made at 10:00:03, is stuck in a queue and arrives afterwards. If the write block files changes in arrival order, the late change overwrites the correction and the user's fix is gone. If it orders them by the time they were made, the correction stands. Both are defensible. Nobody has decided which one the product wants, so an agent picks the simpler one, arrival order, and writes tests that pass.

That decision belongs in the contract.

Step 2: Write the contract before any code exists

A contract has four parts:

  • Scenarios: behavior as examples in Gherkin, the Given/When/Then format.
  • Invariants: statements that always hold. Debit equals credit. A booked entry is never edited, a correction is a new entry.
  • Budgets: NFRs with numbers.
  • Seam clauses: what must hold where blocks meet.

The scenario for the collision names the clock:

Scenario: A correction is placed in chronological order, not in arrival order
  Given booking B-1042 has an automatic change made at 10:00:00
  And the user corrects the account of B-1042 at 10:00:05
  When another automatic change for B-1042, made at 10:00:03, arrives after the correction
  Then the change list shows 10:00:00, 10:00:03 and 10:00:05 in this order
  And the current account of B-1042 is the one from the correction

The time a change was made decides. That is the silent decision from Step 1, now one sentence the team can argue about before any code exists.

Budgets need more care than scenarios. The first draft of ours read: "Processing must not slow down the UI." What does slow mean? 300 milliseconds? Three seconds? Under what load? An agent reads that sentence as permission. A budget has a number, a load and a way to measure, such as a percentile. ISO/IEC 25010 files this under performance efficiency, which tells you where it belongs and nothing about how fast. Someone has to name the number:

NFR-004  While 5,000 automatic bookings per minute are processed,
         the booking list answers in under 300 ms (95th percentile).
NFR-005  A change appears in the booking list within 2 seconds after the
         write block accepts it (99th percentile).
Seam     Every change carries an ID. Applying the same change twice
         leaves the change list unchanged.

The 95th percentile means 95 of 100 requests are at least that fast. A caveat: we know of no study that shows how reliably agents write load tests. Treat budgets as a recommendation, and read the load tests like any other test. Write the contract from the consumer's side. The projection is the consumer: it states what it needs from the write block, in the spirit of Ian Robinson's consumer-driven contracts. Don't let the agent derive the contract from its own code. The Pact documentation makes the point for contracts generated from the provider's own API description: that is like "marking your own exam".

Copy this template for your own blocks:

Block        <name>
Scenarios    <Given/When/Then, one per rule and per failure>
Invariants   <what always holds>
Budgets      <NFR id: number, load, percentile>
Seam         <what must hold where blocks meet>
Open         <what is still undecided>

The last line is the one people skip. Whatever sits under Open is a decision the agent will make for you.

Steps 3 and 4: One agent writes the tests, another one has to pass them

Agent A gets the contract and nothing else. It writes tests for the block's edge. It never sees an implementation, so it can't copy one's decisions into its assertions. A prompt for it can be this short:

Read the contract for this block. Write tests at the block's edge for every scenario and every budget. Don't read or write implementation code. If anything is ambiguous, stop and ask.

Then humans read the tests. That is more reading, but a different kind. Scenarios are written in the language of the contract, so a reviewer doesn't need to follow control flow. Two questions are enough. Does every decision in the contract have a test? Does any test assert something the contract doesn't say? The second question finds silent decisions that leaked in.

Add a gate on the tests themselves. Mutation testing changes the code on purpose and checks whether the tests notice. The share of changes they catch, the mutation score, tells you how much a green run is worth. Property-based tests and fault injection belong in the same gate. Each is a field of its own, and we don't teach them here.

Agent B starts in a new session. It gets the contract and the tests, and it implements until the tests pass:

Implement this block against the contract. The tests in the protected folder are read-only. Make them pass. If a test contradicts the contract, stop and ask.

It can't change the tests. They sit in a protected path that only humans can edit, CI runs them from there, and a changed test file fails the build. How you enforce this depends on your repository setup. The principle is that the agent building the code has no write access to the exam.

The ImpossibleBench authors tested versions of this. Hiding the tests from the agent brought cheating close to zero but also lowered performance on legitimate tasks. Read-only access restored legitimate performance and blocked attempts to modify tests. It didn't stop every other trick. That is why the process doesn't end here, and why Step 5 exists. StrongDM goes further and keeps scenarios outside the codebase where the agent never sees them. It works and it costs effort. Treat a hidden set as the next step for your money path, not as the starting point.

Tests don't prove that code is right. They give evidence that it matches the contract, and the contract is only as good as the decisions in it. Two sessions of the same model also share blind spots, so a separate session is not an independent judge. That is why humans read the tests and why the mutation score matters.

Four mistakes that undo the setup

  • One session writes tests and code. The agent grades its own work.
  • Tests are generated from the finished code. They record what the code does, not what it should do.
  • NFRs have no numbers. "Fast" gives the agent nothing to fail against.
  • Nobody reads the tests. The decisions stay hidden one level down.

Step 5: Decide per block how much you read

The team decides, block by block, how much human attention a block earns. We use three levels:

  • Level 1: a human reads the code and understands it.
  • Level 2: humans read the contract and the tests. The code is a black box.
  • Level 3: the gate decides. A human looks when it fails.

Four questions place a block:

  • Oracle. Can we state the right result completely, as examples and invariants, without looking at the code?
  • Interplay. Does the behavior only appear together with other blocks?
  • Damage. What does a mistake cost, how quickly do we notice it, and can we reverse it? The "blast radius" idea from understandingdata.com is the model here.
  • Invisible remainder. Which properties does no gate see? Readability, maintainability, some security patterns. CodeRabbit's study flagged readability findings at more than three times the rate in AI-co-authored pull requests, and no scenario will catch a confusing function. Name an owner for this remainder or accept the debt knowingly.
The rule: a block moves up a level as soon as one of the four answers is bad.

Gates that make level 3 possible are what architects call fitness functions: any mechanism that gives an objective integrity assessment. A dependency rule is one. In our reading, a performance budget in CI is another. At the block's edge, fast tests carry the load, in the spirit of the service layer in Fowler's test pyramid.

The worksheet for the accounting service looks like this:

BlockOracleInterplayDamageInvisible remainderLevel
Write blockStrong: debit equals credit, idempotent, entries never editedLowHigh, but visible and reversibleReadability2
SeamWeak: order and replay depend on both blocksHighHighWiring1
Read projectionStrong: can be rebuilt from the write sideLowLowReadability3

The write block is the money path, so damage is high. But the oracle is strong, errors surface quickly, and a reversing entry fixes them. The seam is where nothing at one block's edge can see the problem. The projection can be rebuilt, a wrong list is cheap to fix, and the lag budget in NFR-005 is a gate.

Our classification of the constructed service. Your team will rate its own blocks differently, and that is the point of asking the four questions.

Where the black box ends: read the hotspots

A hotspot is a place where a mistake can slip past every test at a block's edge. We see two kinds.

The first kind is a decision that no single story contains and that an agent would make silently. The clock is one. Once the team has fixed it in the contract, a scenario at the block's edge checks it, and the block can sit at level 2.

The second kind is a property that tests at one block's edge can't see or reproduce, because it only appears when blocks work together. Those places stay at level 1.

They hide in places like these, one sentence each:

  • Event order and duplicates: with at-least-once delivery the same change can arrive twice and out of order.
  • Consistency window: how far the read side may lag behind the write side.
  • Retries and failure propagation: what happens when step two fails after step one succeeded.
  • Transaction boundaries: which changes must succeed or fail together.
  • Data ownership and schema changes: who may change a table others read. Jackie Luo of Sigil, quoted in the Pragmatic Engineer, treats the schema as the part worth the most review attention because it is the hardest to change.
  • Security boundaries: one tenant never sees another tenant's data.
  • Backpressure: what happens when the producer is faster than the consumer.
  • Replay and rebuild: whether the read side can be rebuilt from the write side, and how long that takes.

In our service the clock was the first kind. The seam is the second: whether the projection survives duplicates and a replay depends on how the two blocks are wired.

Each of these fills books, and we leave them there. The list earns its place by prompting one question for every seam in your system: which kind is this? Settling the answers is architecture work: someone has to decide which clock counts, how long the lag may be and who owns the schema. That work comes before the contract, and the contract is where its result gets written down.

Not everyone agrees that code can ever stay unread. Osmani, for one, argues for reviewing all AI-generated code, and the Pragmatic Engineer found far more discussion of skipping review than teams actually doing it. We don't say "never read". We say where: the levels and hotspots above are an answer to where reading pays off.

Willison asked the question that StrongDM's rule raises: how can a ban on human review be sensible "when we all know how prone LLMs are to making inhuman mistakes"? He called proving that code works without reading every line "the most consequential question in software development right now". For a team of two to ten, the hotspot list is a workable answer: leave the rest to the gates and keep reading where blocks meet.

Where the contract comes from: a refinement interview

Everything so far assumes the contract exists. It doesn't appear by itself. Scenarios, invariants and NFR numbers are decisions, and decisions come out of a conversation: someone has to ask which clock counts, and someone has to answer.

That conversation belongs in refinement, story by story. Speclr runs it as an interview. For the booking service, the turn that settles the clock and the budgets looks like this:

Speclr: A user corrects the account of a booking at 10:00:05. An automatic change made at 10:00:03 arrives afterwards. Which one wins: the one that arrived last, or the one made last?
You: The one made last. The user's correction must not disappear.
Speclr: While 5,000 automatic bookings a minute are processed, how quickly must the booking list answer?
You: Under 300 milliseconds, for 95 of 100 requests.

Then the perspectives read the result. Speclr's review agents look at the spec from their own viewpoint: consistency, risk, QA, security and architecture. In our example, QA asks what happens when two changes carry the same time. Architecture asks where NFR-005 is measured from: the moment the change was made, or the moment it reached the write block. The answer is the second one, because a late change can sit in a queue for longer than the budget and the projection can't fix that. Each finding is a gap the agent would otherwise have filled itself.

The result lands in your repository as plain files, in the places agents already look:

AGENTS.md / CLAUDE.md
docs/
  domain/rules.md
features/
  bookings/
    us-021-correction-order.md
    us-021-correction-order.feature

The spec file carries the rules and the budgets. The feature file holds the Gherkin scenarios, once. AGENTS.md / CLAUDE.md holds a short block that tells every agent how to treat them:

<!-- Generated by Speclr · edit in Speclr · exported 2026-10-14 -->
# US-021 — Corrections keep their chronological place

## Business rules
- BR-014  Changes are ordered by the time they were made, not by arrival.
- BR-015  Two changes with the same time are ordered by their ID.
- BR-016  A booked entry is never edited. A correction is a new entry.

## Non-functional requirements
- NFR-004  While 5,000 automatic bookings per minute are processed,
           the booking list answers in under 300 ms (95th percentile).
- NFR-005  A change appears in the booking list within 2 seconds
           (99th percentile), measured from the moment the write
           block accepts it.

## Scenarios
See ./us-021-correction-order.feature

## Open questions
- None
<!-- speclr:begin — managed by Speclr, edit in Speclr -->
## Spec
- Before implementing a story, read its spec and its .feature file.
  Scenarios are the acceptance tests.
- If a scenario contradicts the code or another story, stop and ask.
  Don't change scenarios silently.

## Invariants
- A booked entry is never edited. A correction is a new entry.
- Changes are ordered by the time they were made.
<!-- speclr:end -->

Agent A reads these files to write its tests. Agent B gets the same files plus the tests. The export runs in one direction, so there is one place to edit and no drift between the spec and its copy. Everything after that, the separate sessions, the protected test path, the mutation gate and the reading levels, your team does with its own tooling. Speclr's part is the beginning: a contract that exists before the code does.

The clock in our example is a single sentence: the time a change was made decides the order. Agents don't remove the work of deciding, they remove the excuse to decide late. The teams that get faster are the ones that move their decisions in front of the code, where a decision costs one sentence in a contract and not a rebuild after the review.

Do you have to review all AI-generated code?

Not all of it with the same depth. Read the code at level 1 blocks and at the seams, read the contract and the tests for level 2, and let a gate watch level 3. The four questions in Step 5 tell you which is which.

How do you do an AI code review without reading every line?

By reading less of it and reading the right parts. Move the decisions into a contract, let a separate agent write the tests, and put your reading time into the tests and the places where blocks meet.

Can AI write tests for its own code?

It can, but not in the same session, and not against its own code. An agent that writes both marks its own exam. Let one session write tests from the contract and another implement against them.

What are non-functional requirements (NFRs), with examples?

They are the requirements about how well the software works: speed, security, reliability, and so on. For a coding agent they have to be measurable. A budget has a number, a load and a way to measure, such as "under 300 ms for 95 of 100 requests while 5,000 bookings a minute are processed".

How do I stop an AI agent from changing the tests?

Put the tests in a protected path that only humans can edit, let CI run them from there, and fail the build when a test file changes. Read-only tests block the easiest trick but not every trick, which is why humans still read the tests and the seams.

What is spec-driven development?

Building from a written spec instead of from a prompt. The spec decides what the agent builds. This guide covers the part that decides how much of the result you can trust without reading it: the contract.

What is Gherkin syntax, and do I have to use it?

Gherkin is the Given/When/Then format for writing scenarios, as in the booking example above. You don't have to use it, because any format with concrete examples works. It is a good default because each scenario is one example the agent can turn into a test, and it reads like plain English.

Does spec-driven development work with Claude Code?

Yes. Claude Code reads CLAUDE.md and the files in your repository, so the spec, the feature files and the instructions block can sit where it already looks. The steps are the same for any coding agent: the contract comes first, one session writes the tests, another one implements.

What is AGENTS.md?

A markdown file in the repository that tells coding agents how to work in that project: conventions, commands, rules. Claude Code reads CLAUDE.md for the same purpose. In this guide the file carries a short block that points the agent to the spec and the scenarios.

What is mutation testing?

A technique that changes the code on purpose, for example by flipping a condition, and checks whether the tests fail. The share of changes the tests catch is the mutation score. A low score means green tests prove little. Tools exist for most languages, for example PIT for Java.

Speclr
Further Reading

Your next contract starts as an interview

Run your next refinement as an interview in Speclr and export the result as a spec file, feature files and AGENTS.md. Speclr is free for 14 days.

Get started

Tags

spec-driven-developmentai-generated-codecode-reviewnon-functional-requirementsgherkinai-coding-agents