Skip to content

Spec-driven development: writing contracts for AI agents

Strahinja Polovina · Engineering10 min read

Something quietly inverted in software engineering over the last two years. For most of the industry’s history, the code was the artifact and the spec was the disposable scaffolding around it - a document written once, skimmed twice, and abandoned by the third sprint. Now AI agents write a growing share of the code, and the economics have flipped. Code became cheap to produce and cheap to regenerate. What’s scarce is a precise statement of what the code must do. The spec is no longer scaffolding. It is the engineering artifact.

Teams that treat it that way get compounding leverage from agents: they hand over well-defined work, verify the result mechanically, and merge with confidence. Teams that don’t get a new kind of technical debt - plausible-looking code nobody asked for, generated faster than anyone can review it. The difference between the two isn’t the model or the tooling. It’s whether the spec is a contract.

A prompt is not a spec

The most common failure is treating the chat box as the spec. A prompt is a conversation opener: it describes intent in prose, leaves the edge cases to the reader’s goodwill, and vanishes into a scrollback buffer the moment it’s sent. Nobody can review it, diff it, or point to it three weeks later when the behaviour is disputed. An agent given a prompt does what a contractor given a hallway conversation does: it fills every gap with its own assumptions, confidently.

A spec is a different object. It lives in the repository next to the code it governs. It is versioned, so you can see what was agreed and when it changed. It is reviewable, so a colleague can object to the requirement rather than the implementation. And it is checkable - the crucial property - meaning that for any piece of delivered work there is a mechanical way to ask: does this satisfy the spec, yes or no? The moment that question requires a judgment call, you have prose, not a contract.

What makes a spec a contract

Four ingredients separate a contract from a wish. Acceptance criteria come first: observable, binary statements about behaviour. “Fast” is a wish; “p95 under 200ms at 50 requests per second” is a criterion. “Handles bad input” is a wish; “rejects malformed payloads with a 422 and a machine-readable error code” is a criterion. If you can’t decide pass or fail from outside the implementation, rewrite it until you can.

Worked examples come second, and they do more work than any other section. Concrete input-output pairs - including the ugly ones: the empty list, the duplicate submission, the timezone edge, the user who pastes an emoji into the amount field. Examples are where hidden assumptions surface. They are also, not coincidentally, test cases waiting to be transcribed.

Invariants come third: properties that must hold no matter what path the implementation takes. Balances never go negative. Every state transition is logged. Deletes are soft. No call to the payment provider without an idempotency key. Invariants are the guardrails that survive refactoring - which matters doubly when the refactoring is done by an agent that never attended the meeting where the rule was explained.

Non-goals come last and are the most neglected. An agent’s failure mode is rarely doing too little; it’s doing too much - inventing configuration options, adding caching nobody requested, “improving” adjacent code. A short list of what this change must not touch and what problems it must not solve is the cheapest scope control you will ever write.

An agent fills every gap in the spec with its own assumptions - confidently. The contract exists so there are no gaps worth filling.

Contracts need enforcement

A contract nobody enforces is decoration. The enforcement layer is what makes spec-driven development an engineering practice rather than a documentation habit, and it has three parts. First, tests as the executable form of the contract. Acceptance criteria and worked examples get transcribed into a test suite before the agent writes implementation code. The suite is the spec compiled into something a machine can check; the agent’s job is to make it pass without weakening it. Guard the tests themselves in review - an agent that edits an assertion to make it pass has renegotiated the contract unilaterally.

Second, typed boundaries. Schemas, types and API contracts pin down every interface the agent’s code touches. A strict schema at the boundary turns a whole class of plausible-but-wrong output into compile errors and validation failures - feedback the agent gets in seconds, in the loop, rather than feedback a reviewer delivers days later. The stricter the boundary, the less room there is for confident garbage to leak through it.

