DRIFT SPEC CODE

Field notes · spec-driven development

Notes on Spec Drift

As we progressed with our project using Spec Kit, we noticed the specs and the code starting to move apart and no longer telling the same story. The divergence was not random: the same handful of patterns kept recurring, each with a structural cause.

The documents in play

  • Specsspecs/<feature>/spec.md, plan.md, tasks.md and contracts/. What we intended to build, one directory per feature.
  • Agent instruction filesAGENTS.md at the repository root and in each major directory. How the system is put together right now: what runs where, which service owns what, how to work in this part of the codebase. Written for coding agents, and read constantly by people.
  • The constitution.specify/memory/constitution.md. The principles every feature is meant to honour.

The types of drift

Type What it looks like What we do
Plan drift tasks.md goes stale the moment the work merges The issue tracker owns progress; the completing PR freezes tasks.md
Status drift Every spec still says Draft Five statuses, each set by a specific PR
Context drift Work lands with no spec at all Untracked state file, loud failure, CI rejects spec-less work
Governance drift The constitution forbids what the code does Code or principle changes in the same PR; never neither
Inventory drift Agent instruction files describe a system that has moved on Generated between markers; CI fails on any diff
Reference drift References point at the old shape of a file that has moved Checked in open specs; outbound references pinned at freeze

Plan drift

What it looks like

tasks.md files with unticked boxes and file paths that no longer resolve, worst in the features that were merged the longest time ago.

Why it happens

tasks.md is a plan, but it also gets used as a progress board. One file cannot serve both roles.

What we do

tasks.md is never edited after the plan lands. Progress lives in the issue tracker.

  • Every task in tasks.md gets a tracker issue when the plan is written and carries its key on the task line. Any issue tracker will do; the point is that exactly one of them is authoritative, and it is not tasks.md.
  • Nobody ticks a box mid-flight. Ticking as you go is maintaining a second tracker by hand, badly, alongside one that does it properly.
  • The PR that completes the feature is the only place boxes get resolved, all at once, from what the issue tracker already says.
  • No box survives that PR undecided. It is ticked, or deferred with a date, an owner and a reason, or it keeps the issue key that now owns it.
[x]  done, verified
[~]  deferred — with a date, a named owner, and a reason
[ ]  outstanding — carrying the tracking issue that now owns it

Status drift

What it looks like

Every spec's status stuck in Draft, including features that have been merged in for months.

Why it happens

The status field ships with Spec Kit's spec template. It has no defined vocabulary and no moment when anyone is expected to change it.

What we do

Defined vocabulary and a PR for each transition.

  • The vocabulary is specified: Draft, Active, Partial, Frozen, Superseded. CI rejects any other value in the status field.
  • Each transition belongs to a specific PR, not to a person remembering: Active when the implementation PR opens; Frozen or Partial when the PR that completes the feature is merged; Superseded in the PR that merges the spec replacing it.
  • A status that claims something has to say what. Partial requires a partial_reason naming what is unresolved; Superseded requires a superseded_by pointing to the spec that replaces it.

A status answers exactly one question: how far should a reader trust this spec against the code as it stands today?

Status Means What it claims about the code Spec file
Draft Intent is still being written Nothing. Not a description of anything that exists Open
Active Implementation is underway The code is moving toward this and does not match it yet; the one state where a gap is expected Open
Frozen Merged, every task resolved Described the code at one commit; nothing about the code since Closed
Partial Merged, some tasks never resolved The same, plus: these named gaps were known and left open Closed
Superseded Intent replaced by a later spec Nothing current; read the successor instead Closed

Context drift

What it looks like

A feature directory with contracts and schemas committed but no spec.md, plan.md or tasks.md at all.

Why it happens

Someone is skipping the Spec Kit workflow. Or the state file Spec Kit uses to resolve the current feature points at a directory that has been abandoned or renamed.

What we do

Assume the pointer is wrong until something proves it is not.

  • The state file that resolves the current feature is untracked, so you must set it deliberately on each checkout, and it can't arrive stale from someone else's branch. Newer Spec Kit versions do this by default.
  • A pre-flight check refuses to run a Spec Kit command when the resolved directory doesn't exist or contains no spec.md. Loud failure, rather than a silent no-op that writes nothing.
  • CI fails any directory under specs/ that has contracts or schemas but no spec.md, plan.md and tasks.md.
  • We compare the branch name and resolved feature directory on every PR, and flag any mismatch.

Governance drift

What it looks like

A constitution principle stating something that the code has outgrown. For example, a constitution principle specified that backend functions should only proxy upstream APIs, with no business logic beyond schema mapping, but in reality some services' implementations contain a substantial body of first-party logic.

Why it happens

The principles were true when they were written. The architecture moved, and the constitution did not. Nothing holds the two together, so the gap opens silently and then widens.

What we do

Every principle states how it is enforced, and a contradiction is resolved in the PR that creates it.

  • Each principle in the constitution carries one of three enforcement notes: the script that checks it, the review step that checks it, or unenforced, said out loud rather than left implied.
  • A PR that contradicts a principle does not merge until either the code or the principle changes, in that same PR.
  • Principles carry the date they were last confirmed, and any architecture decision touching the same boundary reconfirms or rewrites them.

Inventory drift

What it looks like

Agent instruction files listing a fraction of the services that exist, a stale count of infrastructure stacks, a stated number of CI jobs that no longer matches the CI pipeline.

Why it happens

Some of what an agent instruction file says is a claim the source tree already answers: which services exist, how many stacks there are, what the CI pipeline runs. Once written down, that claim is a detached copy, correct when it was written and possibly never re-derived since, and it sits in a different file from the thing it describes, so whoever adds the next service has no reason to know it exists.

What we do

Anything the source tree can answer is never written.

  • Service, stack, endpoint and CI-job lists sit between generated markers in the agent instruction files, produced by a script that reads the source tree.
  • CI regenerates them and fails on any diff, so a stale list breaks the build in the PR that made it stale, not months later.

Reference drift

What it looks like

An open spec sending a reader to a section of an agent instruction file that no longer contains it.

Why it happens

An agent instruction file was renamed, or its contents moved, leaving the old name behind as a stub. References to it scattered across other specs were never updated.

What we do

References are checked mechanically, and a spec's outbound references freeze when the spec does.

  • Open specs must resolve. A CI job checks every relative reference and anchor in Draft and Active specs, the constitution and the agent instruction files. Anything that does not resolve fails the build.
  • The PR that freezes a spec rewrites its outbound references into commit-pinned form and verifies them one last time. After that, the spec points to what it meant, not what the file has become.

What to watch for

  • Nothing in CI has ever read a spec. A single grep answers it, and it predicts every other type on this list. Drift doesn't creep past a guard; without a check, there is no guard.

Where to start

  • Decide what a spec is once its work merges: a record, or a living document. Almost every other question follows from that answer, and it costs nothing to decide.

  • Keep live status somewhere built for it. The issue tracker already does this job; a checkbox in tasks.md is a status board with no notifications, no assignee and no queries.

  • Put one cheap check in CI early, even a crude one. The habit of the CI pipeline having an opinion about specs is worth more than the specific rule it starts with.


Written from practice on one monorepo. The types generalize; the fixes may need adapting.