Undocumented decisions get made twice
Nineteen architecture decision records for a codebase of twenty source files, written by one person in a build measured in days, looks like ceremony worship. The usual advice runs the other way: ADRs are for big teams and big bets, and a solo project should just write good code. That advice assumes the reader of the record is a future teammate. On this project the reader was an agent session that woke up every morning with no memory.
The migration that happened twice
Early in the project I moved the test files out of src/ into a top-level test/ folder and excluded them from the Docker build — it's in the log, commit message and all. Days later, the log shows the same migration again: two commits, both titled "Moved tests to test folder." The layout had drifted back in between, and I was moving the same files a second time.
It drifted back because the first move was never recorded as a decision. To the next agent session, an unwritten decision is indistinguishable from an accident of history — the file layout is just how things happen to be, and "how things happen to be" is exactly what a model feels free to improve. Colocated tests are a perfectly defensible default, so the model chose it, fresh, the way it would for any repo that had no written opinion on the matter.
Session amnesia
An agent session starts from zero. Whatever I decided yesterday exists for the model only if it lives in the files the session loads — the instructions file, the code, the docs. Everything else, the conversation where we settled it and the reasoning I hold in my head, is gone by the next session. A decision that lives only in my head isn't a decision to the agent; it's a preference I have to keep re-asserting, and re-litigation is what it looks like on the days I forget to.
That's what nineteen records over twenty source files actually is: not ceremony, a memory prosthesis. After the test layout finally got its record, the migration stayed put — not because the model got smarter, but because the decision had become something a session could load.
Three layers for one rule
The strongest decision in the repo is enforced three times. The rule says controllers read their query parameters through a dedicated DTO, never as raw @Query('name') primitives. Its text lives in the decision record. A one-line restatement sits in the instructions file every agent session loads. And an ESLint no-restricted-syntax gate on **/*.controller.ts fails the build if anyone — me or the model — writes the banned form anyway.
Each layer catches a different reader. The record persuades a human who wants the why; the instructions line reaches every session cheaply; the lint gate catches us both on the days we read neither. The rule held not because anyone remembered it, but because forgetting it stopped compiling.
I argued in Rule drift that a rule should live in one canonical home precisely so copies can't disagree, and this arrangement looks like a violation of my own rule. The defense I'd offer: the record holds the rule's text, and the other two layers hold a pointer and a tripwire — enforcement is not a copy. But I hold that distinction by hand, and hand-held distinctions drift; that's the part of this setup I trust least.
The Monday version: find the decision you've re-explained to your agent twice — that's the tell — and give it the three layers: a recorded why, a line in the file every session loads, and a lint rule if it's mechanically checkable. This is agent-specific advice; human teams re-litigate too, but not because their memory resets overnight.
The first test-folder move felt too small to record, and that's the part I keep chewing on. The decisions that get re-made are precisely the ones that felt too small to write down — which means the natural filter, record the big ones, selects for the wrong set. I don't yet have a rule for which small decisions deserve their tripwire before they've been violated once.