Skip to main content
ThunderLang
← All articles
ai-engineering

The Spec Versioning Gap: Why AI Agents Silently Inherit Stale Intent

6 min read · 2026-08-08 · Allen Codewell

Versioning your code but not your specs is the same mistake as tagging a Docker image but ignoring the Dockerfile. The moment AI agents enter your delivery chain, that mistake becomes load-bearing.

A team I know spent three weeks debugging behavior that had no failing tests, no lint violations, and no deployment errors. The culprit was a spec that had been updated in Confluence two months earlier, after product had quietly changed the interpretation of a status field. The AI agent generating their service layer had been pointed at the old spec file on disk. Every function it wrote was internally consistent. All of it was wrong.

This is not a story about hallucination. The agent did not invent anything. It faithfully executed a contract that had expired.

Intent drift is what happens when generated code diverges from current intent without any mechanism to surface the gap. It is distinct from bugs, regressions, or test failures. Conventional CI catches none of it, because the code is correct relative to the spec the agent saw. The spec is just not the spec anyone cares about anymore.

Most engineering teams treat specs as documents, not as artifacts. Documents live somewhere and get edited. Artifacts have a version, a hash, a lineage, and a provenance chain. When you merge a pull request, Git records the exact tree state your change was authored against. When an AI agent generates a diff, almost no team records the spec state that change was authored against. The asymmetry is absurd once you see it.

Consider how this plays out. An agent receives a task: "implement the transfer endpoint per the payments spec." The agent reads specs/payments.yaml off the main branch. That file is at an implicit version that no one has labeled. Two weeks later, product updates a field constraint. The file changes. The previously generated implementation is now stale, but there is no foreign key between the code and the spec state it was written for. No audit trail, no diff to inspect, no test that knows what the spec said when the code was generated.

The fix is to treat a spec as a versioned artifact from the moment it exists.

# specs/payments.yaml
spec_id: payments-transfer-endpoint
version: "1.4.2"
changelog:
  - version: "1.4.2"
    date: 2024-11-01
    change: "status field now requires explicit terminal states; PENDING no longer valid on settlement"
  - version: "1.3.0"
    date: 2024-09-14
    change: "added idempotency key requirement"
intent:
  transfer_status:
    allowed_values: [INITIATED, COMPLETED, FAILED]
    deprecated_values: [PENDING]

Every agent invocation that consumes this spec should pin the version it used, the same way package-lock.json pins a dependency. The generated artifact, whether a function, a service, or a migration, should carry a provenance annotation: which spec, which version, which commit. When the spec changes, you diff not just the spec but the delta between the spec version used in generation and the spec version now live. That delta is the list of generated artifacts that may need to be revisited.

This reframes spec versioning from a documentation hygiene concern, the kind that gets a polite nod in a retrospective and no follow-up action, into a hard correctness requirement. The spec version is a precondition for evaluating whether an AI-generated diff is valid. A diff is not reviewable without knowing the intent it was generated against.

Some teams try to solve this with prompt engineering: "always read the latest spec before generating code." This does not work. It serializes generation through a single mutable file, creates a race condition when specs and generated code are in flight simultaneously, and provides no auditability after the fact. Reading the latest spec at generation time is the equivalent of using latest as your container tag. Fine until it isn't.

The better model: specs are immutable once tagged. Changes produce new versions. Agent tasks carry a spec_version field. Review gates check that the spec version in the task matches the spec version in the generated artifact's provenance record. If they diverge, the diff is held until a human confirms the delta is either acceptable or addressed.

This is not theoretical. ThunderLang's intent contract model does exactly this: you declare the spec version your agent task was issued against, the agent's output is gated against that version, and the proof artifact records the lineage. If the spec has moved since the task was issued, the gate fails and surfaces the diff between spec versions, not just the code diff.

The practical question: what does rollout look like? Start with the specs that touch the most agent-generated code. Assign them spec_id and version fields. Establish a tagging discipline: minor version for non-breaking clarifications, major version for any change in allowed values, required fields, or behavioral constraints. Wire your agent task schema to require a spec_version field. Add a CI step that checks the version in the task against the current version in the repo and flags drift. You do not need a full ThunderLang setup to get 70% of the value; the discipline alone catches most of the silent failures.

The remaining 30% is where automated verification earns its keep. Checking that generated code satisfies the constraints expressed in the spec, not just that it compiles, requires something more than a linter. It requires the spec to be machine-readable as a contract, not a PDF that lives in a wiki that nobody version-controls.

Most spec drift goes undetected not because teams lack discipline, but because the tooling never demanded it. When humans wrote all the code, a developer reading an outdated spec would often notice the inconsistency, check with product, and correct course. Agents do not notice. They execute. The cognitive safety net that informal, human-in-the-loop spec reading provided is gone the moment you add an AI to the delivery chain. The only replacement is structural.

Every generated diff is a claim about what the system should do. That claim is only valid relative to the intent that was live when the task was issued.

Gate your first AI change

ThunderLang lets you declare what a change must satisfy, then gates AI-written code against that intent with a verify-diff and durable proof artifacts that record the spec version used. If your current workflow has no answer to "what spec version was this diff generated against?", that is the gap to close first. Try it here.

A spec without a version is not a contract; it is a rumor, and AI agents will execute it faithfully.