my-server
← Engineering

SOP?! The What-Its, Whys-Its, and Hows-Its of SOP Docs (and the One That Lied to Me)

A row of five binders on a shelf, one with a crossed-fingers bookmark poking out
Generated locally on the Cortex with z-image-base.

Every project directory in my ~/git is supposed to carry the same small set of files: README.md, vision.md, state.md, todo.md, audit.md, and usually a wiki/ and sometimes a blog/. I call this the SOP doc pattern, and I've been assuming for a while that having a pattern means having coverage — that if I say "every project maintains these files" often enough in a CLAUDE.md, it becomes true.

It doesn't. I ran a recursive audit across the whole workspace to find out how un-true, and along the way found a documented "fact" in one repo's CLAUDE.md that was simply wrong — backwards, in fact, in a way that would have made an AI agent archive the worse copy of a two-year-old tool if I'd let it run on autopilot.

This is the "what, why, how" of the pattern, what the audit actually found, and the lesson from the doc that lied.


What: the timeline, not just a file list

FileRole
README.mdHuman-readable entry point — what is this, why does it exist
audit.mdThe past — what happened, and why, in the order it happened
state.mdThe present — what's true right now
todo.mdThe path — the concrete steps from state.md toward vision.md
vision.mdThe future — the north star; why this is worth building toward at all
wiki/The design/strategy scratchpad — human thinking-out-loud, not a deliverable
blog/Outward narrative — the subset of the above worth telling someone else about

Listed as a flat file list, this looks arbitrary — five-ish files, pick your favorite subset. It isn't arbitrary; it's a timeline. audit.md is where we used to be. state.md is where we are. vision.md is where we're eventually trying to get. todo.md is the only one of the four that isn't a snapshot — it's the path between two of the others, which is exactly why it's the one that has to change constantly and the other three don't. A todo.md that isn't visibly pulling from state.md toward vision.md is just a list with no reason to exist.

README.md, vision.md, wiki/, and blog/ are aimed at a human — future-me, or someone outside the repo entirely. state.md, audit.md, and todo.md are increasingly aimed at an AI agent reading the repo cold, with no memory of the session that wrote them. state.md and audit.md in particular are the ones an agent leans on hardest — they're the closest thing this workspace has to a save file.

Why: docs as a forcing function, not a courtesy

The honest reason I started this pattern wasn't for other humans — it's essentially just me and an AI agent working this repo, session after session, with no continuity between them except what's written down. state.md and audit.md exist so that the agent starting a new session doesn't have to re-derive "is this project finished, abandoned, or mid-flight" from git log archaeology every time. todo.md exists so intentions survive a context reset — but only point in a direction if there's a vision.md somewhere for that direction to point at. wiki/ exists so my own half-formed design thinking has somewhere to live that isn't a stray file at a repo root (a distinction I got corrected on, hard, earlier today — agents/knowledge/ is agent-facing product knowledge, wiki/ is my space, and conflating them means the agent loads my working notes as if they were shipped config).

The failure mode this is supposed to prevent is simple: an agent (or me, six months later) walks into a project with no memory and has to guess. The SOP pattern's whole value proposition is that it removes the guessing. Which only works if the docs are true.

How: it doesn't hold together the way I assumed

I ran a recursive find across ~/git — excluding work/ and secrets/, which are off-limits by policy regardless — counting how many of these files actually exist, and where.

README is close to universal. 141 README.md against 9 lowercase readme.md. Almost all the lowercase stragglers are inside vendored/reference code (a bundled RecoilEngine submodule, third-party RmlUi samples) — someone else's convention leaking into my tree, not mine drifting. Only two of my own projects use the lowercase form. README, in other words, basically works. It's written once, rarely needs updating, and nothing about it decays.

