SOP?! The What-Its, Whys-Its, and Hows-Its of SOP Docs (and the One That Lied to Me)
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
| File | Role |
|---|---|
README.md | Human-readable entry point — what is this, why does it exist |
audit.md | The past — what happened, and why, in the order it happened |
state.md | The present — what's true right now |
todo.md | The path — the concrete steps from state.md toward vision.md |
vision.md | The 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 inarchive/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.