Skip to content
Back to skills

Decision Record Scan

ASecurity

ADR home, status, supersede links, superseded ADRs cited as current, chosen option absent from manifests.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentspythongorubyphpbashreactexpressdjangorailsgit

Works with

  • api

Security analysis

A100/100

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

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill decision-record-scan --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Decision Record Scan?

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

Security grade badge for Decision Record Scan
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-decision-record-scan/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-decision-record-scan)

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: decision-record-scan
description: ADR home, status, supersede links, superseded ADRs cited as current, chosen option absent from manifests.
kind: skill
pack: learning
allowed-tools: [Read, Grep, Glob, Bash]
---

# Skill: decision-record-scan

## Premise

Agents treat an Accepted ADR as binding and "fix" code back to it. So an ADR whose decision was reversed in code, a superseded ADR still cited as current, or a supersede link that runs only one way sends an agent back to a decision the team already left. Nygard's rule (2011) and MADR 4 say an ADR is never deleted, only superseded, with the status pointing at its successor; Zimmermann's review checklist asks for the same reciprocity.

There is a defect before any of that: **where the ADRs live**. On a real NestJS + React monorepo the 40 ADRs sit in `docs/decisions/`, and the project's own decision index says not to start a competing `ai/decisions/` ledger. 49 installed agents, commands, skills and rules still read or write `ai/decisions/`, which holds only a README and a template — so a health check counts 0 ADRs and `/promote-decision` would start a second ledger (KN-29).

`scripts/adr-index.py` (this skill) owns: §0 the ADR home and the consumers that read another one (KN-29), §1 status not machine-readable (KN-28), §2 one-way supersede links (KN-28), §3 a superseded or retired ADR cited as current (KN-28), §4 an Accepted ADR whose chosen package is in no manifest (KN-28, Unconfirmed).

**Every finding cites the ADR or the citing `file:line` + the ADR id and its parsed status + the other end (the successor that does not link back, the manifest that lacks the chosen package, the consumers that read the wrong home) + the edit.** "Decisions consistent" is never printed without the class-coverage block.

## The rule

A row is a finding when all four hold:

1. **The ADR home is known.** An index file declares it, or it is the directory with the most `NNNN-*.md` files among `docs/decisions`, `docs/adr`, `doc/adr`, `adr`, `architecture/decisions`, `ai/decisions` and any `decisions/` or `adr/` directory. Without one, every class is `N/A (no ADR directory)`.
2. **The status is read in the shape the project writes it.** `Status: Accepted`, `- **Status:** Superseded by [0014](…)`, `**Status:** accepted · **Date:** …`, `Date: … · Status: accepted`, a `## Status` heading, a frontmatter `status:`, a table row.
3. **The other end is checked, not assumed.** A successor's text for the back link; the corpus outside the ADR home and the index for current citations; the manifests and lockfile for the chosen package.
4. **The fix never deletes a decision.** Add the reciprocal link, re-point the citation, mark `Superseded by` or `Amended by`; a decision reversed in code needs a new ADR, which is the user's to write.

## Boundaries

- **ADR authoring format and the `/add-adr` template** → documentation, when the documentation pack is installed. This skill owns lifecycle and the home.
- **A missing ADR for an architectural dependency** (KN-30) → `/setup-project-health` check 2, which must use the home this skill detects (IR-3). Without it, the row is `N/A (owner: /setup-project-health)`.
- **Decision index rows vs ADR files** → `knowledge-fact-scan` §5 (KN-13); this skill prints the counts in its ledger only.
- **An `ADR-NNNN` id with no file** → `knowledge-ref-scan` §4 (KN-04).
- **A decision the code contradicts at large** (a convention) → align `convention-oracle` AL-04 when the align pack is installed.

## Adapt to the codebase — the ADR lanes

| Lane | Where ADRs live | Status shape | Chosen-option truth | Trap |
|---|---|---|---|---|
| **MADR 3/4** | `docs/decisions/` | `status:` frontmatter or `## Status`; "Decision Outcome: Chosen option: …" | manifests and lockfile | a `Considered options` list names rejected packages on purpose (P-2) |
| **Nygard / adr-tools** | `doc/adr/` or `docs/adr/` | `## Status` heading, next line `Accepted` / `Superseded by [n]` | manifests | adr-tools writes "Superseded by" in the old ADR and "Supersedes" in the new: both ends exist when tooling was used |
| **Refract installs** | detected home; `ai/decisions/` holds the template | as the project writes it | manifests | installed commands may hard-code `ai/decisions/` (§0) |
| **Monorepo** | one home per repo, or per app (`apps/api/docs/adr`) | — | the manifest of the app the ADR governs | several homes: the most populated wins; the ledger lists all candidates |
| **Python / Go / Ruby / PHP / JVM** | same locations | same | `pyproject.toml`, `go.mod`, `Gemfile`, `composer.json` | framework words map to packages (Django → `django`, Rails → `rails`, Laravel → `laravel/framework`) |

