# Spec-driven development needs enforcement, not more prose

> Spec-driven development works when formats, checks, and ownership turn decisions into enforceable contracts. Learn what to adopt first and what to skip.

Spec-driven development fails when a team treats the specification as a better requirements document. A document can be polished, approved, and still have no effect on the software. The useful version of this practice turns selected decisions into inputs that tools can reject, test, or compare. If a decision cannot trigger a disagreement, it is probably context, not a contract.

I have watched teams spend weeks standardizing templates while production behavior kept drifting. I have also watched small teams get real control from a plain Markdown file, a JSON Schema, and one CI job. The difference was not the format. The second team knew which statements had to become executable, who owned them, and where failure should stop the work.

The emerging toolchain looks larger than it is because vendors bundle several jobs under the word "spec." Separate those jobs and the adoption order becomes much clearer. You need a durable place for intent, machine-readable contracts at system boundaries, enforcement at the cheapest useful point, and evidence that implementation still matches the decision. Start there before buying a platform or feeding a repository to an agent.

## A spec must be executable enough to disagree

A useful specification states a decision at the precision needed for a person or tool to detect a violation. It does not need to describe the whole system. It needs to remove ambiguity where ambiguity is expensive.

Teams routinely blur four different artifacts: product intent, interface contracts, implementation plans, and verification. Product intent explains the behavior and why users need it. A contract defines allowed inputs, outputs, states, or dependencies. A plan orders the work. Verification proves that a particular build meets selected claims. Calling all four a spec creates a fat document that no tool can consume and no owner can maintain.

Take a password-reset flow. "Users can reset a forgotten password" is product intent. "A reset token expires after 15 minutes and can be used once" is a behavioral contract. "Add a token table, then update the mail worker" is an implementation plan. Tests that reuse a token and advance the clock past 15 minutes are verification. Each sentence belongs somewhere different, and each changes for a different reason.

This distinction matters more when coding agents enter the workflow. An agent can turn vague intent into plausible code, but plausibility is exactly the risk. If the repository does not say whether token reuse returns 400, 401, or an idempotent 204, the agent will choose. The code may compile and pass generic tests while creating an API behavior nobody approved. More prompting will not solve an absent decision.

RFC 2119 gives standards authors the familiar MUST, SHOULD, and MAY vocabulary. Its useful lesson is narrower than copying capitalized words into every ticket: reserve binding language for statements that truly constrain implementations. If everything is a MUST, reviewers stop distinguishing a compatibility break from a preference. Use ordinary prose for explanation and explicit requirement identifiers for decisions you intend to verify.

A quick test exposes weak specs. Ask two engineers to name the observation that would prove a sentence false. If their answers differ, sharpen the sentence or admit that it is guidance. That is not a demand to formalize every thought. It is a way to keep background material from masquerading as an enforceable contract.

## Formats should follow the decision they carry

No single spec format should own the entire development process. Choose the smallest format that can express a decision and connect to a checker without forcing maintainers to duplicate the same fact.

Markdown works well for goals, exclusions, terminology, state transitions, and acceptance examples. It is readable in review, survives vendor changes, and sits beside the code. Its weakness is structural ambiguity. A heading called "Error handling" tells a parser little, and natural-language examples can contradict a table without producing an error. Use Markdown for meaning that benefits from explanation, then give binding statements stable IDs such as `AUTH-RESET-004`.

OpenAPI fits HTTP interfaces when clients, gateways, contract tests, or documentation need the same description. OpenAPI 3.1 deliberately aligns its Schema Object with the JSON Schema vocabulary, which reduces the old gap between API descriptions and validation. That does not make an OpenAPI file a complete behavior spec. It can say a field is required and match a pattern; it cannot, by itself, explain why a reused reset token must fail or how concurrent requests resolve. Put those behaviors in tests tied to requirement IDs.

JSON Schema is the better fit for events, configuration files, tool inputs, and stored documents that are not primarily HTTP operations. It can reject unknown properties, constrain combinations, and evolve through explicit versions. Teams often omit `additionalProperties: false` because strictness feels inconvenient. They then accept misspelled fields silently, which is much worse. Strictness at an owned boundary is useful; strictness on an open extension surface needs a deliberate compatibility policy.

Protocol Buffers and other interface definition languages make sense when generated clients and binary compatibility already shape the system. They are costly as a first adoption merely because they look formal. A format earns its place when a real consumer reads it. Otherwise the team maintains ceremony.

Architecture decision records belong beside these contracts, but they answer a different question. An ADR records a consequential choice, its context, and rejected alternatives. It should not repeat every endpoint or schema. Link a contract change to an ADR when the reason will matter after the original reviewers leave, such as choosing at-least-once delivery and requiring idempotent consumers.

