All writing

A spec for coding agents needs precedence rules and a graveyard

Structure a large-migration spec so AI agents (or contractors) can execute it safely: explicit conflict-precedence rules, a rejected-alternatives section, phase gates with acceptance criteria, and a stop-and-flag escape hatch.

Handing a large migration to coding agents fails in a characteristic way: not through incompetence, but through plausible improvisation. The agent hits a spot where the plan and the codebase disagree, or where the plan is silent, and it resolves the ambiguity with something reasonable-looking — a slightly different architecture here, a “helpful” extra feature there, a legacy behavior quietly dropped because the new design made it awkward. Each resolution is locally defensible. The sum is a system nobody designed.

Before rebuilding Cookey — a migration that replaced the plugin system, deleted Redis, and introduced an entirely new grant model — I wrote an 849-line spec (update.md, still in the repo) addressed explicitly to “coding agent(s) executing the migration, and future maintainers.” The migration then happened as a sequence of commits that track the spec’s phases almost one-to-one: 9a627ac (schema + test setup), 1a46be6 (gateway, Redis removal), bd4c67b (grant lifecycle), ffe307c (connectors as data), 0411479 (marketplace), 92a1333 (admin app), 103dc6d (docs rewrite). What made the document executable wasn’t the architecture prose — it was four structural mechanisms that ordinary design docs omit.

A source of truth needs an error policy

The spec’s self-description sets the contract: “This document is the single source of truth for the migration described below.” But single-source-of-truth is worthless without saying what happens when the source is wrong — which, over 849 lines describing a living codebase, it will be somewhere. The document’s job is not to be perfect; it’s to make every foreseeable ambiguity resolve the same way no matter which agent, session, or context window hits it.

The four mechanisms

1. A precedence rule for conflicts. The single most load-bearing sentence in the document, from its Working Agreements section:

When this document and the code disagree about current behavior, the code wins; when they disagree about target behavior, this document wins.

This one rule converts the two commonest agent failure modes into defined behavior. An agent that finds the spec’s description of the existing pipeline slightly off doesn’t “fix” the code to match the description (the code wins on current behavior — the running system is the reference implementation). An agent that finds the target design inconvenient doesn’t drift toward what’s easy (the document wins on target behavior). Alongside it sits the escape hatch that makes the rule safe: “No silent scope changes. If something here proves wrong or infeasible mid-implementation, stop and flag it rather than improvising a different architecture.” Agents follow instructions like this remarkably well — but only if the instruction exists.

2. A graveyard of rejected alternatives. Part 1 ends with “Explicitly rejected alternatives (do not re-introduce)” — rotating tokens (rejected because target apps can’t persist new secrets at runtime), executable runtime plugins (rejected because a key-holding gateway must never run fetched code), SDK-required onboarding, and attested counterparty promises (“unverifiable theater”). Each entry carries its reason, not just the verdict. This section does double duty: it stops an agent from helpfully “adding refresh tokens for security” mid-migration, and it stops a future maintainer — or the author, a year later — from relitigating a decision whose constraints they’ve forgotten. Design docs record what you chose; executable specs must also record what you refused, because the refused options are exactly the attractive-looking paths an optimizing executor will wander down.

3. Phase gates with acceptance criteria, ordered by risk. The execution plan is four phases, each ending with a testable “Accept:” block — not vibes, but checks like “full test run green with Upstash env vars absent” (proving Redis is really gone) and “a pre-existing PoP app still authenticates unchanged” (proving wire compatibility survived). Two ordering decisions encode judgment: hygiene first (Phase 0 normalizes 369 wrong file modes and untracks committed build artifacts, so every later diff is readable), and the risky novel part second — the grant system was built against the old plugin architecture in Phase 1, before Phase 2 replaced the plugins. Sequencing the unproven design before the mechanical rewrite means you learn whether the novel part works while the rest of the system is still familiar.

4. Deltas against a verified baseline. Part 2 is a snapshot of current state — file layout, Prisma models, the existing pipeline’s stage order — prefixed with “Verify against the code; this summary was accurate at the time of writing.” Target-state sections then reference the baseline explicitly, down to instructions like “read lib/redis.ts checkAndIncrementBudget for the exact current semantics and preserve them.” The pattern to copy: never describe a migration as pure end-state. Describe from what, to what, preserving which invariants — and name the files that serve as reference implementations for the invariants.

If you’re adapting this, the artifact is one Markdown file at the repo root (agents reliably find and re-read it there), with these sections in roughly this order: plain-English goal, rejected alternatives, verified current state, target architecture, phased execution with acceptance gates, working agreements. Write the working agreements as imperatives addressed to the executor — they end up quoted back to you in commit messages, which is how you know they’re being read.

When the spec itself is wrong

The spec anticipates its own failure modes. When it’s wrong about the present, the precedence rule routes around the error without halting work. When it’s wrong about the future — infeasible target behavior — the no-silent-scope-changes rule converts what would be invisible drift into an explicit interruption. When an agent ignores the document entirely, the phase gates catch it: acceptance criteria are checked against the system, not against the agent’s report, and a phase that doesn’t end green doesn’t proceed. The residual risk is staleness after the migration ships — an 849-line plan describing a completed journey slowly becomes misinformation. Cookey’s mitigation was making the docs rewrite itself a phase deliverable (commit 103dc6d), so the durable documentation was regenerated from the result, and update.md could retire into being history rather than pretending to be reference.

Delegation without a babysitter

The practical effect is that delegation stops requiring supervision-in-the-loop for every ambiguity. Agents (and human contractors — nothing here is AI-specific) interrupt you only at genuine spec defects, because everything else has a defined resolution; review effort concentrates on the acceptance gates, where it verifies outcomes instead of re-deriving intent. And the document keeps paying after the migration: when a plausible-sounding proposal resurfaces — and “what if connectors could run a little code?” always resurfaces — the answer is a link to the graveyard entry with its reasons, not a meeting.