Git-native architecture decision record tooling from adrkit.
Use the package name for zero-install runs. The published binary is adr, but a
bare npx adr resolves an unrelated npm package:
npx @adrkit/cli lintFor project use, add it as a dev dependency and invoke it through a script:
npm install --save-dev @adrkit/cli # or: bun add --dev @adrkit/cli
npm pkg set scripts.adr=adr
npm run adr -- lintThat keeps you on the installed local binary instead of whatever npx adr might
download from the registry.
You can also install it globally:
npm install -g @adrkit/cli
adr lintEvery install also provides adrkit, an identical alias for the same binary.
Prefer adrkit in CI, Makefiles, and agent instructions because its name is
unambiguous:
adrkit lintDo not use npx adrkit or bunx adrkit. There is no unscoped adrkit
package, so those forms would resolve against the registry rather than the
published @adrkit/cli package.
Human-readable CLI output is TTY-aware: it stays ANSI-free when redirected or
piped, honors NO_COLOR, and can be forced with --color auto|always|never.
The binary includes:
newlintgraphexplaincheckqueueapproveobjectresolveacceptmigrate --from madrcompletionevaluate
Run adr --help for the command list, adr help <command> for one command's
flags, and adr --version to print the installed version.
adr queue lists every proposed record with its SLA state, deadline,
approvals, and objections. In a terminal it prints a list sized to the window,
and each item ends with the next step: the command to accept it, or the reason
it cannot be accepted yet. Piped, redirected, or captured output is the
deterministic Markdown report, unchanged; --format json emits QueueReport
v1, and --format terminal|markdown choose explicitly.
adr queue
adr accept 0044 --by @octocatadr accept sets status: accepted, provenance.ratifiedBy, and
review.decidedAt (now, UTC, with seconds), and changes no other line. It
commits nothing. It refuses, and leaves the file untouched, when the record is
not proposed, has an unresolved objection, has fewer approvals than its
review.quorum, or would not be a valid accepted record. --by is required and
is never inferred. Exit codes: 0 accepted, 1 refused, 2 usage error.
adr approve 0044 --by @octocat
adr object 0044 --by @hubot --summary "Needs a load test first"
adr resolve 0044 --objection 1 --by @hubotadr approve adds --by to review.approvals. adr object appends
{ by, summary, resolved: false } to review.objections; the summary is one
line of at most 500 characters. adr resolve sets resolved: true on
objection n (counting from 1, in file order), and only the objector may
resolve it. Each acts only on a proposed record, changes no other line,
commits nothing, and leaves the file untouched on a repeat (approving twice,
the same open objection twice, resolving a resolved objection). --by is
required and is never inferred, and identities compare case-insensitively, so
@Bob after @bob is the same approval and quorum counts distinct people. Exit codes match adr accept: 0 written or
unchanged, 1 refused, 2 usage error; --json is available on all three.
adr queue's approval and objection counts follow these writes
(ADR-0051, proposed).
adr graph chooses its default at the stdout boundary: an interactive terminal
gets a compact status and relationship view, while a pipe, redirect, or captured
subprocess keeps receiving deterministic Graphviz DOT. Pin a channel whenever
another tool consumes the output:
adr graph # terminal view on a TTY; DOT in a pipe
adr graph --focus 0014 # one ADR and its direct neighborhood
adr graph --kind supersedes # one relationship kind
adr graph --format mermaid > decisions.mmd
adr graph --format dot | dot -Tsvg > decisions.svg
adr graph --format json > decisions.json--kind is repeatable and accepts supersedes, relatesTo, and
conflictsWith. Explicit --format terminal|dot|json|mermaid always overrides
TTY selection. DOT, Mermaid, and JSON remain ANSI-free even with forced color.
Large sparse and focused terminal views are bounded; use another --kind or a
machine format for the complete edge set.
If one or more corpus records are invalid, graph still emits the complete
projection of every valid record, writes the error findings to stderr, and
exits 1. Exit 0 means rendered without corpus errors; exit 2 remains a
usage or unreachable-directory error.
adr completion <bash|zsh|fish> prints a deterministic completion script to
stdout. The generated script registers both adr and adrkit where the shell
format permits.
adr completion bash > ~/.local/share/bash-completion/completions/adr
cp ~/.local/share/bash-completion/completions/adr ~/.local/share/bash-completion/completions/adrkit
adr completion zsh > ~/.zsh/completions/_adr
cp ~/.zsh/completions/_adr ~/.zsh/completions/_adrkit
adr completion fish > ~/.config/fish/completions/adr.fish
cp ~/.config/fish/completions/adr.fish ~/.config/fish/completions/adrkit.fishIf you prefer symlinks, point both Fish entry points at the same generated file:
ln -sf ~/.config/fish/completions/adr.fish ~/.config/fish/completions/adrkit.fishadr explain <path> and adr check <files...> resolve decisions in both
directions. A record declares the paths it governs with affects; a source
file can declare the decision it lives under with a comment marker:
// @adr 0012
export function syncOnce() { /* ... */ }$ adr explain src/services/sync/retry.ts
Decisions governing src/services/sync/retry.ts:
0009 [accepted] Resolve affects deterministically
via path: src/services/sync/**
0012 [accepted] Bind catalog entities to owned paths
declared by src/services/sync/retry.ts:1 (@adr 0012)
Marker scanning is intentionally narrow:
- only the first 8192 bytes of a file are considered
- the scan stops at the last complete line inside that window
- at most the first 64 declarations per file are retained
- the marker must be the first content on a dedicated comment line
- lines inside fenced code blocks are ignored
- markdown files (
.md,.mdx,.markdown) accept only<!--and{/*
Nothing is written back to the record.
A marker that names a superseded, rejected, or deprecated record still resolves
to that historical record and emits an advisory stale-marker warning. For a
superseded chain, the warning names the terminal live successor when one can be
resolved; adrkit never silently substitutes it. The warning does not affect the
exit code.
Under --as-of, a marker naming a record that was in force on that date is not
stale — it was an accurate declaration then. The present-tense warning is
unchanged.
adr explain <path> --as-of <date|ref> answers which decisions governed a path
on a past date. Every record carries a date, and a superseded record names its
successor, so a valid-time window is derivable with no schema change: it opens on
the record's own date and closes on its immediate successor's.
$ adr explain src/auth/session.ts --as-of 2026-03-01
As of 2026-03-01
Note: matchers and @adr markers are read from today’s corpus and working tree; only standing is re-dated.
Decisions governing src/auth/session.ts as of 2026-03-01:
0007 [superseded] Use JWT sessions (superseded by 0019)
in force 2026-01-15 → 2026-06-01 (closed by 0019)
via path: src/auth/**
Not yet recorded as of 2026-03-01:
0019 [accepted] Use opaque server sessions
recorded 2026-06-01
The status in brackets is the record's status now; the window underneath is
what placed it. Both are true, and a reader needs both. The note under the header
is there because the evidence lines (via path:, declared by …) are read from
today's corpus and today's file — they are the one thing --as-of does not
re-date.
- The value is a date first, a git ref second.
YYYY-MM-DDor an ISO datetime with an explicit timezone is read as a date; anything else is resolved withgit rev-parse --verify <ref>^{commit}in the current directory and dated by that commit's committer date. A tag named2026-03-01therefore reads as a date, not as that tag. - Windows are half-open. The successor owns its own start day, so exactly one record along a supersession chain is in force on any given date.
deprecatedis reported asundetermined, not guessed at. The schema allowssupersededByonly onsuperseded, so a deprecated record records no date it stopped governing. Saying so is the honest answer; arejectedrecord is history on every date, because it was in force on none.--as-ofre-dates the corpus, never the working tree. Which decisions reach the path is still read from today's records and today's file contents. Reading file contents at a past ref is a larger contract and is not attempted.- It is additive. Without the flag, output is unchanged. With it,
--jsongains anasOfblock and the present-tensegoverning,activeProposals, andhistorykeys keep their meaning (ADR-0039).
In --json, pattern matches appear in firedMatchers and file declarations in
declaredBy. explain --json includes a single-file markers block plus
scannedBytes, fileBytes, and exact retained/omitted declaration counts.
check --json includes a markerScan report with state counts; exact
unavailable, truncated, and skipped paths; and aggregate declaration counts.
Multi-file scans are capped at 3,000 normalized paths with 16 concurrent reads, and retain at most the first 10,000 declarations in code-unit path then source order. Skipped paths and declaration overflow each collapse to one bounded warning; neither they nor marker claims can change the exit code.
The published ESM CLI runs on Node.js 22 or newer; development in the adrkit repository uses Bun.
See also @adrkit/mcp, a local read-only Model Context
Protocol server that exposes the same corpus to coding agents.
Documentation: https://adrkit.dev
License: Apache-2.0