The practical stack is usually mixed:

- Markdown for intent, exclusions, examples, and decisions that require explanation.
- OpenAPI or an interface definition language for callable boundaries.
- JSON Schema for events, configuration, and structured agent outputs.
- Tests for temporal rules, authorization, concurrency, and failure behavior.
- ADRs for choices whose rationale must survive team turnover.

Duplication is the warning light. If an enum appears in Markdown, OpenAPI, application code, and a test fixture, decide which copy generates or validates the others. Four manually synchronized truths will drift.

## Enforcement hooks turn a spec into a control surface

A specification changes behavior only when a violation reaches the person who can fix it at a useful time. The best hook is the cheapest one that catches the mistake with enough context to act.

Editors and local hooks provide fast feedback for syntax, schema validity, generated-file drift, and simple policy checks. Keep them quick and deterministic. Developers bypass slow local hooks, often for good reasons. A hook that starts containers and runs an integration suite belongs in CI, not between `git commit` and the next command.

Pull-request CI should validate the spec itself, compare compatibility, regenerate derived artifacts, and run contract-focused tests. A minimal shell sequence can be boring:

```sh
set -eu
./bin/validate-spec specs/orders.openapi.yaml
./bin/check-api-compat origin/main specs/orders.openapi.yaml
./bin/generate-api check specs/orders.openapi.yaml
./bin/test-contracts requirements specs/orders.md
```

The output should identify the decision and the observed conflict, not just return exit code 1. A useful failure has a stable shape:

```text
COMPATIBILITY_ERROR operation=POST_/orders path=response.201.id
change=required_property_removed requirement=ORD-CREATE-012
baseline=origin/main candidate=HEAD
```

That shape matters for humans and agents. It tells a developer where to look, tells automation which requirement failed, and leaves evidence in the pull request. A vague "schema check failed" sends everyone spelunking through logs.

Repository rules should block merges on binding checks, but not every warning deserves that power. Separate three levels. Errors protect compatibility, security boundaries, or promised behavior. Warnings identify likely drift that a reviewer can waive. Informational output reports generated changes or coverage gaps. Teams that block on every preference train people to seek bypasses.

Runtime validation belongs at boundaries where untrusted or independently deployed components meet. It should not become a second business-logic engine. Validate that an event has the expected version and fields when it enters a consumer; test business invariants in the service that owns them. Revalidating the same internal object at every function call adds cost and muddies ownership.

Agent hooks need the same discipline. A pre-tool hook can deny edits outside an allowed directory. A post-tool hook can run the narrow validator for the changed contract. A completion hook can require clean generated output and a passing requirement set. Do not let an agent rewrite the baseline spec merely to make its implementation pass. Changes to intent and changes to code need separate review signals, even when they share one pull request.

## Traceability must lead to evidence, not a matrix

Traceability is useful when a requirement ID lets a reviewer move directly from a decision to the code or test that supplies evidence. A giant spreadsheet maintained for its own sake is worse than no map because it creates false confidence.

Stable identifiers work if they describe a behavior rather than a document location. `ORD-CANCEL-007` survives heading edits and file moves. `SECTION-4.2.1` does not. Put the ID next to the binding sentence, then reference it in test names, contract fixtures, or structured test metadata. Do not scatter the prose itself across those places.

A small index can be generated from source files and test results:

```json
{
  "requirement": "ORD-CANCEL-007",
  "status": "covered",
  "evidence": ["contract/cancel_order_test.go::expired_orders_are_rejected"],
  "build": "8f31c2a"
}
```

The build identifier prevents a common audit mistake: presenting a passing test from one revision as evidence for another. The status should come from the current build, not from a checkbox someone marked last quarter. If a test is skipped, the index must say skipped. If no automated proof makes sense, name the manual review and its owner rather than inventing coverage.

Coverage percentages are tempting and usually misleading. Ten trivial requirements with tests can produce a better number than one carefully tested authorization boundary. Report uncovered binding requirements by risk and owner. A count can help a team notice drift, but it should not become a target that rewards splitting easy statements into tiny IDs.

Bidirectional links are also overrated. The spec needs to show where evidence should exist, and the evidence needs to name what it verifies. You do not need every production function to point back to a requirement. That level of tagging pollutes code and breaks during refactoring without improving confidence. Trace at observable boundaries and important invariants.

Reviewers should be able to answer four questions from a pull request: Which decisions changed? Which compatibility checks ran? Which evidence changed with them? Which gaps received an explicit waiver? If the toolchain cannot answer those questions without a meeting, it has produced records rather than control.

## A thin vertical slice proves the toolchain

The first implementation should cover one change from intent through runtime behavior. A vertical slice reveals integration costs that a month of format evaluation will hide.

