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 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, 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 — 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.

Revisions

  1. Published; written from specs and decision logs across roughly 25 projects.