Architecture decision records for coding agents
More and more of Evolu is written by AI coding agents, often several sessions at once. They read the code and the comments in it well. What they could not read was why some things are the way they are.
Why does the relay's Docker image install the published npm package instead of a bundle? Why does sync use WebSocket and not WebTransport? The answers existed, in my notes, in chat transcripts, and in the private memory of the agent on my machine. Contributors, other agents, and sessions on other machines saw only the code. So rejected ideas kept coming back, and each time someone researched them again.
Evolu now writes those decisions down as architecture decision records.
What an ADR is
An architecture decision record, or ADR, is a short document that records one decision: the context that forced it, what was decided, the alternatives considered, and the consequences. Michael Nygard coined the term in 2011, and Martin Fowler's Architecture Decision Record is a good introduction.
Two ideas make it work. A record is short, so it gets written and read. And a decided record is never rewritten. When a decision changes, a new record supersedes the old one, so the log shows which decision governed the code and for how long.
Where the why goes
Evolu already had places for reasons. Module documentation describes how something behaves and what constrains it, such as how Evolu handles clock drift. A comment at a piece of code explains why that code is the way it is, such as why an oversized mutation throws.
An ADR is for a decision that no single place in the code owns. Some decisions shape several places: the relay image follows from how the package is built, released, and installed. Others are about something the code does not do, such as use WebTransport. Even then, we write an ADR only when the decision is hard to reverse, surprising without context, and a real trade-off. Anything else gets a comment at the code.
What we changed for agents
Most ADR guides assume a team of people. We changed four things for agents working in parallel.
Names, not numbers. ADRs are traditionally numbered. Two agent sessions in
different worktrees can both pick the same next number, and renumbering breaks
every path that points to the record. Evolu names each file after its title,
such as sync-uses-websocket-not-webtransport.md. Numbers make sense for
specifications like Bitcoin's BIPs, which other projects cite. An ADR is read
by the people and agents working on one codebase, and they find it by its
path.
Proposed means do not implement. Nygard's and Fowler's ADRs already start as Proposed. We use that status for designs that used to stay in one agent's memory, so other sessions and contributors can read them before they are decided. A Proposed ADR does not bind, and agents must not implement it. Only a maintainer accepts or rejects it. Unlike Fowler, we keep rejected ADRs, so the next agent with the same idea finds out why it lost.
Pointers in the code. Agents read the code they change, but they don't always go looking for documentation elsewhere. So code that a decision governs points to its ADR:
// Why WebSocket and not WebTransport: see
// docs/adr/sync-uses-websocket-not-webtransport.md.
AGENTS.md, which most coding agents load at the start of every session, tells
them to read the ADRs the code points to, to check docs/adr before proposing
a design, to stop and ask when a task conflicts with an accepted ADR, and to
write new ones as Proposed.
A test for the rules. Instructions are advice. A test is not.
scripts/adr.test.mts
checks that every record follows the template, that every path to an ADR
resolves, that code points only to accepted ADRs, and that an ADR is superseded
only by an accepted one. It runs with the other unit tests in under a second.
The template
Every ADR starts from the same template:
# The decision, stated as a sentence
- Date: YYYY-MM-DD
- Status: Proposed
## Context
The problem or constraint that forces a decision. Link issues, module docs, and
measurements instead of restating them.
## Decision
What we decide and why, in a few sentences.
## Rejected alternatives
- An alternative: why it loses.
## Consequences
What gets easier or harder, what this rules out, and what would make us revisit
the decision.
The rejected alternatives are what stop an idea from coming back. Our first records are why Evolu writes ADRs, why the relay image installs the published npm package, and why sync uses WebSocket, not WebTransport yet. Each fits on about a page.
Try it
We have only just started, so this post describes the setup, not results. Everything is in the repository: the README, the template, the rules, and the test. Copy whatever helps. If your agents work in parallel, the two changes we would start with are names instead of numbers and the rule that agents never implement a Proposed ADR.