## Pre-flight

Run as **one** bash script from the repo root. Step **N** feeds detector **§N**. Every `rg` names its path; any `rg` exit 2 is `blocked`.

```bash
[ -n "${BASH_VERSION:-}" ] || { echo "pre-flight: run it with bash (arrays and PIPESTATUS are bash-only)"; exit 2; }
KS="${KS:-.claude/skills}"        # installed engines; in the Refract repo itself: templates/packs/learning/skills
RUN="${RUN:-$(mktemp -d "${TMPDIR:-/tmp}/knowledge.XXXXXX")}"; mkdir -p "$RUN"; echo "RUN=$RUN"
export GIT_OPTIONAL_LOCKS=0 DO_NOT_TRACK=1 CI=true

# 0-4. The engine: home detection, status parse in every shape above, reciprocity, current citations, chosen option.
python3 -I "$KS/decision-record-scan/scripts/adr-index.py" . > "$RUN/adr.tsv" 2> "$RUN/adr.ledger"
echo "adr-index exit $? (0 clean · 1 rows · 2 blocked)"; cat "$RUN/adr.ledger"; cut -f1-7 "$RUN/adr.tsv"
python3 -I "$KS/decision-record-scan/scripts/adr-index.py" . --list > "$RUN/adr-list.tsv"      # id, file, status, superseded-by, supersedes
HOME_DIR=$(awk -F'\t' 'NR==2{print $2}' "$RUN/adr-list.tsv" | sed 's#/[^/]*$##'); echo "ADR home: ${HOME_DIR:-none}"

# Cross-checks.
if [ -n "$HOME_DIR" ]; then
  # 1. Status lines in every shape: the count must equal the engine's parsed statuses.
  rg -c -i --pcre2 '^\s*(?:[-*]\s*)?\**Status\**\s*:|^#{1,4}\s*Status\s*$|[·|]\s*\**Status\**\s*:' -g '[0-9]*.md' "$HOME_DIR" | wc -l; echo "rg 1 exit ${PIPESTATUS[0]}"
  # 2. Supersede wording: every hit is an end of a pair the engine checks both ways.
  rg -n -i 'supersed' -g '[0-9]*.md' "$HOME_DIR" > "$RUN/g2.txt"; echo "rg 2 exit $? ($(wc -l < "$RUN/g2.txt" | tr -d ' ') lines)"
fi
# 0. Installed artifacts that name ai/decisions/ (the engine groups them into one root-cause row).
[ -d .claude ] && { rg -l '(^|[^/\w])ai/decisions/' -g '*.md' .claude > "$RUN/g0.txt"; echo "rg 0 exit $? ($(wc -l < "$RUN/g0.txt" | tr -d ' ') files)"; }
```

Read the ledger first: the home and how it was chosen (declared by an index, or most `NNNN-*.md`), every candidate directory with its count, the status histogram, and the index's row count against the file count.

**Measured** (2026-10-19), read-only on a real NestJS + React monorepo (0.75 s; 0 files written): home `docs/decisions` (declared in `ai/_decision-index.md`), 40 ADRs — 37 accepted, 2 superseded, 1 retired, every status parsed (rg 1: 40 status lines). Rows:
- §0: **1 root-cause row, 49 sites** — installed agents, commands, skills and rules that read or write `ai/decisions/` (rg 0: 50 files under `.claude/`; the engine also reads `CLAUDE.md` and `AGENTS.md` and drops lines that name the home too), and the second directory itself (README + template, 0 ADRs).
- §2: ADR-0028 "Supersedes: the chrome clause of [0016]" while 0016 stays Accepted — a partial supersede, Unconfirmed, fix `Amended by` (P-29). 0001 → 0014 links both ways: quiet (the decoy).
- §3: 0. A first cut flagged a runbook citing `ai/audits/2026-10-13-order-variant-matching.md` as ADR-0013 (retired): a dated file name, not an ADR citation (P-30).
- §4: 0 — every chosen package of an Accepted ADR is in a manifest.
- Index: 33 ids cited against 40 files (graded by `knowledge-fact-scan` §5).
- On `benchmarks/fixtures/*`: one tree has an ADR directory (`infra-ops`, `iac/docs/adr/`, 1 ADR whose status is written mid-line, `Date: … · Status: accepted`): 0 rows after the parser learned that shape (the first run printed "status missing"). The other 23 print `N/A (no ADR directory)`. The self-test seeds 7 rows (a second ADR directory, a command hard-coding it, a status-less ADR, a superseding ADR with no back link, an ADR another one supersedes still Accepted, a superseded ADR cited as current, an Accepted ADR choosing `axios` with only `fastify` in the manifest) and 5 decoys (history-marked citations, a dated file name, a reciprocal pair, a rejected option, a partial supersede reported as Unconfirmed).
- On `stale-knowledge` (the seeded fixture; home `docs/decisions` declared by its index, 8 ADRs — 6 accepted, 2 superseded): 5 rows, **5 of 5 seeds** — a superseding ADR that never names its predecessor, an Accepted Express ADR on a Fastify API (Unconfirmed), a superseded ADR cited as current in `CLAUDE.md`, the installed curator and `/promote-decision` writing to `ai/decisions/` (one row, 2 sites), and that second directory. Decoys: 0 of 26 (a reciprocal pair, a rejected option). The ledger counts both index id forms (`6 ids cited vs 8 files` for an index of `ADR-NNNN` rows).

