Skip to content
Back to skills

Adr Ops

ASecurity

Author, index, and lint Architecture Decision Records — append-only memory that recovers the WHY behind a system's shape. Triggers on: adr, architecture decision record, decision log, record this decision, supersede an adr, why was this decided, adr template, next adr number.

  • 50 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 5, 2026
ai-agentspythonrustgobashgit

Security analysis

A100/100

Pro scans all 10 files and shows the line behind each finding

Scanned September 22, 2026

npx -y skills add 0xDarkMatter/claude-mods --skill adr-ops --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Adr Ops?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Adr Ops
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/0xdarkmatter-adr-ops/badge)](https://www.skillsdirectory.com/skills/0xdarkmatter-adr-ops)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: adr-ops
description: "Author, index, and lint Architecture Decision Records — append-only memory that recovers the WHY behind a system's shape. Triggers on: adr, architecture decision record, decision log, record this decision, supersede an adr, why was this decided, adr template, next adr number."
license: MIT
allowed-tools: "Read Write Edit Bash Glob Grep"
metadata:
  author: claude-mods
  related-skills: "git-ops, doc-scanner"
---

# ADR Ops

An **Architecture Decision Record (ADR)** captures one architectural decision: what was
decided, why, what was rejected, and what it costs. ADRs are **append-only project
memory** — they exist so a future maintainer touching a subsystem can recover the
*reasoning* behind its shape without archaeology through git history or chat logs.

This skill encapsulates a battle-tested ADR protocol and generalizes it to **any repo**.
The default location is `docs/adr/`, but every script takes `--dir` so a repo can keep
records anywhere (`docs/decisions/`, `architecture/adr/`, …).

---

## When to write an ADR

Write one when a change has **any** of these properties:

- It **constrains future options** — a boundary, an invariant, a "we will always / never
  do X" rule that later work must respect.
- **Multiple alternatives were seriously evaluated** and the choice is not obvious in
  hindsight.
- The **rationale is non-obvious from the code** — the code shows *what*, the ADR
  preserves *why*.

Write one decision per ADR. If a change bundles two separable decisions, write two.

### When NOT to write one

- A bug fix, refactor, or feature that follows existing architecture without changing it
  — that is a commit message.
- A reversible, low-stakes choice — that is a code comment.
- A point-in-time event with no forward constraint (a benchmark run, an incident
  write-up) — that is an audit/log entry, not an ADR.

> **Rule of thumb:** if someone could plausibly undo this next month without
> re-litigating a trade-off, it is **not** an ADR.

---

## Naming, location, numbering

- Path: `<adr-dir>/ADR-NNN-slug.md` (default `<adr-dir>` = `docs/adr`).
- `NNN` is zero-padded three digits, assigned **sequentially**: the next number is
  `highest existing + 1`. **Numbers are never reused, never reordered** — a superseded
  ADR keeps its number forever.
- `slug` is short kebab-case naming the subject (`oauth-only-auth`, `per-trial-container`).
- A protocol/how-to file (e.g. `00_*`) sorts above the numbered records and is **not**
  part of the sequence.

### The directory IS the index

Do **not** maintain a hand-curated numbered list as the source of truth — it drifts from
the filesystem. The authoritative list is the directory itself; `adr-index.sh` is just a
clean parse of it. Any prose list elsewhere (README, AGENTS.md) is a convenience pointer
that **may lag** and must say so.

---

## Canonical format (compact view)

Full template in `assets/ADR-template.md`; full rules in
`references/canonical-format.md`. The shape:

```markdown
---
status: accepted
date: YYYY-MM-DD
supersedes: []
superseded-by: []
touches:
  - "path/one.py"
---

# ADR-NNN: Title in Title Case

## Decision (one sentence)
<BLUF — one present-tense sentence stating the standing rule; greppable, stands alone.>

## Context
## Alternatives considered
## Consequences
### Positive / ### Negative / ### Non-goals
## See also
```

**Fixed section order:** Decision → Context → Alternatives considered → Consequences →
See also. Extra sections (Migration path, Enforcement, Implementation summary) go *after*
Consequences. For a multi-part decision, keep the one-sentence BLUF and add a
`## Decision (detail)` section lower down.

### Frontmatter fields

| Field | Required | Rule |
|---|---|---|
| `status` | yes | `proposed` / `accepted` / `superseded` / `deprecated` (lowercase). |
| `date` | yes | Decision date, `YYYY-MM-DD`. |
| `supersedes` | yes | YAML list of ADR ids this replaces (`[]` if none). |
| `superseded-by` | yes | YAML list of ADR ids that replace this (`[]` until superseded). |
| `touches` | yes | YAML list of paths / globs / config keys this governs. **Quote each.** The grep discovery surface — `grep touches:` answers "is there an ADR about the thing I'm changing?" |
| `extends` | optional | ADR ids this builds on without replacing. |
| `related` | optional | Companion ADR ids (not parents). |
| `deciders` | optional | Who made the call. |

A new field is a protocol change — record it in a new ADR, don't invent per-record keys.

---

## Status lifecycle & immutability

```
proposed ──► accepted ──► superseded   (superseded-by: [ADR-NNN])
                │
                └────────► deprecated  (withdrawn; nothing replaces it)
```

ADRs are **append-only**. Once `accepted`, you do not rewrite the Decision or Context.
Three change modes (full detail in `references/lifecycle-and-supersession.md`):

1. **Supersede** — the rule itself changes. Write a new ADR with `supersedes: [ADR-OLD]`;
   flip the old record's frontmatter to `status: superseded` + `superseded-by: [ADR-NEW]`
   in the **same commit**. Body stays intact (obsolete reasoning is itself a record).
   Supersession is **bidirectional** — a one-sided link is a lint error.
2. **Addendum** — new facts that refine an in-force decision. A dated
   `## Addendum — YYYY-MM-DD: <topic>` at the **end** of the body. Never use it to
   quietly reverse the decision.
3. **In-place edit** — typos, dead links, a renamed path in `touches:`. Preserves
   meaning; never rewrites rationale.

---

## End-to-end workflow

1. **Scaffold** the next record:
   `bash scripts/adr-new.sh --dir docs/adr --title "Your decision title"`
   (computes `NNN = highest+1`, derives the slug, fills frontmatter).
2. **Fill it in** — BLUF first, then Context, Alternatives, Consequences, See also.
   Keep `touches:` accurate; it is the discovery surface.
3. **Cross-check the number** against the directory to avoid a collision with a parallel
   session: `ls docs/adr/ADR-*.md` (or `bash scripts/adr-index.sh`).
4. **If superseding**, flip the old record's frontmatter in the **same commit** — either
   by hand or with `adr-new.sh --supersedes ADR-OLD --apply-supersede`.
5. **Lint** before committing: `python scripts/adr-lint.py --dir docs/adr`.
6. **Commit** with a `docs(adr):` conventional-commit subject, e.g.
   `docs(adr): ADR-020 — <subject>`.

Adopting ADRs in a fresh repo? Run `bash scripts/adr-init.sh --first-title "…"` once to
bootstrap the directory + a lint-clean ADR-001. Before changing an existing subsystem, run
`python scripts/adr-touching.py <path>` to surface any decision already governing it.

---

## Tools

All scripts take `--dir` (default `docs/adr`), `--help`, and follow semantic exit codes
(`0` ok, `2` usage, `3` not-found, `5` precondition, `10` findings/domain-signal). Pair
with the `git-ops` skill for the commit/PR step. The three read tools form the legs of a
stool: **lint** = integrity, **index** = overview, **touching** = "what governs this file
before I change it".

### `scripts/adr-init.sh` — bootstrap a repo adopting ADRs cold

```bash
# Create docs/adr/, scaffold a lint-clean ADR-001, write a generated README:
bash scripts/adr-init.sh --first-title "Adopt ADRs"

# Custom dir + preview without writing:
bash scripts/adr-init.sh --dir docs/decisions --first-title "OAuth-only auth" --dry-run
```

Refuses to run in a directory that already holds `ADR-*.md` (exit 5) unless `--force`. The
ADR-001 it scaffolds is rendered by `adr-new.sh`, so it lints clean immediately. The
generated `<dir>/README.md` is self-labeled "generated — do not hand-edit; the directory
is the index" and says to run `adr-index` to regenerate. `--dry-run` writes nothing.

### `scripts/adr-new.sh` — scaffold the next ADR

```bash
# Next number, slug derived from the title, frontmatter pre-filled:
bash scripts/adr-new.sh --title "OAuth-only auth"

# Custom dir + explicit slug + proposed status, preview without writing:
bash scripts/adr-new.sh --dir docs/decisions --title "Per-trial container" \
  --slug per-trial-container --status proposed --dry-run

# Supersede an old record and flip its frontmatter automatically:
bash scripts/adr-new.sh --title "Replace router" --supersedes ADR-002 --apply-supersede
```

Refuses to overwrite an existing file (exit 5). Atomic write. `--dry-run` prints the path
+ rendered content and writes nothing. `--number N` forces a specific number (backfilling
or coordination) — use sparingly; sequential `highest+1` is the discipline.

### `scripts/adr-index.sh` — the directory as a table (read-only)

```bash
bash scripts/adr-index.sh                       # number | status | date | title
bash scripts/adr-index.sh --json | jq '.data[] | select(.status=="accepted")'
```

Prefers `yq`; degrades to a built-in parser when yq is absent (announced on stderr).
Pass `--output FILE` to write a **generated Markdown index** (heading + a `do not
hand-edit` marker + the `| # | Status | Date | Title |` table) atomically to a file
instead of stdout — for a README pointer that you regenerate rather than hand-curate.

### `scripts/adr-touching.py` — what governs this file? (the discovery surface)

The `touches:` frontmatter is the grep target answering "is there an ADR about the thing
I'm changing?". This tool *is* that grep, done properly — match a path, glob, or config
key against every ADR's `touches:` list.

```bash
# Before editing src/auth.py, ask what decisions constrain it:
python scripts/adr-touching.py src/auth.py        # exit 10 if an ADR governs it
python scripts/adr-touching.py 'src/**'           # glob query
python scripts/adr-touching.py --json src/ | jq '.data[].number'
```

Matching is bidirectional and pragmatic: exact equality; fnmatch glob either direction
(touches `src/**` matches query `src/auth.py`; query `src/*` matches touches
`src/auth.py`); path-prefix containment (query `src/` governs touches `src/auth.py`, and
vice-versa); config keys (`file.yaml:key`) by exact-or-prefix.

**Guard contract (the load-bearing bit):** exit **0 = no governing ADR found**, exit
**10 = at least one ADR governs the query**. A pre-edit hook or CI step branches on it —
"heads up, ADR-010 governs this path; read it before changing." Exit `3` dir not found,
`2` usage.

**Batched queries:** pass several positionals and the ADR set is parsed once —
one spawn for N paths, which is what makes it cheap to call from a lint loop
(fleetflow's `ff-plan lint` went from 225 spawns to 35 on a 35-packet plan). The
exit code is any-governed (`10` if at least one query is governed, `0` only when
none is); the per-query split is in the `--json` envelope's `queries` list, each
entry `{query, governing, rc}`. `data` stays the deduped union so `.data[].number`
keeps working, and a single-query call's envelope is unchanged.

```bash
python scripts/adr-touching.py --json src/a.py src/b.py lib/ \
  | jq -r '.queries[] | select(.rc==10) | .query'   # which of these are governed
```

### `scripts/adr-lint.py` — conformance validator

```bash
python scripts/adr-lint.py --dir docs/adr        # exit 0 clean, 10 if findings
python scripts/adr-lint.py --strict --json | jq '.data[] | select(.severity=="error")'
```

Checks required + well-typed frontmatter, the `# ADR-NNN:` title matching the filename,
the BLUF placement, core section order, **no duplicate numbers** (gaps are a warning),
and **supersession bidirectionality** (the high-value cross-file check). Plus:

- **Lifecycle consistency** (errors): `superseded` with an empty `superseded-by`;
  `deprecated` with a non-empty `superseded-by`; an in-force (`accepted`/`proposed`) ADR
  carrying a `superseded-by`. These complement the bidirectionality check without
  double-reporting.
- **Stale `touches`** (warning): a `touches:` entry that is a literal filesystem path
  (not a glob, not a config key) which no longer resolves under `--repo-root` (default:
  git toplevel, else cwd) — the discovery surface may have drifted. Warning-tier only;
  counts toward exit 10 under `--strict`.

`--strict` makes warnings count toward exit 10. Exit 4 if a file's frontmatter is
unparseable.

---

## CI integration

ADRs only stay trustworthy if the integrity contract is machine-enforced. Gate the lint
in CI; `--strict` turns the stale-`touches` drift warning into a hard signal.

```yaml
# .github/workflows/adr-lint.yml
name: adr-lint
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint ADRs
        run: python skills/adr-ops/scripts/adr-lint.py --strict --dir docs/adr
        # exit 10 (findings, incl. stale-touches under --strict) fails the build
```

**Local pre-commit gate:** add `python scripts/adr-lint.py --strict --dir docs/adr` to a
pre-commit hook so a one-sided supersession or a stale discovery surface is caught before
the commit lands. A pre-edit hook can additionally call `adr-touching.py <changed-path>`
and surface the governing ADR (exit 10) before a subsystem is modified.

---

## See also

- `references/canonical-format.md` — the full template, field table, and body rules.
- `references/lifecycle-and-supersession.md` — status lifecycle + the three change modes.
- `assets/ADR-template.md` — copy-ready canonical template.

Files in this skill

  • SKILL.md12.4 KB
  • assets/ADR-template.md1.2 KB
  • references/canonical-format.md4.8 KB
  • references/lifecycle-and-supersession.md3 KB
  • scripts/adr-index.sh8.2 KB
  • scripts/adr-init.sh7.6 KB
  • scripts/adr-lint.py21.8 KB
  • scripts/adr-new.sh12 KB
  • scripts/adr-touching.py13.4 KB
  • tests/run.sh22.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.