# Spec-driven development

Spec-driven development means writing the decisions down before the code: what is being built, what deliberately isn't, and how you'll know it worked — precisely enough that an AI agent can implement it without inventing the missing parts. This page documents the methodology as I actually practise it: the two-document structure, the judgement rules, the verification gates, and the places it still breaks down.

Published: 2026-07-27
Canonical: https://umar.codes/spec-driven-development

## Revisions

- 2026-07-27 — Published; written from specs and decision logs across roughly 25 projects.

---

Spec-driven development means writing the decisions down before the code: what is being built,
what deliberately isn't, and how you'll know it worked — precisely enough that an AI agent can
implement it without inventing the missing parts. This page is the methodology as I actually
practise it: the documents, the judgement rules, and the places it still breaks down.

I made the argument for working this way in [Why I'll never go back to vibe
coding](/vibe-coding), on the NearForm blog. This page is the *how*, and it's a living
document — the revision log at the bottom records how it changes as the practice does.

## The core loop

The loop has four artifacts, and the discipline is that each one exists before the next:

1. **A design spec** — the decisions, locked. What we're building, what we're not, which
   alternatives were rejected and why.
2. **An implementation plan** — the design decomposed into tasks small enough that each one
   states its files, its code, and its expected output.
3. **Execution with gates** — an agent (or I) work the plan phase by phase. Each phase has a
   done check, and nothing moves forward until it passes. One commit per phase, named for its
   outcome.
4. **A verification report** — pass/fail against the spec's own criteria, with evidence.
   Not "it works"; the actual command output.

Alongside the loop run two logs. `DECISIONS.md` records every judgement call made during
execution — what the spec said, what was done instead, why. `BLOCKERS.md` records anything the
spec couldn't anticipate: write the blocker, skip the task, keep going. The run never stops to
wait for a human unless it hits a *gate* — and a good spec has very few of those. The spec that
rebuilt this website had exactly one (the framework choice) across ten phases, and the
execution produced twelve dated decision entries without a single mid-run question beyond it.

The split between spec and plan matters. The spec is small and dense — decisions per square
inch. The plan is large and mechanical; the biggest I've run to completion was 1,484 lines,
four stacked pull requests of three to six tasks each, every task ending in a test. They fail
differently: a bad spec produces the wrong thing correctly, a bad plan produces the right
thing slowly. Keeping them separate means you can tell which one to fix.

## What belongs in a spec

Six things earn their place. Everything else is padding that an agent will either ignore or,
worse, obey.

**Locked decisions, with the rejected alternatives named.** Not "we will use approach A" but
"A over B and C, because B violates seam discipline and C is over-engineering." When an agent
mid-task discovers a reason to prefer B, the spec has already had that argument, and the agent
can see it lost. My design specs carry these as a table; one recent migration locked eight
decisions this way, another twelve.

**Non-goals.** The highest-leverage section, and the one most specs omit. The spec for this
site ends with five things it deliberately does not do — no per-page markdown twins, no CMS,
no newsletter — each with the reasoning. Agents are eager; non-goals are the fence.

**Verbatim constants.** Strings that must appear byte-identical everywhere: canonical URLs,
entity descriptions, API names. Marked explicitly as *use this verbatim, do not paraphrase*.
Anything not marked verbatim is direction, and direction invites judgement — which is exactly
what you want, as long as the boundary is explicit.

**Pinned facts.** Package versions, environment values, deadlines. A table the implementation
must match, so "it drifted during the run" becomes a checkable claim.

**Done checks, per phase.** Each phase ends with a test the phase must pass — "`pnpm build`
succeeds", "every URL in the discovery doc resolves or 301s", "all ten verification rows
pass." A phase without a done check isn't a phase; it's a hope.

**Escape hatches.** The `DECISIONS.md` / `BLOCKERS.md` contract from the loop above, written
into the spec itself: exercise judgement and record it; when blocked, log and continue. This
is what lets a run survive contact with reality without either stopping constantly or
improvising silently.

## What stays out