Third, review gates that compare the diff to the contract. Human review doesn’t disappear; it changes target. Reviewing agent output line by line at agent speed is a losing game - there is simply more code than attention. Reviewing the diff against the contract is tractable: does the change satisfy each acceptance criterion, respect each invariant, stay inside the non-goals? Anything in the diff the contract doesn’t explain is the first question of the review, not a bonus.

The three ways it goes wrong

Vague spec, confident garbage. The team writes three bullet points, the agent produces eight hundred lines, and the gap between intent and behaviour is discovered in production. The tell is review conversations that start with “that’s not what I meant” - a sentence that indicts the spec, not the agent. The fix is boring: more examples, sharper criteria, written before generation rather than after.

Over-specification, no leverage. The opposite ditch. If the spec dictates the implementation - file by file, function by function - you have written the code twice, once in English and once in the language it runs in, and the agent is reduced to a typist. Specify behaviour at the boundary and the invariants inside it; leave the implementation to the party writing it. If specifying a task takes longer than doing it, that task didn’t need an agent.

Spec drift. The quietest failure. The contract said one thing, production evolved into another, and after three months of small renegotiations-in-passing the spec describes a system that no longer exists. Drift is inevitable; unrecorded drift is a choice. The rule that prevents it costs one sentence: behaviour changes land as spec changes first, in the same pull request, or they don’t land.

The workflow, end to end

What this looks like in practice is a short loop. Write the contract - criteria, examples, invariants, non-goals - and have it reviewed by the people who will live with the behaviour; this is where disagreement is cheap. Transcribe the checkable parts into tests that fail. Hand the contract to the agent and let it draft against the failing suite, iterating until the mechanical checks - tests, types, schemas, linters - pass without edits to the checks themselves. Then review the diff against the contract, not against your patience: criteria met, invariants held, nothing outside the non-goals. Merge records the contract and its fulfilment in one atomic unit, and the next task starts from an honest baseline.

The loop has a second-order benefit that surprises teams: contracts make agent work parallelisable. Two agents on one vague brief collide; five agents on five contracts with typed boundaries between them merge cleanly, because each contract states what its piece must do and where it ends. The same property that makes a spec checkable makes it composable - which is how a small senior team supervises an amount of concurrent implementation that would have required a department five years ago.

One more mechanical rule keeps the loop honest: the checks run in CI, not on the agent’s say-so. An agent reporting “all tests pass” is a claim; a green pipeline on an unmodified test suite is a fact. The contract, its tests and the pipeline together form a neutral arbiter that doesn’t care whether the code was written by a person having a bad day or a model having a confident one - and that indifference is exactly what makes the output trustworthy.

Teams adopting this discover the bottleneck has moved. The scarce skill is no longer producing code - it’s deciding precisely what the code should do and expressing it checkably. That was always the hard part of software; agents didn’t create the problem, they removed the place it used to hide.

When it pays off - and when it’s overhead

Spec-driven development is not free, and pretending otherwise kills adoption. The contract pays for itself when the work is consequential and the cost of being wrong is real: systems that move money, touch customer data, sit on team boundaries, or will be maintained by people - and agents - who weren’t present when the decisions were made. It pays off fastest exactly where agent output is hardest to review by eye.

It is overhead for spikes, prototypes and one-off scripts - code whose entire purpose is to be thrown away once it has answered a question. Write a sentence of intent, let the agent run, keep the learning and delete the code. The discipline is knowing which mode you are in, and being honest the moment a prototype starts quietly becoming a system. That moment - not the rewrite six months later - is when the contract gets written.

The industry spent two decades learning that tests are not bureaucracy but the thing that lets you move fast without breaking what matters. Specs are now making the same journey, for the same reason. When the marginal cost of writing code approaches zero, the value concentrates in the statement of what the code must do - and the teams that write that statement as a contract are the ones for whom the agents actually compound.

Putting agents to work on a system that matters?

We build the contracts, the enforcement and the software. The diagnosis call is free.