adr-writer
ADRs (Architecture Decision Records) live in docs/graph/decisions/ and
record every non-obvious technical choice. They are not philosophy
papers and not design docs; they are a recorded answer to "why did
we pick this?"
This skill encodes the discipline of writing them well.
When to apply this skill
- A new dependency is being committed to (the wiki page handles the what; the ADR handles the why).
- A framework, language, or platform is being chosen.
- A one-way door is being opened (data model, public API contract, vendor lock-in).
- A boundary between two services or modules is being drawn.
- Two specialists disagreed and the orchestrator picked an option.
- A bug post-mortem revealed an implicit decision that should have been explicit.
- Anyone asks "why did we do it this way?" and the answer isn't in an existing ADR.
ADR numbering and status
ADRs are numbered monotonically: adr-NNNN-short-slug.md. Find the
next free number in docs/graph/decisions/. Never reuse a number.
Status lives in frontmatter, in the schema's lifecycle vocabulary
(docs/graph/_schema.md §"Lifecycle status" is the home — read it for
the meanings): status: proposed | accepted | open | deferred | hotfix | rejected | superseded | closed, always with status_date, plus the
companion the value requires — owner while open | hotfix | deferred,
reopen_when when deferred, status_evidence when closed,
superseded_by when superseded. The body's ## Status section is a
pointer to the frontmatter and never restates the value; a body value
that disagrees is a lint failure (python3 docs/graph/status-register.py --root docs/graph). An ADR is born proposed and becomes accepted
when the owner ratifies it (the grill.md §6 row); the base values apply
as the schema defines them — a "do nothing now" decision parked behind
a named trigger is deferred with reopen_when, a choice taken under
pressure that owes a proper decision is hotfix with an owner.
To replace a decision, write a new ADR with a new number and
status: accepted, and set the old ADR's frontmatter to
status: superseded + superseded_by: ADR-NNNN + a fresh
status_date. That flip is the only edit a ratified ADR ever
receives; its body is never rewritten (the append-only exception in
holistic-editing). To see what is still owed:
python3 docs/graph/status-register.py --by-kind adr --open --hotfix
(add --deferred for parked decisions) — the register is the query
surface; the index at docs/graph/decisions/README.md is the human
table.
The template (4 sections that matter most)
Use docs/graph/templates/adr.template.md. Its frontmatter is the
status home (above); the four body sections that earn their keep:
Context
What is the situation that forces a decision? Include the constraint that makes "do nothing" not viable. Cross-link to grill.md and the relevant spec.
Bad: "We need a database."
Good: "Spec SPEC-0003 requires submissions to persist across restarts. Grill.md §4 caps p95 latency at 200ms and cost at $X/month. The current implementation uses an in-memory map, which loses state on restart. We need to choose a persistent store."
Decision
What we have decided, in one sentence. Optionally a short paragraph naming the central tradeoff.
Bad: "We'll use PostgreSQL."
Good: "We will persist submissions in PostgreSQL 16, using the managed instance on platform X, accepting an additional ~$45/mo in exchange for ACID guarantees and ecosystem maturity over the in-memory alternative."
Consequences
What changes downstream. Be concrete:
- New constraints (e.g. "migrations now belong in
migrations/and run on deploy"). - Migration or rewrite cost if reversed.
- Effect on the verification plan (new gates, integration tests against a test database).
- Effect on the wiki (new library to wikify: the database driver and the migration tool).
Alternatives considered
For each rejected alternative: one paragraph naming the alternative and the concrete reason it lost.
Bad: "SQLite — not as good for our use case."
Good: "SQLite — rejected because grill.md §4 requires concurrent writes from multiple workers; SQLite serializes them and would violate the 200ms p95 budget at the projected request rate."
Reversibility tag
Every ADR tags reversibility as one of:
reversible— can be changed in a single session without data migration.expensive— can be changed but requires a multi-day project.one-way— changing it later requires a rewrite or a migration on live data.
one-way ADRs get extra scrutiny. They are the ones where the
"alternatives considered" section earns its keep — the next agent
needs to understand why the alternative was rejected, not just
that it was.
What counts as a decision — don't fabricate one
An ADR records a choice that was made, not a fact about how the system happens to be built. An implementation detail reconstructed from source is an observation, not a decision: record it where observations live (a node, a runbook) with its rationale marked "not recorded", and never dress it up as a ratified ADR. If a survey turns up no genuine decisions, the index stays empty and says so — a fabricated ADR is worse than a missing one, because the next agent trusts it.
Two decisions people forget to record because they feel like inaction:
- "Do nothing now" is a decision. Ratifying a destination while
taking no code yet — deferring the first increment behind a named,
checkable trigger — is an ADR. Separate the destination (which
end-state is correct) from the timing (what licenses starting),
and state what makes waiting safe ("the drift is now tested, not
invisible"). Its frontmatter is
status: deferredwithreopen_when:naming that trigger, so the register lists it among what is parked. - The asymmetric cost of being wrong is often the whole rationale. Record what being wrong costs in each direction — "wrong on X risks an irreversible incident; wrong on Y costs a bounded, recoverable delay" — and let the asymmetry decide, rather than arguing which option is abstractly "best".
Workflow
- Find the next free number. Read the most recent few ADRs to match the project's tone.
- Copy
docs/graph/templates/adr.template.mdtodocs/graph/decisions/adr-NNNN-<slug>.md, and fill the frontmatter:status: proposed(oracceptedwhen the owner has already ratified),status_date, and the companion the value requires. Leave the body## Statusas the pointer it is. - Fill Context first. If you can't write the context, you don't yet know what decision you're making.
- Fill Decision in one sentence. If you can't, the decision isn't yet made; back up to research or brainstorm.
- Fill Consequences — concretely, with file paths and budget impacts where applicable.
- Fill Alternatives considered — at least one alternative, usually two or three, each with a concrete rejection reason.
- Fill Reversibility and, if
expensiveorone-way, name the cost in concrete terms. - Cross-link: spec, grill.md, wiki pages, external sources.
- If this ADR replaces one: set the old ADR's frontmatter to
status: superseded+superseded_by: ADR-NNNN+status_date, and nothing else in it. - Add a row to
docs/graph/decisions/README.md(the index). - Add a row to grill.md §6 with the ADR's identifier; when the owner
ratifies, flip the frontmatter to
acceptedwith a freshstatus_date. - Before the status flips to
accepted: the body is prose a person reads in a year. Applydocs/graph/skills/humanizer.mdin file mode and runpython3 docs/graph/prose-lint.py --file <adr> --against HEAD; a strong tell or a dropped number, heading, or code span blocks the flip.
Anti-patterns
- ADR as design doc. Design docs are different artifacts; ADRs are decision records. Keep ADRs short.
- Decision sentence that is actually three decisions. Split into three ADRs.
- Alternatives with vague rejection reasons. "Not as good" doesn't help the next agent. Be concrete.
- No reversibility tag. Without it, the next agent doesn't know how much weight this decision carries.
- ADR with no cross-links. Link to grill.md, the spec, the wiki pages — the ADR isn't an island.
- Rewriting an ADR after the fact. Supersede; do not edit — the frontmatter status flip is the one exception.
- Status in the body, or in two places. The frontmatter is the
home; a
## Statusparagraph readingacceptedunder a frontmatter that sayssupersededis exactly the drift the register lints for. - An as-built observation dressed as a decision. Reconstructed detail with no recorded rationale is an observation, not an ADR — don't mint one to fill an empty index.
Reference files
docs/graph/templates/adr.template.md— the template.docs/graph/_schema.md§"Lifecycle status" — the vocabulary and its companions;docs/graph/status-register.py— its lint and query.docs/graph/agents/01-architect.md— the agent that primarily writes ADRs.docs/graph/templates/docs/+decisions/README.md— the index template installed atdocs/graph/decisions/README.md.