Suppose a service must add an optional `delivery_note` to order creation. The field accepts up to 280 Unicode characters, clients may omit it, stored orders preserve it exactly, and older consumers ignore it. This is small enough to finish but rich enough to exercise prose, an API contract, generated code, tests, and compatibility.

1. Add `ORD-CREATE-019` to the Markdown spec with the limit, omission behavior, preservation rule, and compatibility expectation. State that whitespace is preserved if that is truly the product decision. Otherwise developers will make different "helpful" choices.
2. Add the optional field to the request and response schemas. Use the contract's native length keyword and keep the property optional. Regenerate types instead of editing generated files.
3. Add examples at the awkward edges: omitted, empty, 280 characters, 281 characters, leading spaces, and a multi-code-point character. Decide whether the limit counts Unicode code points, grapheme clusters, or encoded bytes. "Characters" alone is not precise enough for enforcement.
4. Run compatibility against the production baseline, not whichever branch happens to be checked out locally. Then run a contract test that sends the request, reads it back, and names `ORD-CREATE-019` in its metadata.
5. Deploy runtime validation in observation mode if existing traffic might violate the new schema. Count and inspect failures before rejecting requests, then switch to enforcement with an owner and rollback condition.

This slice exposes questions a format bake-off misses. Can the compatibility tool find the correct baseline in a shallow CI checkout? Does generation produce a dirty repository on a different operating system? Can tests attach requirement metadata without ugly names? Does the gateway implement the same Unicode length semantics as the service? Those are adoption facts.

Do not begin with the authentication subsystem, the oldest API, or a company-wide event catalog. Teams choose an important domain to prove commitment, then spend the pilot negotiating exceptions. Pick a boundary with active development, a clear owner, a few real consumers, and reversible deployment. The goal is to test the workflow under normal pressure.

Keep a short pilot ledger with the failure, time to diagnose, and action taken. Avoid invented productivity claims. After several changes, you should know whether the toolchain catches defects earlier, merely moves work around, or adds noise. That evidence supports the next adoption decision better than a feature comparison table.

## Coding agents need bounded authority and explicit outputs

Spec-driven development becomes more useful with coding agents because agents amplify both clarity and ambiguity. Give an agent a bounded task, authoritative inputs, allowed write locations, and commands that produce objective evidence.

A repository-level instruction file should point to the relevant specs rather than repeat them. Repetition lets the agent satisfy stale instructions while violating the source contract. A task envelope can stay small:

```yaml
task: implement ORD-CREATE-019
authority:
  intent: specs/orders.md
  interface: specs/orders.openapi.yaml
allowed_paths:
  - services/orders/
  - tests/contract/
forbidden_actions:
  - modify_requirement
  - edit_generated_files
verify:
  - ./bin/generate-api check specs/orders.openapi.yaml
  - ./bin/test-contracts requirement ORD-CREATE-019
output_schema: specs/agent-result.schema.json
```

The output schema should require changed files, commands run, results, unresolved questions, and requested spec changes. Structured output does not make an agent truthful, so the orchestrator must run the commands independently. It does stop a paragraph of confident narration from being mistaken for evidence.

Separate proposing a spec change from implementing one. An agent may discover that the 280-character rule conflicts with an existing database column. It should report the conflict and suggest options. It should not silently lower the limit in Markdown, modify the schema, and update the test until everything agrees. Internal consistency can hide an unauthorized product decision.

Context limits also reward narrow specifications. Feeding every ADR, API file, and engineering handbook into a prompt creates collisions and wastes attention. Resolve the task's requirement IDs first, load their owning documents and direct dependencies, then add repository instructions. Retrieval should follow explicit references, not semantic similarity alone. Similar text from an obsolete spec is still obsolete.

Multi-agent pipelines need single-writer rules. One agent can propose contract edits, another can implement, and another can verify, but concurrent edits to the authoritative schema create nondeterministic results. Assign one writer per artifact, pin every worker to the same revision, and make verification consume the final diff. More agents do not remove the need for ownership.

At oleg.is, I use this kind of bounded workflow when restructuring engineering work around Claude Code, Codex, MCP tools, and multi-agent pipelines. The technology is the easy part; deciding what agents may change and what evidence leaders require is where most of the work sits.

## Adoption order should minimize irreversible mistakes

Adopt the workflow in an order that creates evidence before it creates organizational dependency. Start with one owned boundary and existing delivery pain, then expand only after the checks prove useful.

First, define a small vocabulary: what the team calls intent, a binding requirement, a contract, evidence, and a waiver. This is not a style guide. It prevents the same word from meaning a ticket to product, an OpenAPI document to backend, and a test plan to QA. Keep the definitions on one screen.

