Decision records
This skill owns the registry tool decisions.py, symlinked into the repo at scripts/decisions.py
(run decisions.py install to set up the symlink + a CI check). The tool finds the repo's docs/
by walking up from the CWD, so run it from anywhere in the repo:
python scripts/decisions.py build [--relink] # regenerate INDEX.md (+ refresh links)
python scripts/decisions.py check # validate (CI-safe; exit 1 if stale)
python scripts/decisions.py promote <name…> [--deref] [--allow-replace] # draft(s) -> accepted/
python scripts/decisions.py rename-draft-id <name> <NEW> # re-ID a draft
python scripts/decisions.py migrate-layout # one-shot: old accepted/<type>/ -> flat
python scripts/decisions.py install [repo] # adopt in a repo: symlink + pre-commit
Layout
Everything the convention owns lives under docs/decisions/:
docs/
decisions/ # the convention's namespace (the umbrella)
INDEX.md # GENERATED registry over accepted/ + archived/
README.md # human guide to the convention (scaffolded by install)
AGENTS.md # agent rules: decisions are binding here (scaffolded by install)
accepted/ # ACCEPTED numbered records — flat; `type` lives in front-matter
archived/ # RETIRED records (superseded | deprecated) — flat
drafts/ # WIP candidates — flat, 4-UPPERCASE-letter IDs, NOT in INDEX
threat-model.md # other repo docs stay siblings — still cross-reference decisions
| Stage | Dir | ID | Status |
|---|---|---|---|
| candidate (WIP) | drafts/ |
4 UPPERCASE letters, mnemonic (CONF) |
draft |
| decision | accepted/ |
global counter (0001…) |
accepted |
| retired | archived/ |
(keeps its counter) | superseded / deprecated |
Types are open — type is any lowercase slug (software: architecture, product, security;
governance: policy, legal, finance, people, compliance, operations). It lives in
front-matter only — every lifecycle dir is flat — and the set in use is whatever the accepted
records carry; the INDEX groups by it. A repo still on the old accepted/<type>/ layout runs
python scripts/decisions.py migrate-layout once: filenames (the identity) survive the move, so the
ending build --relink self-heals every reference, and check points here whenever it finds a
nested record.
There is no proposed status — "proposing" is the act of opening a PR that promotes a draft. Mint a
draft ID yourself (a mnemonic of the topic); check enforces format + uniqueness. Cross-reference
by writing the bare ID as inline code — `0006` (decision) or `CONF` (draft); never
hand-author a path — build --relink generates and self-heals every link across every docs/*.md
(records, drafts, and other docs like threat-model.md).
A collision with origin/main is warned about, not gated. Uniqueness is checked against one
tree, so two branches can each mint 0044 and both stay green until they meet. check therefore
also reads the origin/main already on disk and prints a WARN line when an ID there names a
different file, along with the next free counter. It never fetches, never fails the run, and says
nothing at all when that ref is not present — a fresh clone or an offline machine is not a finding.
promote mints past what origin/main holds. Minting is the other side of that: it is a write
— the record is renamed, its H1 rewritten and every inbound link repathed — so promote reads the
ref rather than reporting on it afterwards, and prints which counters it stepped over. The hole that
leaves is not a gap: check reads a counter origin/main holds as held rather than missing, and
the rebase closes the sequence. A number neither tree has still fails, and with no ref on disk both
behave exactly as they did before.
A citation of a superseded record is warned about, not gated. Supersession retires a record, but
nothing used to re-read the docs that cite it: a rulebook resting on 0007 kept resting on it after
0019 replaced it. check (and build) print a WARN naming the successor for every such cite in
a living doc. It never fails the run, because a mention can be deliberately historical; archived
records are frozen history and cite what they like, and the record that supersedes an old one may of
course name it.
Authoring
Copy drafts/_template.md — the only template. A record is born a draft, and promote is the only
door into accepted/: it mints the counter, flips the status, and rewrites the H1, so there is
nothing an accepted-record template would add except a way around sign-off.
Write the ruling, not the debate:
- Cite, don't restate. The full argument lives in the linked thread, memo, or log; the record carries the decision, the live rationale, and pointers under References.
- Delete a section with nothing to say. A Rationale that repeats the Decision, an unargued alternatives table, a padded consequences list — deletion beats padding, and the template marks which sections may go.
- Every sentence earns its place. A flat sentence that states the fact beats a paragraph that performs it; one flourish is a voice, a flourish per paragraph is a tell.
- One decision per record. A record that keeps growing is usually several rulings sharing a file — split them.
There is deliberately no length rule. A pure ruling often fits in a few dozen lines; a record that also carries specification — in some repos the accepted record IS the spec of what shipped — legitimately runs hundreds. Length is a symptom to weigh, not a number to police; what the bullets above remove is padding, which shows up at every length.
Promoting drafts
Promotion requires explicit human sign-off. Promoting is a finalizing, semi-irreversible act
(accepted records are held firm — changing course requires supersession, never a rewrite;
decider-approved maintenance edits such as clarity, staleness, and cross-record consistency are
allowed). Author, edit, and validate drafts freely; but never run promote — or its downstream
steps (replacing naming placeholders, resolving threads, regenerating INDEX.md) — without the
user's explicit go-ahead in the current turn. Don't infer approval from an adjacent choice (a scope
answer, a cleared checklist); when unsure, ask.
An accepted decision may never reference a draft. promote enforces this: it refuses a set that
would breach and prints exactly how to fix it (co-promote, --deref, or --allow-replace) with a
copy-paste prompt. Before any promotion the tool refuses — or any supersession — read
references/promotion.md for the mechanics.
Promotion changes a record's ID and its directory, and promote carries both through the tree:
the H1, every relative link (drafts/ and accepted/ are siblings), the mnemonic in prose, and any
spelled-out path to the draft file. It stops at docs/, and it never edits code — a 4-letter
mnemonic also reads as an identifier. Mentions outside docs/ are listed after the run for you
to work through by hand; leave any identifier that merely shares the name. mdformat reflows the
rewritten paragraphs on commit.
Adopting this in a repo
Run this skill's decisions.py install [repo] from the target repo. (repo defaults to the current
dir; install sets up there — it does not search upward.) It is idempotent: it fills in only what's
missing and regenerates INDEX.md. What it does:
- Symlinks
<repo>/scripts/decisions.pyto this skill's copy, and gitignores that path (creating.gitignoreif absent) — the symlink is machine-specific, so each clone recreates it withinstallrather than committing it. - Scaffolds
docs/decisions/:accepted/,archived/,drafts/with its template (the only one), a humanREADME.md, and an agent-facingAGENTS.md. - Generates
INDEX.md(a build artifact, not a starter). - Wires the root entry points — when the repo has no root
README.mdorAGENTS.md(a fresh or empty repo), creates each as a placeholder linking the scaffold so people and agents discover it. An existing file is left untouched (see below). - In a git repo, adds a
pre-commithook runningdecisions.py check; run that same command in CI.
If the repo already has these entry points, install leaves them alone — wire the scaffold in yourself so people and agents discover it:
- Link the scaffolded
docs/decisions/README.mdfrom the repo's contributor-facing docs —CONTRIBUTING.md, or theREADME.mdonly if it addresses contributors (skip a user-facing README) — and point contributors atdocs/decisions/INDEX.mdto browse the accepted decisions. - In the repo's root
AGENTS.md/CLAUDE.md, linkdocs/decisions/AGENTS.mdso agents pick up that decisions are binding here.
Keep each link to a one-line note on what it is.