state.md and audit.md are a different story. Across the whole workspace: 18 state.md files, 37 audit.md files, against 150 README files. At the top level — lab/* and projects/*, 13 directories — only two have the full four-file set (ai-workbench and sovereign-covenant). todo.md shows up in 9 of 13; state.md and audit.md barely at all outside those two.

     README state todo audit wiki
ai-workbench   Y     Y    Y    Y    Y
ansible-root   Y     .    .    .    Y
audiophile     Y     .    Y    .    Y
autografi      Y     .    Y    .    Y
blueprint      Y     .    Y    .    Y
gource-of-truth Y    .    .    .    .
my-server      Y     .    Y    .    Y
noop           Y     .    .    .    .
rust1          Y     .    .    .    Y
sovereign-covenant Y Y    Y    Y    Y
strategos      Y     .    Y    .    Y
voluptas       Y     .    .    .    Y
zed-angelscript Y    .    Y    .    .

todo.md is the file that actually survives contact with reality, because it's the one I use — every session either reads it or writes to it, so drift gets caught almost immediately. state.md and audit.md have no equivalent forcing function. Nothing breaks if audit.md goes six months without an entry. Nothing complains if state.md describes a project that quietly got abandoned three sessions ago. They decay silently, exactly because nothing needs them to be current in order to keep working — until an agent trusts one.

Zoom into ai-workbench's own projects/ — a monorepo nested inside the outer sprawl — and compliance jumps to 7 or 8 out of 10 with the full set. Same pattern, same person, wildly different adherence. The difference isn't discipline, it's age: SOP docs get applied prospectively to new work, almost never retrofitted onto old work. The stuff from before the pattern existed just... doesn't have it, forever, unless something forces a look back.

vision.md barely exists at all, and not where I'd have guessed. Six of them in the entire ~/git tree — sovereign-covenant/VISION.md, rust1/wiki/VISION.md, audiophile/wiki/VISION.md, ansible-root/wiki/VISION.md, blueprint/wiki/VISION.md, autografi/wiki/vision.md (the one casing straggler, same drift as README.md's). Every one of them lives in a project I'd call mid-maturity — enough design thinking to have written down why, not yet grown into full SOP compliance otherwise. ai-workbench — the one project with a full README/state/todo/audit/wiki set — has zero. The most procedurally-disciplined project in the workspace is the one with no stated destination, which is a strange thing to notice about your own repo: it's very good at recording where it's been and what's next, and has never once written down where "next" is supposed to add up to.

And todo.md itself doesn't stay singular the way the table above implies. That table counts one todo.md per top-level project, which undercounts badly the moment a project is itself a nested monorepo. ai-workbench/projects/** alone has 14 todo-family files against 10 sub-projects: aureum/ has both todo.md and TODO.md sitting side by side (a casing duplicate I now need to reconcile, not just report on), ipsa-agent/ deliberately keeps a second todo2.md next to its real todo.md as an unedited raw-notes archive, and there are todo.md files buried several directories deep — ipsa-agent/wiki/design/nexum/todo.md, the-bothy/bothy-discord/todo.md — for sub-components that apparently outgrew "one bullet point in the parent's todo.md" and grew their own file, with no convention covering when that split is supposed to happen. The "one todo.md per project" mental model I had going into this audit doesn't survive contact with a repo that's actually a monorepo of monorepos.

Even git's own bookkeeping decays the same way. The top-level ~/git superproject pins 12 submodules. git submodule status right now shows most of them either + (checked-out commit has drifted from what the superproject pins) or - (uninitialized entirely) — only two match cleanly. That's not a docs problem, it's the exact same shape of problem: a pointer that's supposed to describe current reality, checked once, then left to rot because nothing forces a re-check.

The doc that actively lied

The audit above is about absence — files that should exist and don't, or exist and went stale. Today's cleanup pass found something worse: a file that exists, looks authoritative, and is confidently wrong.

ai-workbench's CLAUDE.md carried this in its "Known Organizational Debt" section:

ctx-sizer/ duplication — projects/ctx-sizer/ is an incomplete migration. The full working tool is in archive/projects/ctx-szr/. Consolidation needed.

Today's task list included finally resolving this — archive whichever copy is canonical, per an older backlog item. The reflex move is to trust the note: it's right there in the repo's own instructions to the agent, written with total confidence, no hedging. Archive projects/ctx-sizer/, keep archive/projects/ctx-szr/, done.

Instead of trusting it, I checked:

mtime, src/main.rs:
  projects/ctx-sizer/src/main.rs        1776054557  (newer)
  archive/projects/ctx-szr/src/main.rs  1774591948  (older)

diff -qr src/ src/:
  config.rs, ollama.rs, quality.rs, report.rs, types.rs — all differ

SOP docs present:
  projects/ctx-sizer/:      AGENTS.md, audit.md, state.md, todo.md
  archive/projects/ctx-szr/: none of the above

The note had it exactly backwards. projects/ctx-sizer/ was the newer, more complete, actively-worked copy — not an incomplete migration at all. Following the documented "fact" would have archived the better copy and left the stale one as "canonical," and the next agent to read that CLAUDE.md would have inherited the same lie with even more confidence behind it, because now it'd also match what got archived.

There's no mystery about how this happened — the note was almost certainly true when someone (me, or an agent, session unknown) wrote it, and then work continued on projects/ctx-sizer/ without the note ever getting revisited. That's the ordinary failure mode for hand-maintained state: it's accurate at the moment of writing and nothing forces it to stay that way. The problem isn't that the note went stale — staleness is inevitable. The problem is that nothing about reading it signals staleness. It reads exactly as confidently as a true fact would.

What I actually do about it now

Not "write better docs" — that's the same failed plan with more effort behind it. The actual change is treating any doc claiming something about current state — which is canonical, which is complete, which is dead — as a hypothesis to check against ground truth before acting on it, especially before anything hard to reverse (archiving, deleting, treating one copy as authoritative over another). git log, mtimes, diff -qr between suspiciously similar directories — cheap checks, and the ctx-sizer note took about two minutes to falsify once I actually looked.

README.md doesn't need this treatment; it barely changes and nothing depends on it being current-to-the-minute. todo.md mostly polices itself through use. It's specifically the "here's the current state of the world" documents — state.md, audit.md, and especially prose "debt notes" and "known issues" sections — that carry the risk, because they look like ground truth and are actually just someone's last-verified snapshot with no timestamp on the confidence.

Concretely, today: projects/ctx-sizer/ is now archived to archive/projects/ctx-sizer/, the stale archive/projects/ctx-szr/ duplicate is gone, and the CLAUDE.md note is rewritten to say what actually happened instead of what I used to believe happened. Small fix. The bigger one is just not reflexively trusting the next note that reads with the same confidence.

One more thing changed as a direct result of writing this: I already run repo-sentinel, an Atomus/Mimir model that audits every top-level repo — git status, SOP-doc presence, submodule drift — and writes a health report. Its SOP check only looked for README/state/todo/audit, so it would have missed vision.md entirely and had no way to notice aureum/'s todo.md/TODO.md duplicate above. Widened the sensor for both, verified against aureum/ directly. What it still can't catch by construction: a doc whose content actively contradicts reality, the way the ctx-sizer note did — presence and mtime checking doesn't touch that. That's a real gap, tracked, not solved tonight.