Skip to content

02 · Working with AI agents in a real codebase

Proposal versus decision

An agent cannot tell "we might do X" from "we decided X" unless the repository says so. How standing gets lost, and what declaring it actually costs.

6 min read

"We might move billing onto the event bus" and "we decided to move billing onto the event bus" describe the same architecture and differ in exactly one respect: whether anyone is bound by it. To a reader with no memory of the conversation, the two are frequently indistinguishable.

The distinction lives outside the document

Take a well-written design document proposing an approach. It states the problem, surveys the options, recommends one, and describes the implementation. Now take the same document after the team adopted it. The text has not changed. What changed is a fact about the world that lives nowhere in the file.

That is the whole problem in one sentence. Standing is a relationship between a document and an institution, and unless somebody writes that relationship into the document, it exists only in the memories of the people who were present.

Humans do not experience this as a gap, because their memory fills it instantly. A developer opening docs/proposals/event-bus.md knows it went nowhere. They do not consult the file to learn this; they bring it. Then they close the file and the knowledge goes with them.

How standing gets lost

Nobody destroys this information deliberately. It leaks through a sequence of individually reasonable steps.

  1. Someone writes a proposal. It is genuinely useful and correctly labelled by its location.
  2. It is discussed in a meeting, a thread, or a pull request comment. The outcome is recorded in that medium, not in the document.
  3. The decision is implemented, or partially implemented, or shelved. The document is not updated, because updating it serves no immediate purpose.
  4. The folder is reorganised. The proposal moves next to documents that were adopted. Its location no longer signals anything.
  5. The people who remember leave, change teams, or simply forget.

Now the file is a well-written technical document, in a plausible location, describing an approach, with no marker of standing anywhere on it. Every step was sensible. The information was lost anyway, which is the signature of a structural problem rather than a behavioural one.

What an agent does with it

The failure mode is not that the agent gets confused and asks. It is that it resolves the ambiguity silently and confidently.

Given a proposal describing an event bus and code that uses direct calls, an agent has to reconcile them. The usual reconciliation is to treat the document as intent and the code as lag — the document says what we want, the code has not caught up, so new work should follow the document. That is often correct, which is what makes it dangerous.

When it is wrong, the output is not an error. It is a working implementation of a rejected design, delivered with a confident summary explaining that it follows the documented architecture. It will pass review, because the reviewer sees code consistent with a document in the repository, and reviewers check consistency far more reliably than they check standing.

Retrieval makes this worse rather than better. A rejected proposal is usually longer, more detailed, and more topically focused than the one-line entry that killed it, so it ranks higher on any similarity measure. See retrieval is not truth.

Review does not catch it

The usual reassurance is that a human reviews everything, so a wrong architectural direction will be caught. In practice review is the wrong instrument for this class of error.

A reviewer is checking whether the change is correct, idiomatic, tested, and consistent with the surrounding code. All four checks pass. The implementation is competent. It matches a design document that exists in the repository. If the reviewer opens that document, it confirms the approach. Everything corroborates.

Catching the error requires the reviewer to already know that the referenced design was rejected — which returns you to the original problem, since that knowledge lives in memory rather than in the repository. Reviewers who were not in the room have no mechanism to detect it, and reviewers who were in the room may simply not remember, because they are looking at a diff rather than at the document's status.

There is a worse variant. If the change is large and well argued, review can convert the rejected proposal into the de facto design. Nobody decided to reverse the earlier decision; a piece of merged code just made the reversal a fact, and the next agent to read the repository finds an implementation and a matching document that now agree. The record has healed itself around the wrong answer.

What declaring it costs

Very little, which is the encouraging part. The cost is a header and a habit.

The header is a few lines at the top of any document that could be mistaken for binding: what kind of statement this is, when it was last confirmed, and what it descends from or was replaced by. Fifteen seconds to write.

The habit is that when a decision is made, someone writes it down where decisions live — with a stable identifier, the date, the alternatives that were rejected, and the reason. Then the proposal that lost gets a line at the top saying so and pointing at the decision.

Two minutes per decision. The reason it does not happen is not the two minutes; it is that at the moment of deciding, everyone involved knows the answer, so recording it feels like writing a note to yourself about something you could not possibly forget. The audience for that note is a person who does not exist yet, and an agent that will start with nothing.

Supersede, do not delete

The instinct when a document becomes obsolete is to delete it. That trades one failure for another.

Deletion destroys the record of why the current design is the current design. Six months on, someone proposes the rejected approach again, nobody remembers it was considered, and the team re-litigates a settled question. It also breaks every link and reference pointing at the removed document, leaving dangling references that neither humans nor agents can resolve.

The alternative is supersession: keep the document, mark it superseded, name what replaced it, and link forward. A reader arriving via a stale link lands on a clear signpost instead of a plausible lie or a 404. The historical reasoning survives. And the reference graph stays intact, which is the property that can actually be checked mechanically.

What a minimal decision record contains

The reason decision records get abandoned is usually that the template was too ambitious. A form with twelve fields does not get filled in during a busy week. The version that survives is short.

  • A stable identifier and a date. Not a filename — an identifier that other documents, commit messages, and code comments can cite and that survives any reorganisation.
  • The decision, in one sentence, in the imperative. "All background work runs through the job queue; no direct thread spawning." Not a discussion, not a summary of the meeting.
  • What was rejected, and why. One line each. This is the field that prevents re-litigation, and it is the field most often skipped because at the time it feels obvious.
  • Standing. In force, or superseded by a named later decision. This is what makes the record readable in two years.

Four fields. A minute to write while the decision is fresh, and effectively impossible to reconstruct six months later, which is the asymmetry that makes it worth doing at the time.

Note what is not in the list: implementation detail, extended rationale, diagrams, alternatives explored in depth. Those can live in the proposal document, which stays where it is and gets marked with its outcome. The decision record's job is not to explain everything; it is to make standing retrievable.

Stable identity is what makes this work

A decision that can only be referred to by a file path is fragile — paths change. Decisions need stable identifiers that survive reorganisation, so that a document, a commit message, or a code comment can point at DEC-0042 and still resolve in two years.

The related discipline is refusing version suffixes. A file named architecture-v2.md declares a successor while leaving the predecessor in place, which forks truth: two documents, both plausible, neither authoritative. One timeless name with recorded evolution behind it is strictly better than a family of versioned names, because there is never a question of which one applies.

Codee3 installs a decisions ledger with stable identifiers as part of its governed .ai/ layer, and ships guards that fail CI on version-suffixed names and on surviving references to retired names. What it cannot do is know whether a decision you recorded is still the one you are following. Declaring standing remains a human act; the guards only keep the structure that holds it from quietly falling apart.