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
  • 0 views
  • 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…