Spec-driven development with AI agents
The interesting problem with AI-assisted development is not getting code written. Models are good at that now. The problems are that an agent starts every session knowing nothing about the decisions you made last week, and that when it does not know something it will frequently produce a confident, well-formed, entirely wrong answer instead of stopping.
Both problems have the same shape: there is no durable, checkable record of what is true about the project.
Where this sits
That diagnosis is no longer contrarian. Through 2026 it became the mainstream reaction to vibe coding, under the name spec-driven development.
GitHub’s Spec Kit is an open-source toolkit that runs an agent through four phases – Specify, Plan, Tasks, Implement – and its announcement puts the thesis plainly: “We’re moving from ‘code is the source of truth’ to ‘intent is the source of truth.’” AWS shipped Kiro, an agentic IDE built on the same premise, where the spec is the source of truth and code is a build artifact. Tessl ships a framework plus a registry of more than ten thousand pre-built library specs, explicitly aimed at stopping agents from hallucinating APIs.
So the general idea is settled. What is still open is how rigorous the artifact has to be, and the honest answer in most write-ups is “a requirements document and some acceptance criteria.”
This post is about a project that goes considerably further, because the difference turns out to matter.
The project
yqr is a small Rust CLI we develop alongside Accent – a jq-style query and
edit tool for YAML, open source and
on crates.io at 0.5.0, so cargo install yqr
works. It is not a large program. It
carries 27 specification documents, and those are not a side artifact:
they are the issue tracker, the design record, and the standard the work is
held to. The whole set is published as part of the project’s own
website, which is itself built with
Accent.
Five kinds of document, each with one job
There is no issue tracker. No Jira, no scratch to-do files, no in-chat task
lists. Every unit of work is a markdown file in one of five directories, each
carrying a Status: field – Draft, In Progress, Done, Resolved, Superseded,
Historical.
- Research asks a question and answers it with evidence. One evaluation of
a candidate YAML library opens with a
Questionsection and then aMethodsection beginning: “Three independent, cross-checked lines of evidence.” - Architecture records a decision, what it forecloses, and what it explicitly is not.
- Features run Problem, Prior art, Design, Acceptance criteria.
- Bugs run Summary, Impact, Reproduction, Observed behavior, Root cause, Remediation options, Acceptance criteria.
- Implementation holds cross-cutting conventions and is the declared source of truth for them.
The separation matters more than the exact taxonomy. A research note that concludes nothing is fine; an architecture document that concludes nothing is a failure. Giving each kind of thinking its own container prevents the fuzzy document that is half investigation, half decision, and binding as neither.
The part that does the real work: a falsifiable invariant
The architecture document makes one product promise – the tool never rewrites bytes it did not change, so comments, key order, quoting, and blank lines survive an edit. On its own that sentence would be worth very little.
What makes it useful is the next section, which reduces the promise to a single property you can run:
yqr '.' input.yaml == cat input.yaml # byte-for-byte identical
and then the line that gives it teeth:
If
yqr .does not reproduce its input byte-for-byte, the build is broken.
That is the methodology in miniature. The prose promise is not the contract;
the executable property is the contract, and the prose exists to explain it.
Everything downstream inherits it. The bug that opened the work measured the
tool against that property across a 14-file corpus and found 13 of 14
failed. The features that followed existed to move that number. The
validate subcommand shipped later re-asserts the same invariant at runtime.
One invariant, stated once, doing work in four documents and in CI.
This is where the project departs from most of the tooling. Spec Kit, Kiro, and Tessl all give an agent structured requirements and acceptance criteria; what they do not generally produce is a single load-bearing property that fails the build. The distinction is between criteria an agent can argue it satisfied, and a command whose output either matches or does not.
That difference is the whole reason to prefer the harder version. A model
asked to “preserve formatting” produces something plausible and you end up
reviewing prose. A model asked to satisfy yqr '.' f equals cat f over a
corpus has a test it cannot talk its way past, and so do you.
Claims carry citations
The bug report that started the fidelity work is the most instructive document in the set. It argues that the library then in use could not possibly meet the guarantee – not that it did not, but that it could not, because the information is destroyed before the emitter ever runs.
An argument like that is exactly where a fluent model confabulates. This one
closes with a 24-row citation index mapping every claim to a specific
file:line in the dependency’s source, and a note on method:
Method: root cause was produced by reading the source across the load/compose/emit layers; every claim above was independently re-verified against the source (0 of 64 claims refuted).
Sixty-four claims, each re-checked, with the refutation count reported – including the number that would have been embarrassing had it been non-zero.
This turns out to be more than good manners. A cross-model study published in June 2026, Citation Discipline in Spec-Driven Development (Subham Panda), tested a framework requiring per-line requirement citations against an uncited control. Because a cited requirement that does not exist in the specification is mechanically detectable, the cited condition caught hallucinated requirements at an 86.4 percent true-detection rate on Claude Sonnet 4.6 and 88.0 percent on GLM-5-turbo, with no false positives. The uncited condition detected zero. The paper also reports the trade-off honestly: uncited generation was more deterministic; citations bought verifiability, not consistency.
Which is the point. A claim without a reference is not checkable by anything, human or machine. A claim with one is a hypothesis you can refute in seconds.
The habit shows up inline too. Assertions are annotated where they were verified – that a position index is a byte offset rather than a character offset, for instance, is marked as verified against the function that advances it. And when a later investigation contradicted the architecture document, it did not quietly diverge: it added a corrections section listing four specifics the source review had disproved, flagged to be folded back.
Specs correcting other specs, in writing, is a property worth engineering for. It is the mechanism by which a body of documents converges on being true rather than drifting into being decorative.
The false pass
One detail is worth the whole corpus. The test set included a file with a UTF-8 byte-order mark, and it was the only one of the fourteen whose output was byte-identical to its input. By the headline metric, it passed.
It was corrupt. There was no BOM handling in the library at all, so the mark
was being absorbed into the first key – the key became something other than
a, and .a returned null. The bytes round-tripped because the file had been
misparsed in a way that happened to be stable for the identity filter.
The spec records this under a heading that says so plainly: the case is corruption, not preservation.
An agent optimising for a green metric ships that. So does a tired human. It was caught because the document format demanded an explanation of each result rather than a tally, and “this one passes for a reason I cannot state” does not survive that format.
If you take one habit from this post: make the artifact require a mechanism, not just an outcome. A count of passing checks is not evidence. It is a number that happens to be green.
Superseded, never deleted
Of twelve feature specs, three are marked Superseded. Two were made obsolete by consolidating on a single YAML engine; one was replaced when an opt-in flag became default behavior.
None were deleted. They sit in the tracker with their status and a pointer to what replaced them, and the epic summary narrates the sequence – byte fidelity first shipped behind a flag, then became the default while the old pipeline moved behind a different flag, then the engine-selection flag was removed entirely.
That narration is the part an agent cannot reconstruct and will otherwise guess at. A fresh session reading only current code sees one engine and no flags, with no way to tell whether that is a considered endpoint or an accident. Reading the tracker, it knows the shape of the road.
The same instinct appears in the architecture document, about a deliberate breaking change:
Breaking the v0.1.1 contract is intended. Current
yqr .reformats; under this decision it must round-trip. This is a deliberate, pre-1.0 correction, captured here so the change is a decision and not a regression.
“Captured here so the change is a decision and not a regression” is the entire argument for writing specs, in one clause.
Prior art as a required section
Feature specs carry a Prior art section before the design. The one for the
validation subcommand surveys how jq handles it (no such command; the idiom is
jq empty), that one YAML tool shipped a validate command and later dropped
it, what the dedicated linter in the space catches that parsers do not, and
the long-standing precedent of xmllint --noout.
Only then does it decide, and the decision explicitly names what it replaces.
Models are excellent at this section and reliably skip it unless asked. They have read the ecosystem. Requiring the survey converts latent knowledge into a written comparison you can disagree with, and heads off the characteristic failure of agent-designed features: a locally sensible interface that ignores the convention every neighbouring tool already established.
Why this shape suits agents in particular
Most of the above is good engineering discipline that would pay off with an all-human team. Four parts pay off specifically because of how models fail.
Specs are memory. An agent’s context does not survive the session; a file does. This is not a hypothetical weakness – Spec Kit Agents (Taghavi and Bhavani, April 2026) identifies agents going “context blind” in large, evolving repositories, producing hallucinated APIs and architectural violations, and proposes hooks that ground each phase in actual repository evidence. Written specs are the low-tech version of the same idea: grounding that persists. The rule that a spec’s status must be updated in the same change that advances it is what keeps that memory honest, and it is enforced before a pull request rather than during review.
Falsifiability beats fluency. The characteristic model failure is not bad code, it is a convincing account of code. Byte-equality over a corpus does not care how convincing anything is.
Navigability is a design constraint. The project caps source files at around 500 lines, splitting them into directory modules past that, with the stated reason that it “keeps files agent-navigable.” Whatever you make of the number, treating “can a reader with a limited window find its way” as an architectural requirement is genuinely new, and it is downstream of the tooling.
Admitting is a rule. Two standing rules say, in effect: if a URL or a file cannot be fetched or read, stop and say so, and never reconstruct its contents from inference – never fabricate a version number, an API shape, or a changelog entry. This is written down because it is the most damaging thing a capable model does, and because it does it while sounding completely reliable. A visible blocker is worth more than a plausible guess.
What it costs
The honest accounting: this is heavy. Twenty-seven documents for a pre-1.0 CLI is a lot of prose, and a real fraction of the effort goes into writing and maintaining it rather than shipping features. The bug report discussed above is longer than the module it indicts.
It earns that on work with three properties. The problem must be subtle enough that the wrong solution looks right – byte fidelity qualifies, because a semantic round-trip looks fine until you diff it. The work must span more sessions than fit in one context. And correctness must be checkable by machine, because a spec whose acceptance criteria cannot be run is just a longer opinion.
Where those do not hold – exploratory work, a spike, anything you intend to throw away – this apparatus is friction with no return. The failure mode of spec-driven development is spec-driven procrastination, and it is real.
The smallest version worth trying
If you want the benefit without adopting the whole structure, three habits carry most of it:
- Write the falsifiable property first. Before the design, before the code: what command, run against what input, proves this works? If you cannot answer, you do not yet understand the feature, and neither will an agent.
- Make claims cite their evidence. A
file:line, a command with its output, a fetched URL. Anything asserted without one is a hypothesis and should be labelled as such – and, per the research above, it is the only version a machine can check for you. - Never delete a decision. Mark it superseded and point at what replaced it. The cost is a stale file; the benefit is that nobody re-litigates a settled question at 2am, including the agent.
None of this requires particular tooling. It is markdown in a directory, next to the code, in the same commits. If you would rather read a working example than a description of one, the specs are public.
Next: the specs as a site
There is a second half to this, and it is the reason those specs are readable in a browser at all rather than being 27 files you have to clone a repository to see.
A spec corpus is only durable memory if it can be found. Twenty-seven documents that cross-reference each other, supersede each other, and carry status fields are a small hypertext, and a directory listing is a poor way to navigate one. What that corpus wants is what any documentation wants: an index, working cross-links, full-text search, and a build that fails when a reference goes stale – which is exactly the check that keeps a superseded pointer from rotting into a dead end.
The yqr site is built with Accent for that reason. The specs on it are not
a copy or an export: they are the same markdown files the repository holds,
mounted as a section of the site and published on every push, so the record
an agent reads and the record a human browses cannot drift apart.
The next post in this series works through how that is set up – content
mounted from a directory the project already maintains, cross-references that
break the build when they dangle, search over the corpus, and deployment from
CI – using yqr as the worked example throughout.
Sources
- GitHub, Spec-driven development with AI: Get started with a new open source toolkit, and the Spec Kit toolkit.
- AWS, Kiro – agentic IDE built on spec-driven development.
- Tessl, Tessl launches spec-driven development tools for reliable AI coding agents.
- Subham Panda, Citation Discipline in Spec-Driven Development: A Cross-Model Empirical Study of Output Determinism and Automated Hallucination Detection in LLM-Generated Code, June 2026.
- Pardis Taghavi and Santosh Bhavani, Spec Kit Agents: Context-Grounded Agentic Workflows, April 2026.
- The
yqrspecification corpus, repository, and crate.