## The 3,000-hit path

ADR sets are small; the volume is in §0 and §3 citations across installed artifacts.

1. **Group §0 by root cause**: the consumers are one finding — the home is hard-coded — fixed once in the pack sources (IR-3), not per file.
2. **Rank §3 by load**: citations in files loaded every session first.
3. **Read every §2 pair**: both files, whole header.
4. **Ledger the rest** as `Live, unreviewed (k sites)` with `adr-index.py . --classes=3`.

## Scans for

| § | Class | Default |
|---|---|---|
| 0 | KN-29 wrong ADR home: a second ADR directory, consumers reading it | M; H when a file loaded every session names it |
| 1 | KN-28 status missing or not a lifecycle word | L |
| 2 | KN-28 supersede link one-way; Superseded with no successor; partial supersede unmarked | M; L (Unconfirmed) when partial |
| 3 | KN-28 superseded or retired ADR cited as current | M; H in a file loaded every session |
| 4 | KN-28 Accepted ADR whose chosen package is in no manifest | M, Unconfirmed |

### 0. Wrong ADR home (KN-29)

```
BAD:   docs/decisions/ holds 0001–0040; ai/decisions/ holds README + _template; /promote-decision writes `ai/decisions/<NNNN>-<slug>.md`
GOOD:  one home; ai/decisions/README.md points at it; tooling reads the home this script detects
```
**Read the code:** Does an index or README declare the home? Do the installed consumers read the directory literally or resolve it? **Finding when** ADRs live in one directory and installed tooling reads or writes another, or a second directory starts beside the home. **Proof:** tier S + V505 — the listing of both directories and the consumer lines; after the fix the consumers resolve the detected home and `/setup-project-health` check 2 counts the real ADRs.

### 1. Status not machine-readable (KN-28)

```
BAD:   # 6. No status — "We decided things."
GOOD:  Status: Accepted · - **Status:** Superseded by [0014](0014-….md) · Date: 2026-09-28 · Status: accepted · ## Status / Accepted
```
**Read the code:** Is the status written in prose the parser missed (then extend the parser with the shape, and record it)? **Finding when** no status line exists, or its word is not a lifecycle word. **Proof:** tier S — `adr-index.py . --list` shows the status after the fix.

### 2. One-way supersede link (KN-28)

```
BAD:   0001: "Superseded by 0003"; 0003 never mentions 0001 · 0008 "supersedes ADR-0007"; 0007 still Accepted
GOOD:  0001 ↔ 0014 both ways · "Supersedes: the chrome clause of [0016]" → 0016 marked Amended by 0028 (P-29)
```
**Read the code:** Is the supersede total or partial? Did tooling write one end only? **Finding when** one ADR names another as successor or predecessor and the other does not answer. **Proof:** tier S + V504 — both headers quoted; after the fix each names the other.

### 3. Superseded ADR cited as current (KN-28)

```
BAD:   CLAUDE.md: "HTTP follows ADR-0001."        (0001 superseded by 0014)
GOOD:  "Old HTTP was ADR-0001, replaced by ADR-0003" (P-14) · "ADR-0004 → ADR-0005" · `2026-10-13-x.md` (a date, P-30)
```
**Read the code:** Does the citing line also name the successor or mark history? Is the citing file loaded every session? **Finding when** a live line outside the ADR home and the index cites a superseded, deprecated or retired ADR as if it holds. **Proof:** tier S + V505 — the citation re-pointed to the successor; the engine is quiet on that line.

### 4. Chosen option absent (KN-28)

