ADR lifecycle
An ADR answers one question for whoever arrives in six months: why is it like this, and
what else was tried? A record that only says what was chosen is half useless: the reader
re-proposes the rejected options because nothing told them those were already weighed.
Parameters
| Parameter |
How the user passes it |
Resolution when absent |
dir |
dir=docs/decisions, or in prose ("save them under docs/decisions") |
the archive path declared in CLAUDE.md or AGENTS.md at the repo root (see below) → docs/adr/ if it exists → docs/adr/ (bootstrap) |
lang |
lang=pt-BR, or in prose ("in Portuguese") |
language of the existing archive's template.md → en |
Paths are relative to the repo root (git rev-parse --show-toplevel). The repo declares where
its ADRs live in the agent instructions file it already has, CLAUDE.md first, then
AGENTS.md: the first line mentioning ADRs or decision records that also contains a path is
the declaration, and the path is the archive (grep -inE 'adr|decision record' CLAUDE.md AGENTS.md). No dedicated dotfile: an agent reads CLAUDE.md/AGENTS.md anyway, and a
.adr-dir nobody opens is one more file to drift. An archive split across two folders or two
languages is worse than either parameter ignored, so: an explicit dir that differs from the
declared path and an explicit lang that differs from an existing archive are both flagged in
one line, and the existing archive wins unless the user insists.
Front-matter keys, status values and file names are always English/ASCII; only prose changes
with lang.
Modes
| Situation |
Mode |
| The archive folder does not exist or holds no ADR |
Bootstrap, then whatever was asked |
| Record a decision |
New |
| Accept, reject, supersede or deprecate a record |
Status change |
| "Are the ADRs consistent?", or before adding to an archive you have not inspected |
Audit |
| A decision is being made in passing, with no record |
Offer |
The mechanical steps live in scripts/adr.py (standard library only). Use it instead of
editing front-matter by hand: it derives the next number, refuses an invalid transition and
closes both sides of a supersede in one run, which is exactly where hand edits go wrong.
ADR=<skill-dir>/scripts/adr.py # <skill-dir> is the folder holding this SKILL.md
python3 $ADR audit DIR
python3 $ADR index DIR
python3 $ADR new DIR "Title" [--status proposed|accepted] [--supersedes NNNN] [--description "..."]
python3 $ADR status DIR NNNN accepted|rejected|superseded|deprecated [--by NNNN]
Rules
- Name:
NNNN-kebab-case-title.md, four digits, sequential, never reused even after a
deletion. The title is the decision in active voice (0007-use-zod-to-validate-env.md), not
the topic (0007-env-validation.md), so ls reads as a list of decisions.
- Front-matter:
status, date, title, description, supersedes, superseded-by.
date is the day the record reached its current status. description is one sentence,
"why X instead of Y", for whoever scans the archive without opening files. The two
supersede keys stay present even when empty, so the chain stays greppable.
- Lifecycle:
proposed → accepted | rejected; accepted → superseded |
deprecated; the other three are terminal. A rejected or superseded decision that comes back
is a new ADR, never an edit.
accepted is immutable: only the front-matter changes afterwards. Wrong or outdated
content means a new ADR that supersedes it. The record documents what was decided and what
was known at the time; editing it retroactively leaves git history as the only witness, and
nobody reads git log to understand a decision. Typos and dead links may be fixed.
- Supersede is two-sided: new ADR carries
supersedes, old one gets status: superseded,
superseded-by and a fresh date. adr.py new --supersedes does both.
- Worth an ADR: expensive to reverse, or someone will question it in six months. Not
worth one: trivially reversible, "how to do X", or a choice with no real alternative. When in
doubt, ask; an inflated archive stops being read.
Writing
An ADR is read once, fast, by someone under pressure. Budget per section:
| Section |
Budget |
| Context and problem |
up to 5 sentences |
| Considered options |
2 to 4 options, one line each: what it is and why it lost |
| Decision |
1 to 3 sentences |
| Consequences |
2 to 5 bullets, one line each |
| References (optional) |
only what was actually used |
Blowing the budget almost always means two decisions in one file; split them. Two habits do
most of the work:
- Cut the throat-clearing. No "it is worth noting", no restating the heading, no paragraph
explaining what an ADR is. Start at the first fact that matters.
- Consequences must be falsifiable. "More maintainable code" is a wish. "Invalid env
aborts boot, so a config error surfaces at deploy, not at 3 a.m. in production" is a
consequence someone could later find false.
Every claim about the outside world (framework version, open issue, "unmaintained for two
years", benchmark) needs a link under the references section with an access date, because
the premise may stop being true and the date is how the reader knows. Never cite a source you
did not read. Nothing external in play → delete the section.
Bootstrap
- Resolve
dir and lang. Create the folder.
- Copy
assets/<lang>/ (template.md, README.md, the 0001-* example ADR) into it. For a
language without a folder under assets/, translate the three en files, keeping the
structure, the front-matter keys and the NNNN/YYYY-MM-DD placeholders untouched.
- Replace
YYYY-MM-DD with today's date in the 0001-* file (not in template.md), and in
README.md replace <skill-dir> with the real path of this skill folder (relative to the
repo root when it lives inside the repo) and docs/adr with the real dir.
- Declare the archive in the repo's agent instructions so the next session finds it: add one
line to
CLAUDE.md if it exists, else to AGENTS.md, else create AGENTS.md with that line.
The line names the folder and the skill, in the file's own language, for example
- dev/docs/adr/ — Architecture Decision Records (MADR), maintained with mass-adr-lifecycle.
Put it where the file lists the repo layout when it has such a list. Skip when a line
already declares that path.
adr.py index DIR. Nothing else: no status folders, no changelog, no hand-written index.
New
- Grep the archive for the topic first. If it is already covered, this is a supersede
(
--supersedes NNNN), not an independent record.
- Get the real alternatives. If the user named only the winner, ask what else was on the
table. Never invent plausible options: a fabricated deliberation misleads exactly the future
reader the ADR exists for. If there truly was no alternative, say so in one line and
reconsider whether this is an ADR.
adr.py new DIR "Title" --status ... --description "...". Use accepted when the decision
is already in force; writing after the fact is normal, do not stage a fake proposed phase.
- Fill in the body in the archive's language, within the budget. In a supersede, the context
opens with what changed since the old record.
adr.py index DIR, then report path, number and status.
Status change
adr.py status DIR NNNN <status> [--by NNNN]. The script refuses anything outside the
lifecycle table; when it does, explain the rule in one line and offer the supersede path.
- Superseding is a New ADR with
--supersedes, which already updates the old one.
- If the request is to "fix" an
accepted ADR, state the immutability rule once, offer the
supersede, then do what the user decides.
adr.py index DIR.
Audit
adr.py audit DIR. It checks file names, front-matter keys (derived from the archive's own
template.md), status values, ISO dates, the # NNNN. heading, required sections, duplicate
numbers, both sides of every supersede chain, reference bullets without a URL or access
date, size budgets and a stale index.md. Exit code 1 means errors.
- Then read what the script cannot reach: an
accepted record describing something the code
no longer does, or records over budget.
- Report grouped as errors (break the rules) and suspects (need a human decision). Fix
errors on request. Never rewrite the body of an
accepted ADR to fix drift; that is a
supersede.
Offer
When an expensive-to-reverse decision is being made in the conversation and nothing records
it, offer once, in one line, at a natural pause after the work lands:
This meets the ADR bar (expensive to reverse). Want me to record it in <dir>?
If the answer is no, drop it and do not raise it again for that decision. Interrupting delivery
to campaign for documentation is how documentation gets rejected.
1---2name: mass-adr-lifecycle3description: Creates and maintains Architecture Decision Records (MADR minimal) through their whole lifecycle, bootstrapping the archive, writing new records, moving them through proposed/accepted/rejected/superseded/deprecated and auditing numbering and supersede chains, in any folder and language (default docs/adr/, English). Use whenever the user mentions ADRs or decision records, or asks to "write an ADR", "record this decision", "document why we chose X", "supersede ADR 0003", "deprecate that decision", "audit our ADRs" or "set up ADRs", and also when an expensive-to-reverse architecture choice is being made in the conversation with no record of it. Do NOT use for how-to guides, READMEs or changelogs.4license: CC-BY-4.05---67# ADR lifecycle89An ADR answers one question for whoever arrives in six months: **why is it like this, and10what else was tried?** A record that only says what was chosen is half useless: the reader11re-proposes the rejected options because nothing told them those were already weighed.1213## Parameters1415| Parameter | How the user passes it | Resolution when absent |16|---|---|---|17| `dir` | `dir=docs/decisions`, or in prose ("save them under docs/decisions") | the archive path declared in `CLAUDE.md` or `AGENTS.md` at the repo root (see below) → `docs/adr/` if it exists → `docs/adr/` (bootstrap) |18| `lang` | `lang=pt-BR`, or in prose ("in Portuguese") | language of the existing archive's `template.md` → `en` |1920Paths are relative to the repo root (`git rev-parse --show-toplevel`). The repo declares where21its ADRs live in the agent instructions file it already has, `CLAUDE.md` first, then22`AGENTS.md`: the first line mentioning ADRs or decision records that also contains a path is23the declaration, and the path is the archive (`grep -inE 'adr|decision record' CLAUDE.md24AGENTS.md`). No dedicated dotfile: an agent reads `CLAUDE.md`/`AGENTS.md` anyway, and a25`.adr-dir` nobody opens is one more file to drift. An archive split across two folders or two26languages is worse than either parameter ignored, so: an explicit `dir` that differs from the27declared path and an explicit `lang` that differs from an existing archive are both flagged in28one line, and the existing archive wins unless the user insists.29Front-matter keys, `status` values and file names are always English/ASCII; only prose changes30with `lang`.3132## Modes3334| Situation | Mode |35|---|---|36| The archive folder does not exist or holds no ADR | **Bootstrap**, then whatever was asked |37| Record a decision | **New** |38| Accept, reject, supersede or deprecate a record | **Status change** |39| "Are the ADRs consistent?", or before adding to an archive you have not inspected | **Audit** |40| A decision is being made in passing, with no record | **Offer** |4142The mechanical steps live in `scripts/adr.py` (standard library only). Use it instead of43editing front-matter by hand: it derives the next number, refuses an invalid transition and44closes both sides of a supersede in one run, which is exactly where hand edits go wrong.4546```bash47ADR=<skill-dir>/scripts/adr.py # <skill-dir> is the folder holding this SKILL.md48python3 $ADR audit DIR49python3 $ADR index DIR50python3 $ADR new DIR "Title" [--status proposed|accepted] [--supersedes NNNN] [--description "..."]51python3 $ADR status DIR NNNN accepted|rejected|superseded|deprecated [--by NNNN]52```5354## Rules5556- **Name**: `NNNN-kebab-case-title.md`, four digits, sequential, never reused even after a57 deletion. The title is the decision in active voice (`0007-use-zod-to-validate-env.md`), not58 the topic (`0007-env-validation.md`), so `ls` reads as a list of decisions.59- **Front-matter**: `status`, `date`, `title`, `description`, `supersedes`, `superseded-by`.60 `date` is the day the record reached its current status. `description` is one sentence,61 "why X instead of Y", for whoever scans the archive without opening files. The two62 supersede keys stay present even when empty, so the chain stays greppable.63- **Lifecycle**: `proposed` → `accepted` | `rejected`; `accepted` → `superseded` |64 `deprecated`; the other three are terminal. A rejected or superseded decision that comes back65 is a new ADR, never an edit.66- **`accepted` is immutable**: only the front-matter changes afterwards. Wrong or outdated67 content means a new ADR that supersedes it. The record documents what was decided and what68 was known at the time; editing it retroactively leaves git history as the only witness, and69 nobody reads `git log` to understand a decision. Typos and dead links may be fixed.70- **Supersede is two-sided**: new ADR carries `supersedes`, old one gets `status: superseded`,71 `superseded-by` and a fresh `date`. `adr.py new --supersedes` does both.72- **Worth an ADR**: expensive to reverse, or someone will question it in six months. Not73 worth one: trivially reversible, "how to do X", or a choice with no real alternative. When in74 doubt, ask; an inflated archive stops being read.7576## Writing7778An ADR is read once, fast, by someone under pressure. Budget per section:7980| Section | Budget |81|---|---|82| Context and problem | up to 5 sentences |83| Considered options | 2 to 4 options, one line each: what it is and why it lost |84| Decision | 1 to 3 sentences |85| Consequences | 2 to 5 bullets, one line each |86| References (optional) | only what was actually used |8788Blowing the budget almost always means two decisions in one file; split them. Two habits do89most of the work:9091- **Cut the throat-clearing.** No "it is worth noting", no restating the heading, no paragraph92 explaining what an ADR is. Start at the first fact that matters.93- **Consequences must be falsifiable.** "More maintainable code" is a wish. "Invalid env94 aborts boot, so a config error surfaces at deploy, not at 3 a.m. in production" is a95 consequence someone could later find false.9697Every claim about the outside world (framework version, open issue, "unmaintained for two98years", benchmark) needs a link under the references section with an access date, because99the premise may stop being true and the date is how the reader knows. Never cite a source you100did not read. Nothing external in play → delete the section.101102## Bootstrap1031041. Resolve `dir` and `lang`. Create the folder.1052. Copy `assets/<lang>/` (`template.md`, `README.md`, the `0001-*` example ADR) into it. For a106 language without a folder under `assets/`, translate the three `en` files, keeping the107 structure, the front-matter keys and the `NNNN`/`YYYY-MM-DD` placeholders untouched.1083. Replace `YYYY-MM-DD` with today's date in the `0001-*` file (not in `template.md`), and in109 `README.md` replace `<skill-dir>` with the real path of this skill folder (relative to the110 repo root when it lives inside the repo) and `docs/adr` with the real `dir`.1114. Declare the archive in the repo's agent instructions so the next session finds it: add one112 line to `CLAUDE.md` if it exists, else to `AGENTS.md`, else create `AGENTS.md` with that line.113 The line names the folder and the skill, in the file's own language, for example114 `- dev/docs/adr/ — Architecture Decision Records (MADR), maintained with mass-adr-lifecycle`.115 Put it where the file lists the repo layout when it has such a list. Skip when a line116 already declares that path.1175. `adr.py index DIR`. Nothing else: no status folders, no changelog, no hand-written index.118119## New1201211. Grep the archive for the topic first. If it is already covered, this is a **supersede**122 (`--supersedes NNNN`), not an independent record.1232. Get the real alternatives. If the user named only the winner, ask what else was on the124 table. Never invent plausible options: a fabricated deliberation misleads exactly the future125 reader the ADR exists for. If there truly was no alternative, say so in one line and126 reconsider whether this is an ADR.1273. `adr.py new DIR "Title" --status ... --description "..."`. Use `accepted` when the decision128 is already in force; writing after the fact is normal, do not stage a fake `proposed` phase.1294. Fill in the body in the archive's language, within the budget. In a supersede, the context130 opens with what changed since the old record.1315. `adr.py index DIR`, then report path, number and status.132133## Status change1341351. `adr.py status DIR NNNN <status> [--by NNNN]`. The script refuses anything outside the136 lifecycle table; when it does, explain the rule in one line and offer the supersede path.1372. Superseding is a New ADR with `--supersedes`, which already updates the old one.1383. If the request is to "fix" an `accepted` ADR, state the immutability rule once, offer the139 supersede, then do what the user decides.1404. `adr.py index DIR`.141142## Audit1431441. `adr.py audit DIR`. It checks file names, front-matter keys (derived from the archive's own145 `template.md`), status values, ISO dates, the `# NNNN.` heading, required sections, duplicate146 numbers, both sides of every supersede chain, reference bullets without a URL or access147 date, size budgets and a stale `index.md`. Exit code 1 means errors.1482. Then read what the script cannot reach: an `accepted` record describing something the code149 no longer does, or records over budget.1503. Report grouped as **errors** (break the rules) and **suspects** (need a human decision). Fix151 errors on request. Never rewrite the body of an `accepted` ADR to fix drift; that is a152 supersede.153154## Offer155156When an expensive-to-reverse decision is being made in the conversation and nothing records157it, offer once, in one line, at a natural pause after the work lands:158159> This meets the ADR bar (expensive to reverse). Want me to record it in `<dir>`?160161If the answer is no, drop it and do not raise it again for that decision. Interrupting delivery162to campaign for documentation is how documentation gets rejected.