When your method repo and your product repo don't talk to each other
SpecSelf is three repositories. The method is a public repo: markdown templates, persona definitions, steering docs, MIT licensed, forkable from specself.ai. The API is a private repo: FastAPI on Lambda. The app is a third: a private Next.js PWA. A few weeks after all three were live, I went looking for the place where they agreed on what a principle even is. There wasn't one. The domain enum was defined in four places. The persona I rely on most had a lens file in the public repo and a contract file in the API, and the two had quietly stopped saying the same words.
That is the drift I want to describe, because it is the expensive kind. Code drift inside one repo gets caught by tests, by review, by the compiler if you are lucky. Contract drift between the repo that defines the method and the repos that run it gets caught by nothing, because no single repo can see it.
The invariant I thought I had#
The design in my head was clean. The public method repo is the contract. It declares what a principle looks like, what the seven life domains are, what each persona may and may not do. The API and the PWA implement that contract. Anyone who forks the public repo gets the same method the hosted product runs, because they are the same definitions.
The design on disk was three repos that had each written down their own recollection of that contract. Nothing imported anything. Nothing checked anything. The invariant existed only as an intention, and an intention is not an enforcement mechanism. I have made this exact argument about spec-driven development at the code layer for months, and I still shipped the seam without a check on it. Here is the confession: the method repo preached falsifiable records, and its own relationship to the product was an unfalsifiable claim.
What the drift actually looked like#
Three examples, in increasing order of embarrassment.
The seven-value domain enum, the list every artifact in the system tags itself with, was defined in four places: the kit's templates, the kit's docs, the API's models, and the app's UI copy. Four hand-maintained copies of a seven-item list. The day one of them gains a value, the other three are wrong, and no build anywhere goes red.
The principle template declares ten frontmatter fields. Diffable, dated, falsifiable, versioned: that is the whole pitch of the method. The hosted API was storing a principle without confidence, without serves, without last_reviewed, without supersedes. Half the schema simply never made it into storage. The serves field is the input to the method's coherence check, the graph walk that catches a project serving no goal. Which means the hosted product could not run the method's own signature check against its own corpus. The public repo promised an audit; the product had silently dropped the fields the audit reads.
And then the persona system, which is where it stopped being abstract.
The bug that made me look#
Each persona in the method is a lens with a contract: what it looks for, what it must not do, how often it may appear, and which other persona it refuses to share a session with. The Skeptic is the one that hunts borrowed language in my records. Its public lens file says a Skeptic session should be rare. The API's contract file agreed in spirit and drifted in the numbers: the code enforced a 45-day cooldown while the persona spec said 14. Same persona, same product, two different opinions about how often it exists, and both were shipped.
The second half was worse. The Skeptic's contract declares which persona it is mutually exclusive with, so a session never pairs voices that produce whiplash. That declaration is supposed to be symmetric, carried on both sides. It wasn't. The exclusivity held in one direction and not the other, which means the rule worked or failed depending on which persona the selection algorithm considered first. Both halves of this are sitting in the public issue queue: the cooldown drift is specself-api #48 and the asymmetric exclusivity carry is PR #53.
Neither is a bug a compiler can catch. Neither is a bug a code review is likely to catch, because each half of the contradiction reads as perfectly reasonable in its own file, in its own repo, in its own pull request. The reviewer of the API change would have needed the kit's lens file open in another tab and a reason to be suspicious of a number as innocent as 45.
Why review can't hold this seam#
My first instinct was process: be more careful, check the other repo when touching a contract. That instinct is worth naming so it can be dismissed. Two repos means two review contexts and no shared PR. The drift is invisible precisely at the moment of review because review happens inside one repo at a time, and the contradiction only exists across them.
I have written before about config pairs, values on two sides of a contract that must change in lockstep, like a framework's trailing-slash setting and the CDN rewrite function that assumes it. The rule there was that a config pair names its counterpart, and a machine checks the pair. What I had built with SpecSelf was the same problem one level up: not two config values inside one system, but two repositories where one defines a method and the other runs it. Cross-repo config pairs, with no pairing.
A check that spans repos cannot live in anyone's vigilance. It has to be a machine check, running in CI, with the contract itself as its input. Which raises the real question: which repo owns the contract?
The move: the method repo became a schema repo#
The answer we landed on is recorded in an architecture decision record, and it took about a week to implement. The public method repo stopped being documentation and became the contract. It gained a schemas/ directory: an enums file that is the single canonical home of the domain taxonomy and every status vocabulary, plus machine-readable schemas for the principle and persona shapes. The templates that humans read now point at those schemas instead of restating them. The persona lens files carry their contract in frontmatter: cooldowns, exclusivities, the must-not-do list.
The API vendors those files at build time. And three drift tests now run in its CI: one asserting the code's enums match the vendored contract, one asserting every persona's code contract matches the definitions vendored from the kit, including the rule that exclusivity declarations must be symmetric, and one holding the foundation questions in lockstep. The stored principle was extended to the full starter shape, so the coherence check finally has its inputs.
The class of bug did not shrink. It converted. Enum drift, persona drift, schema drift: each is now a failing test with a file and a line, instead of a contradiction waiting in the gap between two repos for the selection algorithm to find it on a Sunday morning. The Skeptic fixes above are that machinery at work, moving through the queue as tracked, testable changes rather than surprises.
Your version of this seam#
The specifics are mine, but the seam is everywhere. A design system has it: the Figma library defines the method, the component repo runs it. An editorial operation has it: the style guide defines, the CMS enforces, or claims to. Contract-first API shops have it in its purest form: the OpenAPI spec and the server that allegedly implements it. In every case there is a repo that says what should be true and a repo that makes something true, and the question is whether anything machine-readable forces them to agree.
The test is quick. Pick the enum, the schema, or the rule that both sides depend on, and ask what turns red when one side changes it. If the answer is nothing, you do not have a contract. You have two repos with similar opinions, and the opinions are diverging at whatever rate you ship.
The fix is not more discipline at review time, and it is not merging everything into a monorepo so the seam disappears. It is promoting the method repo to a schema repo: one canonical, machine-readable statement of the contract, vendored by every runtime that claims to implement it, with a CI job on each side that fails on drift. The drift I found was never really a persona bug or an enum bug. The method and the product had stopped talking, and nothing was listening for the silence.
Related:
- I Keep My Whole Life in Spec Files, what SpecSelf is and the one rule that makes it safe, the essay this series builds on
- Context architecture beats documentation dumps, where the config-pair rule comes from
- SDD is context management, the methodology this post applies one level up
Keep reading
Watch the agent write
A polish agent drafts an essay against a pre-approved topic.
Context architecture beats documentation dumps
Dumping the whole corpus into an AI agent makes it worse, not better. The fix is architectural: each task loads a curated slice, not everything you have. Here is the method, and the same move at three different layers: specs, sensor data, and evaluation lenses.
SDD isn't about managing AI agents, it's about managing context
Spec-driven development reads like a methodology for controlling AI agents. It isn't. It's a methodology for managing context across stateless sessions. The spec is the persistent memory.
Spec-Driven Development and the Folder Architecture That Makes It Work
Why spec-driven development and structured folder architecture are the missing infrastructure for AI-assisted engineering: methodology, common mistakes, and where to start.
Prompt caching is a prefix match, not a flag
Prompt caching looks like a flag you flip for a cheaper bill. It is really the reuse of a stored prompt prefix, governed by three rules, and applying it across four parts of my own system showed where it pays, where it quietly does nothing, and where it is not even my decision. With the token counts I measured to check.
Ten days of June: the SDD velocity numbers, seven weeks in
In April I published one week of SDD production numbers. The same data trail rerun for June 1 through 10 shows the velocity curve: 309 PRs opened, 293 merged, about 185 production deploys, and one footnote about outrunning GitHub Actions' default limits.
Follow the work
New tools and writing as they ship — pick a channel.