```
BAD:   0002 (Accepted): "We use `axios` for HTTP" — no manifest declares axios
GOOD:  a rejected option in "Considered options" (P-2) · a superseded ADR's choice (not checked)
```
**Read the code:** Was the decision reversed in code (a new ADR is due), not built yet (it is Proposed), or does the package live in another app's manifest? **Finding when** an Accepted ADR's chosen package is in no manifest or lockfile of the repo. Unconfirmed always: a process decision may name no package. **Proof:** V506 — the package present, or the ADR's status corrected by its owner.

## Proof

Oracles: [`proof-oracles.md`](../../../../snippets/proof-oracles.md) § Learning — V504 (reciprocal link both ways), V505 (home detected and the citation re-pointed), V506 (chosen option present). `adr-index.py . --only=<file>:<line>` fires before the edit and is quiet after it; `--list` is the before/after status table.

## Output

```
decision-record-scan — home docs/decisions (declared in ai/_decision-index.md) · 40 ADRs: accepted 37, superseded 2, retired 1

1. .claude/agents/api-architect.md:56 (+48 installed artifacts)                    [report-with-fix]   high
   §0 KN-29 — read or write ai/decisions/ while the ADRs live in docs/decisions; ai/decisions/ holds README + _template.
   Fix: the pack sources resolve the home (IR-3), ai/decisions/README.md points at docs/decisions/. One root cause, 49 sites.
2. docs/decisions/0016-one-tab-strip.md:3                                          [report-flagged]    low
   §2 KN-28 — ADR-0028 supersedes "the chrome clause" of 0016; 0016 has no back link. Fix: `Amended by ADR-0028`.

Verification: 3 candidates → 2 findings · 1 unconfirmed · 0 hardening · 0 pre-existing · 0 refuted

Class coverage:
  §0  KN-29 ADR home             checked (1 home, 1 second directory, 49 consumer sites → 2 rows)
  §1  KN-28 status               checked (40 parsed → 0)
  §2  KN-28 supersede links      checked (3 supersede pairs → 1 unconfirmed)
  §3  KN-28 cited as current     checked (3 dead ADRs → 0)
  §4  KN-28 chosen option        checked (37 accepted → 0)
```

`[report-with-fix]`: both ends quoted and the edit adds a link, re-points a citation or moves tooling to the home. `[report-flagged]`: partial supersede, or a chosen option that may be unbuilt. The coverage block lists **all five** classes on every run: `checked`, `N/A (no ADR directory)`, or `blocked (<reason>)`.

## False positives / precedents

The verifier reads [`precedents/learning.md`](../../../../snippets/precedents/learning.md) and [`refute-before-report.md`](../../../../snippets/refute-before-report.md) § 10. The ones this skill meets most: § 2 considered options, § 3 ADR Context sections, § 7 Proposed decisions, § 14 history-marked citations, § 29 partial supersede, § 30 dated file names.

## When to run

- On any diff that adds, edits or renames an ADR, edits the decision index, or edits a file that cites an ADR: the `/audit-knowledge` dispatch signal.
- Before `/promote-decision` writes an ADR: the home comes from this script, never a hard-coded directory.
- On every whole-repo `/audit-knowledge`, reporting all five classes.

## Halt conditions

- Do not report a row as confirmed until a verifier other than the run that found it has passed it through [`refute-before-report.md`](../../../../snippets/refute-before-report.md) (§ 10, with [`precedents/learning.md`](../../../../snippets/precedents/learning.md)). Under `/audit-knowledge` that happens in its verification phase; run standalone, apply the protocol as a separate second pass, tag those rows `self-verified`, and list the rest as Unconfirmed, Hardening, Pre-existing or Refuted. Never drop them.
- Never delete or rewrite an ADR's decision to close a row; never write a superseding ADR on the user's behalf.
- Never report §4 as a Finding: a chosen package that is absent is a question for the ADR's owner.
- Never write into the scanned repository; never print "decisions consistent" without the coverage block.
- Do not re-derive a class another skill owns (§ Boundaries). Cross-link it.

## Related

- `scripts/adr-index.py` (this skill): §0–§4, `--home`, `--list`, `--only`, `--classes`, `--self-test`; imports `kn_common.py` from the knowledge-ref-scan skill (its scripts directory) (`adr_home`, `adr_ids`, manifests). Stdlib only, `python3 -I` safe; nothing is written under the scanned root.
- `knowledge-fact-scan` §5 (index rows), `knowledge-ref-scan` §4 (ids with no file), `/setup-project-health` check 2 (missing ADRs). `/audit-knowledge` dispatches this skill; `/refresh-knowledge --from-audit` closes rows; `/promote-decision` reads the home from it.
- documentation `/add-adr` (when installed) owns the ADR template.

Files in this skill

  • SKILL.md17.1 KB
  • scripts/adr-index.py21.9 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.