Everything the implementer — human or agent — should own. File layout, function names,
internal structure, library micro-choices. Over-specifying these produces worse results than
under-specifying them, for a reason that took me a while to accept: an exhaustive prescription
is always partly wrong, and an agent will follow the wrong parts as faithfully as the right
ones. A judgement rule plus a decision log beats a prescription.

The clearest example from this site's rebuild: the spec said "Astro 5 with the content layer."
By execution time, current stable was Astro 7. Because the spec's contract was *record
deviations in `DECISIONS.md`* rather than *follow versions literally*, the run took Astro 7,
wrote down why, and kept moving. A stricter spec would have either halted or — worse —
dutifully installed an outdated major.

The other thing that stays out is ceremony. Artifacts that no one reads back are cost without
control: I've dropped QA gate files in favour of inline review notes, and I keep working plans
out of the repo entirely when they're execution scaffolding rather than durable record.

## Verification is a document, not a feeling

The end of a run is a report with a row per claim and evidence per row — actual Lighthouse
scores, actual command output, the actual logged JSON line proving the crawler logger fired.
Two phrases are banned from it: "should work" and "probably passes." If the proof wasn't run
fresh, it isn't proof.

The reason for the paranoia is that the most dangerous test result is a **vacuous green**.
I've been burned by these repeatedly, in ways a spec now has to anticipate: a resume check
that "passed" because it reduced to `includes('')` against an empty environment variable; a
layout assertion suite that passed with every rectangle at zero size because the test runner
never loaded the CSS. Both were green. Neither tested anything. Done checks now come with a
guard against emptiness — assert the count is greater than zero before asserting the
properties of the members.

Verification also catches what the spec itself got wrong. This site's spec permitted a
page-load fade; verification showed it delayed first contentful paint badly enough to zero out
the metric in backgrounded tabs. The fade is gone, and the reason is in the decision log —
which is the system working, not failing.

## Where it breaks down

Honest failure modes, all from my own logs:

**Specs that specify impossible states.** One acceptance criterion — and its integration test
— asserted a state the system could not reach, because the role in question didn't hold the
required permission. Both spec and test agreed with each other and disagreed with reality.
Lesson: acceptance criteria need the same "can this actually occur" review as code.

**Hypotheses dressed as decisions.** A performance spec confidently attributed a layout-shift
problem to a specific banner. Measurement disproved it — lab CLS was already near zero — and
the honest move was reshaping the work into a field-measurement task first. A related epic
died entirely the same week, pull request closed, when the data showed its target metric
wasn't the real problem. Specs encode beliefs; some beliefs are wrong, and the spec has to
lose gracefully.

**Drift.** Implementations move after the spec is approved — every long-lived spec is slowly
becoming fiction. The countermeasure is making drift *detectable* rather than pretending it
won't happen: contract-drift checks in CI (warn-only at first, blocking once trusted), and
staleness thresholds on the specs themselves.

**The overhead is real, and mostly unmeasured.** The industry discourse on spec-driven
tooling runs almost entirely on personal anecdotes about token costs and spec sizes. I got
frustrated enough to build a benchmark harness — eight fixed tasks, deterministic acceptance
suites, and a *null adapter* that establishes the floor any real tool must beat. First
measured result: one popular SDD tool completed a brownfield task at a median cost of $0.57,
132 lines of spec, a 0.75 acceptance pass rate. Spec-driven development trades immediate
feedback for consistent output; that trade should be measured, not vibed.

## Relationship to sdx

Everything above is discipline, and discipline erodes. [sdx](/sdx) — published on npm as
`specdx` — is the toolchain I'm building to make the enforceable parts of this page machine-
checked instead of remembered. It gives specs a formal schema (nine document types, each with
required sections), lints them (thirteen rules, from missing frontmatter to vague language),
packs the relevant ones into token-budgeted context for an agent session, and statically
checks implementation drift after the fact. Its `ready` command is the gate between planning
and building, run in CI.

One design decision in sdx mirrors this whole page: the core pipeline makes **no LLM calls**.
Validation is deterministic, keyed to run in CI, because the agent already lives in your
editor — the spec tooling's job is to keep it honest, not to be clever. The spec is the
interface; the tools just make lying to yourself harder.