Second, put current intent and boundary contracts in version control. Do not reconstruct the entire system. Document the next change and the behavior around it. Mark uncertain existing behavior as observed, not promised. That label prevents an accidental snapshot of bugs from becoming a permanent compatibility commitment.

Third, add validation and generation checks without blocking merges. Run them for a short observation period, classify failures, and remove false positives. A tool that misreads common repository patterns will lose trust if leadership turns it into a gate on day one. Observation should have an end date and an owner; otherwise warnings become wallpaper.

Fourth, block only on syntax, deterministic generation, and compatibility rules the team already agrees are binding. Add behavioral gates as their tests become reliable. Record waivers in the pull request with an owner, reason, and expiry condition. A permanent "temporary" exception is a hidden policy change.

Fifth, give coding agents the same commands developers use. Do not create a separate agent-only validation path. One source of checks keeps failures reproducible and lets a human take over without translating an orchestration framework.

Buy a dedicated platform when coordination becomes the bottleneck: many repositories share contracts, several teams need governed waivers, evidence must be retained, or discovery across versions consumes real time. Do not buy one to avoid deciding ownership. A catalog cannot tell you whether product, platform, or a service team owns `Customer.status`; it can only preserve the ambiguity in a nicer interface.

A Team & AI Audit can be useful at this point when the question spans team shape, delivery controls, and agent authority rather than one schema tool. The fixed engagement identifies where engineering work and payroll can be reduced, but the company still has to choose which decisions it is willing to enforce.

## Spec debt deserves a deletion policy

Specifications accumulate debt when they preserve decisions that nobody owns, describe behavior that no longer exists, or duplicate facts generated elsewhere. Treating every sentence as permanent makes the corpus less trustworthy with each release.

Every binding artifact needs an owner and a retirement condition. Version public contracts when consumers need a migration window. Delete internal requirements when the owning behavior disappears, while keeping the repository history and any ADR that still explains a live architectural constraint. An obsolete requirement in the active set is not harmless documentation. Agents retrieve it, reviewers enforce it, and new tests can resurrect it.

Drift checks should compare observable artifacts. Compare OpenAPI to routed operations, migration expectations to the current schema, generated clients to their source, and requirement IDs to current test evidence. Natural-language claims often need human review, but even there a tool can flag unreferenced IDs, duplicate definitions, expired waivers, and documents with no owner.

Set a deletion rule before scaling the corpus. For example: a requirement leaves the active index when its behavior is removed, all supported consumers have migrated, and the owner approves the change. The pull request records the removed evidence and any replacement ID. This makes deletion an ordinary controlled change rather than an argument about preserving history.

Do not measure success by spec count, linked artifacts, or generated pages. Look for shorter arguments about intended behavior, compatibility failures found before deployment, faster handoffs between humans and agents, and fewer decisions made accidentally inside code. Some of those signals are qualitative. They are still more honest than a coverage score built from equally weighted requirements.

One maintenance practice deserves explicit ownership: baseline selection. Compatibility checks are only as honest as the version they compare against. A branch name is convenient, but it can move during a long-running build and may not match what users run. Record the deployed contract revision for each environment, expose it to CI as immutable build metadata, and make the check print both revisions. For a public API with several supported releases, compare against every supported baseline whose consumers still have a compatibility promise.

Waivers need the same precision. A waiver should name the failing rule, affected boundary, owner, reason, and condition that ends it. Dates can help, but an expiry tied to a migration or release is often clearer. Keep the waiver beside the check configuration so CI can report it; do not bury it in a ticket that the validator cannot see. When the condition arrives, the build should fail until the team removes the exception or reviews it again.

There is also a useful line between conformance and correctness. A request can conform to JSON Schema and still charge the wrong customer. An implementation can return the documented status code while violating an authorization decision. Schema validators prove structural claims. Contract tests prove selected observable behaviors. Unit and property tests probe internal invariants. Production telemetry can reveal inputs and sequences nobody modeled. Calling any one layer the spec creates blind spots. Give each claim to the layer that can actually observe it, and keep the requirement ID as the connection between them.

This is why procurement should follow operating experience. A team that has not maintained one baseline, waiver, and evidence path cannot evaluate a platform well. Its demo will make every graph look helpful because the team has not learned which relationships deserve governance. After a vertical slice, requirements become concrete: immutable baseline lookup, waiver expiry, multi-repository ownership, generated evidence, or agent permissions. Buy against those needs, not against the number of artifact types a vendor can display.

The first tool to adopt is therefore not a grand specification environment. It is the smallest executable contract at a boundary your team owns, plus a check that fails with a useful explanation. Make that path reliable, attach evidence to the decision, and give it a deletion policy. Keep the first boundary deliberately boring enough that failures expose the workflow rather than an exotic domain, but active enough that engineers feel the cost of every false alarm. Only then has the team earned a larger toolchain.
