adr-writer · git:20261006.fa6eac5 · 2026-10-06 · sha256 1516c33ac5730f79
adr-writer git:20261006.fa6eac5A
Immutable. This exact content never changes and is served at /api/v1/blob/1516c33ac5730f79.
---
name: adr-writer
description: Author an Architecture Decision Record that captures a non-obvious technical choice — its context, the decision, the consequences, the rejected alternatives, and the reversibility cost. Use whenever a non-trivial dependency is picked, a framework is chosen, a one-way door is opened, two specialists disagree and the orchestrator must pick, or anyone in a future session would ask "why did we do this?" An ADR exists so the answer is on disk, not in someone's head. Its lifecycle status lives in frontmatter in the schema's one vocabulary (proposed, accepted, open, deferred, hotfix, rejected, superseded, closed); superseding is a new ADR plus a status flip on the old — never an edit of its body.
id: skill.adr-writer
tier: 2
kind: skill
origin: seed
title: adr-writer — record non-obvious technical choices as short, superseded-never-edited ADRs
owns:
- adr-writer.method
- adr-writer.reversibility
- adr-writer.numbering
requires:
peers:
- skill.grill-planner
- agent.architect
- skill.humanizer
load_when:
- "write an ADR"
- "record an architecture decision"
- "why did we choose this dependency or design"
- "supersede an existing decision record, mark an ADR superseded"
- "which ADRs are still open, hotfix, or deferred"
- "ADR status: proposed, accepted, superseded_by"
- "opening a one-way door decision"
- "accepted risk, declined fix, or an owner-declared no-go area"
artifacts:
- templates/adr.template.md
- decisions/README.md
prevents: Choices whose reasoning exists only in the session that made them, so a later reader sees what was decided but not what was rejected or what reversing it would cost.
est_tokens: 2760
---
# adr-writer
ADRs (Architecture Decision Records) live in `docs/graph/decisions/` and
record every non-obvious technical choice. Each is a short, recorded
answer to "why did we pick this?"; a design doc is a different artifact.
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.
- A **T2 contained-lane** change made a real choice among options and
owes its why-record (`tiers.contained-lane`). Write the short form:
Context, Decision, Consequences, Reversibility, the sections that
carry the why. Skip Alternatives when none were weighed, and say so in
one line. A contained change whose why needs every section was
misclassified: hand it back for T3.
- 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/` and in the index, because a
number can be taken without a file. Each number names one decision for
good, because citations outlive files.
A number allocated to a decision that is owed but not yet written (a
spec reserves it, or the owner has not ruled yet) gets an index row
marked reserved or owed, naming who owes the body. The index row is the
whole reservation, because a stub ADR file is fiction that satisfies a
linter. The reservation holds even when the decision is abandoned. Where
a program keeps more than one ADR series (one per repository, say), a
citation names the series as well as the number, so two ADRs with the
same number cannot be confused.
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; a body value
that disagrees is a lint failure
(`python3 docs/graph/status-register.py --root docs/graph`), the drift
the register exists to catch. 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`. A ratified ADR's body is never rewritten (the append-only
exception in `holistic-editing`). Besides the supersede flip it takes
one other change: when the decision stands but a fact in its context or
reasons proves false, or execution reached the decision by a route the
body does not describe, a dated `## Correction` is appended after the
body, stating the corrected fact with its evidence and a pointer, and
the text above it stays as written. When the decision itself moves,
supersede it. 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."
When the owner states how an external arrangement works (a partner
handles a class of requests, a contract covers a duty), record it as a
declaration attributed to them, in their words where they matter, and
name what it leaves open. Conclusions drawn from it, legal ones above
all, are `agent.legal`'s to reach against its corpus; until then the ADR
records the question as owed.
### Decision
What we have decided, in **one sentence**. Optionally a short paragraph
naming the central tradeoff. A sentence that holds several decisions is
several ADRs: split it.
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, because "not as good" tells the next
agent nothing.
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.
The tag tells the next agent how much weight the decision carries.
`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
An ADR records a *choice that was made*. An implementation detail
reconstructed from source is an **observation**: record it where
observations live (a node, a runbook) with its rationale marked "not
recorded". A survey that turns up no genuine decisions leaves the index
empty and says so, because the next agent trusts every ADR it finds, and
a fabricated one is worse than a missing one.
Four 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: deferred` with `reopen_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".
- **Declining a fix is a decision.** When the owner accepts a residual
risk instead of remediating it, the ADR says plainly that nothing is
remediated by it; what it changes is the finding's standing, not the
measurement. It separates exactly what is declined from what is not.
An implemented-but-unverified control is a different finding from an
unremediated one; name which each item is, because a record that blurs
the two misleads both ways. It names the residual in plain terms and
its register row, lists any compensating control with what that
control does not cover, and quotes the owner's constraints and the
attesting person. It states the revisit trigger, or states that there
is none; a qualifier like "for now" in the acceptance is kept, because
it makes the standing expire. What a declined fix is in the register,
and when a standing acceptance becomes a `deviation` node, is
`method.incident-posture` §6, its one home; link it.
- **A declared no-go area is a decision.** When the owner puts a
repository, system, or environment off limits, the ADR states the
boundary exactly: which commands are barred (read-only ones included,
if the owner said so), and which nearby configuration that lives
elsewhere stays in scope. A contradiction seen inside the boundary
before it closed is recorded `not recorded`; the owner's boundary
outranks settling it with a probe. The consequences say that the
graph's facts about the area are frozen at their last pin and must be
presented as historical. Its standing form is a `deviation` node
(`docs/graph/nodes/_deviation.template.md`).
## Workflow
1. Find the next free number. Read the most recent few ADRs to match the
project's tone.
2. Copy `docs/graph/templates/adr.template.md` to
`docs/graph/decisions/adr-NNNN-<slug>.md`, and fill the frontmatter:
`status: proposed` (or `accepted` when the owner has already
ratified), `status_date`, and the companion the value requires. Leave
the body `## Status` as the pointer it is.
3. Fill **Context** first. If you can't write the context, you don't yet
know what decision you're making.
4. Fill **Decision** in one sentence. If you can't, the decision isn't
yet made; back up to research or brainstorm.
5. Fill **Consequences** concretely, with file paths and budget impacts
where applicable.
6. Fill **Alternatives considered**: at least one alternative, usually
two or three, each with a concrete rejection reason.
7. Fill **Reversibility** and, if `expensive` or `one-way`, name the
cost in concrete terms.
8. Cross-link: spec, grill.md, wiki pages, external sources.
9. 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.
10. Add a row to `docs/graph/decisions/README.md` (the index).
11. Add a row to grill.md §6 with the ADR's identifier; when the owner
ratifies, flip the frontmatter to `accepted` with a fresh
`status_date`. If what shipped departs from a `proposed` ADR, the
as-written text is not what gets flipped to `accepted`: record the
difference between as-written and as-shipped next to it (its index
row) and owe the amendment to the ADR's author. The close-out leaves
the body as written.
12. Before the status flips to `accepted`: the body is prose a person
reads in a year. Apply `docs/graph/skills/humanizer.md` in file mode
and run
`python3 docs/graph/prose-lint.py --file <adr> --against HEAD`; a
strong tell or a dropped number, heading, or code span blocks the
flip.
## 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 at `docs/graph/decisions/README.md`.