Skip to content

Command reference

The adr binary (published as @adrkit/cli) is a small set of deterministic commands. Run adr with no arguments or adr --help for task-oriented command discovery and common workflows. Run any command with --help, or adr help <command>, for its arguments, options, examples, and exit codes. Compact synopsis:

adr lint [paths...] [--json] [--dir docs/adr]
adr migrate --from madr [--dir docs/adr] [--dry-run] [--rename] [--json]
adr new <title> [--status draft] [--dir docs/adr] [--json]
adr graph [--dir docs/adr] [--format auto|terminal|dot|json|mermaid]
[--focus ID] [--kind supersedes|relatesTo|conflictsWith]
adr explain <path> [--dir docs/adr] [--json] [--as-of <date|ref>]
adr check <files...> [--dir docs/adr] [--json]
adr evaluate <proposal-path> --snapshot <bundle.json> --date YYYY-MM-DD [--json] [--dir docs/adr]
adr queue [--dir docs/adr] [--as-of YYYY-MM-DD] [--format auto|terminal|markdown|json]
adr approve <id> --by <identity> [--dir docs/adr] [--json]
adr object <id> --by <identity> --summary <text> [--dir docs/adr] [--json]
adr resolve <id> --objection <n> --by <identity> [--dir docs/adr] [--json]
adr accept <id> --by <identity> [--dir docs/adr] [--json]
adr completion <bash|zsh|fish>
adr help [command] Show help, or help for one command
adr --version Print the @adrkit/cli version
adr --color <mode> <command> [options] Colorize human-readable output

Running adr with no arguments prints the top-level help to stdout and exits 0. The help groups commands by the task they perform, includes common workflows, and points to this reference. adr help <command> and adr <command> --help print the same focused command help.

Usage errors stay on stderr and exit 2. Instead of appending the full global help, a command error prints that command’s usage and points to adr help <command>. Near-miss command names and long options suggest the closest documented spelling. The closed value sets for new --status, graph --format, migrate --from, queue --format, and completion <shell> do the same:

Error: Unknown option "--jsn". Did you mean "--json"?
Usage: adr lint [paths...] [options]
Run 'adr help lint' for more information.

Suggestions are conservative: an unrelated command, option, or value is rejected without guessing.

--color auto|always|never is a global option. Put it before the command for the clearest invocation:

Terminal window
adr --color always lint

Both --color always and --color=always are recognized anywhere before the -- argument terminator. After --, --color is an ordinary positional value.

Mode Meaning
auto Default. Color each human-readable output stream only when that stream is a TTY and NO_COLOR is unset.
always Force color for human-readable output, including redirects and pipes; this explicit choice overrides NO_COLOR.
never Disable color.

Stdout and stderr are detected independently, so redirecting one does not change the other. Color changes presentation only: exit codes and the stdout/stderr split are unchanged. Machine-oriented output remains ANSI-free even under --color always, including JSON, DOT, Mermaid, generated completion scripts, and the canonical queue Markdown format.

Validate the ADR corpus. With no paths, every discoverable record under --dir is checked.

Option Meaning
--dir <path> ADR corpus directory (default: docs/adr)
--json Emit { checked, findings } as JSON
--help Show help and exit

Exit codes: 0 no error findings · 1 one or more error findings · 2 usage error (invalid invocation or unreachable corpus directory).

Migrate a MADR corpus in place, adding adrkit frontmatter and leaving the body untouched. One-way and non-destructive; round-trip sync is unsupported (ADR-0008).

Option Meaning
--from madr Source format (required; only madr is supported)
--dir <path> ADR corpus directory (default: docs/adr)
--dry-run Report what would change without writing
--rename Rename each migrated file to <id>-<slug>.md so corpus discovery can see it
--json Emit the migration result as JSON
--help Show help and exit

Exit codes: 0 migration ran (findings are reported but do not fail the run) · 2 usage error (missing or unsupported --from, unknown flag, positional argument, or unreachable corpus directory).

Scaffold a new ADR record.

Option Meaning
--status <status> Initial status (default: draft)
--dir <path> ADR corpus directory (default: docs/adr)
--json Emit { id, path } as JSON
--help Show help and exit

Exit codes: 0 created · 1 refused to overwrite an existing file · 2 usage error.

Render the decision graph (supersedes, relatesTo, conflictsWith) for the corpus.

Option Meaning
--dir <path> ADR corpus directory (default: docs/adr)
--format auto|terminal|dot|json|mermaid Output format (default: auto)
--focus <id> Keep one ADR, its incident relationships, and their endpoints
--kind <kind> Keep supersedes, relatesTo, or conflictsWith; repeat to include more kinds
--help Show help and exit

auto selects the compact terminal instrument only when stdout is a TTY. Pipes, redirects, captured subprocesses, and CI continue to receive deterministic DOT, so existing adr graph | ... workflows do not change. Explicit formats always win.

The terminal view shows status and relationship counts. Sparse and focused graphs include their relationship map; dense corpora show the most connected decisions and point to --focus/--kind instead of drawing an unreadable hairball. Large sparse and focused views are bounded as well; select another kind or a machine format for the complete edge set.

Terminal window
adr graph
adr graph --focus 0014
adr graph --kind supersedes
adr graph --kind supersedes --kind conflictsWith --format json
adr graph --format mermaid > decisions.mmd
adr graph --format dot | dot -Tsvg > decisions.svg

DOT, Mermaid, and JSON are deterministic and ANSI-free. The JSON envelope stays { nodes: [{ id, title, status }], edges: [{ from, to, kind }] }.

Graph still emits the complete projection of valid records when the corpus also contains invalid records. Those error findings go to stderr.

Exit codes: 0 rendered without corpus errors · 1 rendered with one or more corpus errors · 2 usage error (invalid invocation, unreachable corpus directory, unknown focus id, or invalid format/kind).

Report which decisions govern one repo-relative path, and why.

A decision reaches a path in two directions, and explain keeps them apart:

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:3 (@adr 0012)

via is the record reaching out through its own affects pattern. declared by is the file reaching in with an @adr marker — see inbound @adr markers below.

Option Meaning
--dir <path> ADR corpus directory (default: docs/adr)
--as-of <date|ref> Report standing as of a past date — see time travel
--json Emit { path, governedBy, governing, activeProposals, history, markers, findings }, plus asOf when --as-of is given
--help Show help and exit

Exit codes: 0 explained · 1 corpus has error findings · 2 usage error (invalid invocation, unreachable corpus directory, or an --as-of value that is neither a date nor a resolvable git ref).

“Which decisions governed this file when this code was written?” 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
adr explain src/auth/session.ts --as-of a1b2c3d
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 bracketed status is the record’s status now; the window underneath is what placed it on that date. Both are true at once, and in --json they appear side by side as bucket (present) and standing (as of).

  • A date is tried before a git ref. YYYY-MM-DD or an ISO datetime with an explicit timezone is read as a date. Anything else is resolved with git rev-parse --verify <ref>^{commit} in the current directory and dated by that commit’s committer date (%cI). A tag named 2026-03-01 therefore reads as a date rather than as that tag. A timezone-less datetime is rejected outright, in the same words adr queue uses.
  • Windows are half-open, [opens, closes). The successor owns its own start day, so exactly one record along a supersession chain is in force on any date.
  • deprecated is reported as undetermined. The schema allows supersededBy only on superseded, so a deprecated record records no date it stopped governing and the corpus cannot say. A rejected record is history on every date, because it was in force on none. Both come with an advisory finding that never changes the exit code.
  • --as-of re-dates the corpus, never the working tree. Which decisions reach the path is still read from today’s records and today’s file contents; only their standing is re-dated. Reading file contents at a past ref is a strictly larger contract and is deliberately not attempted. Because the evidence lines (via path:, declared by …) are the one thing that is not re-dated, the view says so in a note under its header.
  • A marker accurate on that date is not stale. @adr 0007 naming a record that was in force on 2026-03-01 raises no stale-marker warning under --as-of 2026-03-01. The present-tense diagnostic is unchanged.
  • It is additive. Without the flag nothing changes. With it, --json gains an asOf block carrying governing, activeProposals, history, notYetRecorded, and undetermined, each decision annotated with its window and standing; the present-tense top-level keys keep their meaning (ADR-0039).

A record declares the paths it governs with affects. That pattern has to be broad enough to cover a subsystem, and breadth is what costs an agent context. An inbound marker lets affects stay narrow: any other file opts in with one line, at zero cost until something reads that file.

// @adr 0012
export function syncOnce() { … }
  • The grammar is @adr followed by whitespace and an ADR id (0012, or payments:0012 for a federated log). A comma continues a list — @adr 0012, 0013. A bare space ends it, so @adr 0012 1234567 is one declaration followed by a number, not two declarations.
  • Any language. adrkit does not parse your file. A marker must be on its own comment line: after optional whitespace, one of //, /*, {/*, *, #, --, ;, %, <!--, """, ''' begins the line and @adr is the comment’s first content. A trailing } // @adr 0012 does not declare the file.
  • Not inside a fenced block. A marker between backtick or tilde fences is an example of the syntax rather than a use of it, so documenting the feature does not make your documentation governed by the decision you used as the example. A fence closes only on a line of at least as many of the same character and nothing else, and an unclosed fence runs to the end of the scanned window (ADR-0023).
  • In markdown, use a markdown comment. In .md, .mdx, and .markdown only <!-- and {/* declare. # is a heading and * is a list bullet: they render, so they are content rather than comments, and # @adr 0012 does not declare the file. Every other introducer above belongs to a source language and is ignored there.
  • At most the first 8192 bytes of a file are scanned, and the scan stops at the last complete line inside that bound — --json reports the extent it reached. A marker is a claim that the file lives under a decision; a mention far down the file is prose about that decision.
  • At most 64 declarations per file are retained in physical/source order. --json reports the exact total and omitted count, and human output says when declarations were omitted.
  • Nothing enters the record. The edge is discovered when you run explain or check. There is no schema change and no new affects matcher type (ADR-0022, which supersedes ADR-0021).

A marker naming a record the corpus does not have is a dangling-marker finding at warn. A log-qualified marker is marker-unresolvable at info — inert against this corpus, not broken. A marker naming a superseded, rejected, or deprecated record is a stale-marker warning. It still binds to that historical record, and a superseded chain names its terminal live successor when one can be resolved; adrkit never silently substitutes the successor. None of these marker findings changes the exit code.

The --json output reports what was actually scanned, so an empty result never has to be guessed at:

"markers": {
"state": "scanned",
"windowBytes": 8192,
"scannedBytes": 1582,
"fileBytes": 1582,
"truncated": false,
"declarationLimit": 64,
"totalDeclarations": 1,
"omittedDeclarations": 0,
"declared": [{ "ref": "0012", "line": 3 }]
}

state is scanned, absent (no such file), unreadable (not a regular file, or permission denied), or out-of-tree. truncated is true when the file is longer than the header window, so a marker below it was never seen — and truncated: false is what makes declared: [] mean “nothing declares this path” rather than “we stopped reading first”.

scannedBytes and fileBytes report the observed scan extent. When truncated is true, use Math.max(0, fileBytes - scannedBytes) to estimate the unscanned remainder; the values are measured separately and may differ if the file changes during the scan.

In governedBy, a pattern match carries firedMatchers and a file declaration carries declaredBy; declaredBy is omitted entirely when nothing declared the record.

<path> is repo-relative, the same contract affects patterns are matched against. Absolute and traversing paths are out-of-tree. Every symlink is refused as unreadable before its target is resolved, including symlinked parent directories and safe in-tree symlinks, so a fork PR cannot probe target existence or permissions.

adr check and the governing-decisions Action scan markers from the changed paths before calling the pure resolver. They normalize, deduplicate, and sort paths; scan the first 3,000 with at most 16 reads in flight; and expose exact absent and skipped paths through markerScan. Paths beyond the cap still take part in affects matching, but their markers are not read. A non-blocking marker-scan-capped warning makes that loss explicit.

After reads complete, the batch retains at most 10,000 declarations in code-unit path then physical/source order. markerScan.declarations reports exact total, retained, per-file-omitted, and batch-omitted counts. Any overflow collapses into one marker-declarations-capped warning rather than one finding per dropped declaration.

The cap is GitHub’s own changed-file ceiling. The Action refuses to evaluate a pull request whose file list hit that ceiling, so every diff it does answer is scanned completely and only a local adr check can reach the cap.

Report the decisions governing a set of changed files, and validate any changed records. Deterministic and provider-agnostic — the primitive the CI Action builds on.

Option Meaning
--dir <path> ADR corpus directory (default: docs/adr)
--json Emit the CheckOutcome as JSON
--help Show help and exit

Exit codes: 0 ok · 1 a changed record has an error finding · 2 usage error (invalid invocation or unreachable corpus directory).

Marker declarations render as declared by, separately from record-authored via matchers. --json includes markerScan state counts and exact unavailable, truncated, and skipped path lists. Marker-derived information never changes the exit code; only validation errors on changed ADR records do.

Run the deterministic evaluator over one proposal against an offline snapshot bundle. The evaluator routes; it never approves, persists, or writes. There is no --write.

Option Meaning
--snapshot <path> Snapshot bundle (required)
--date <date> Evaluation date, YYYY-MM-DD (required)
--dir <path> ADR corpus directory
--json Emit the evaluation report as JSON
--help Show help and exit

Exit codes: 0 evaluated (including warn/info/inert and escalation) · 1 the proposal was returned on a rubric error · 2 usage error, unreachable corpus directory, or malformed snapshot bundle.

Emit the ARB operations queue report for the local ADR corpus to stdout — a read-only, deterministic projection of the corpus’s review metadata. Identical inputs produce byte-for-byte identical output. See Use in CI for the managed-issue Action.

Option Meaning
--dir <path> ADR corpus directory (default: docs/adr)
--as-of <date> UTC calendar date for SLA computation (default: today, UTC). Accepts YYYY-MM-DD or an ISO datetime with an explicit timezone (e.g. 2026-01-08T00:00:00Z); timezone-less datetimes are rejected.
--format auto|terminal|markdown|json Output format (default: auto — the terminal view on a TTY, the canonical Markdown report when piped, redirected, or captured)
--help Show help and exit

Exit codes: 0 report, no error findings · 1 report emitted with error-severity corpus findings · 2 usage error (invalid flag/value or unreachable corpus directory).

In a terminal the queue prints a width-aware list: each item’s SLA state and days to its deadline, its tier, approvals against quorum, objections, and the next step — either the adr accept command to run or the reason acceptance is blocked. Anything that captures stdout still receives the Markdown report byte for byte, so CI, the managed-issue Action, and agents see no change. Pass --format terminal or --format markdown to choose explicitly.

Accept a proposed record as its named ratifier. It writes, as do new, migrate, and the review commands below (ADR-0044, accepted).

Terminal window
adr queue # see what is waiting
adr accept 0044 --by @octocat # ratify one record

It sets status: accepted, provenance.ratifiedBy, and review.decidedAt (the current UTC time, with seconds), and changes no other line of the file. Nothing is committed: review the diff and open a pull request, as you would for any record. --by is required and is never inferred from git or the environment, because naming who ratified the decision is the point.

It refuses, exits 1, and leaves the file untouched when the record is not proposed, has an unresolved objection, has fewer distinct approvers than its review.quorum (@bob and @Bob count once), already has lint errors, or would not be a valid accepted record — for example, one with no deciders. --by never counts as an approval.

Option Meaning
<id> ADR id as shown by adr queue; 44 and ADR-0044 also resolve to 0044
--by <identity> Required. @handle, team:slug, or an email address
--dir <path> ADR corpus directory (default: docs/adr)
--json Emit { id, path, status, ratifiedBy, decidedAt }
--help Show help and exit

Exit codes: 0 accepted · 1 refused, record unchanged · 2 usage error (missing or malformed --by, unknown id, or unreachable corpus directory).

Record review state on a proposed record under your identity (ADR-0051, proposed). adr queue shows the resulting approval and objection counts, and adr accept reads them.

Terminal window
adr approve 0044 --by @octocat # add an approval
adr object 0044 --by @hubot --summary "Needs a load test first" # raise an objection
adr resolve 0044 --objection 1 --by @hubot # resolve your own objection
  • adr 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, with no control characters, and is always written quoted.
  • adr resolve sets resolved: true on objection n, counting from 1 in file order. Only the objector may resolve an objection: --by must equal its by.

Each changes only the list it owns and no other line, keeps a flow list ([...]) a flow list, and commits nothing. A repeat writes nothing and exits 0: approving twice, raising the same open objection twice, or resolving a resolved objection. --by is required and is never inferred, and identities compare case-insensitively: @Bob after @bob is the same approval, and the objector may resolve under either spelling.

Option Meaning
<id> ADR id as shown by adr queue; 44 and ADR-0044 also resolve to 0044
--by <identity> Required. @handle, team:slug, or an email address
--summary <text> adr object only. Required, one line, at most 500 characters, no control or invisible characters
--objection <n> adr resolve only. Required, a whole number from 1
--dir <path> ADR corpus directory (default: docs/adr)
--json Emit { id, path, by, changed, approvals } (approve) or { id, path, by, changed, objection } (object, resolve)
--help Show help and exit

Exit codes: 0 written, or already in that state · 1 refused, record unchanged (not proposed, lint errors, no such objection, not the objector, or a layout it cannot edit safely) · 2 usage error (missing or malformed --by, --summary, or --objection, unknown id, or unreachable corpus directory).

Write a deterministic completion script for Bash, Zsh, or Fish to stdout:

Terminal window
adr completion <bash|zsh|fish>

The generated definitions cover top-level commands, each command’s documented options, and closed value sets such as --format and --status. They register both adr and the adrkit binary alias where the shell format permits.

Argument or option Meaning
<shell> Required; one of bash, zsh, or fish
--help Show help and exit

Exit codes: 0 script emitted · 2 usage error (missing or unsupported shell).

Shells commonly lazy-load completion definitions by filename. Install the same generated script under both the adr and adrkit names so completion works regardless of which binary you type first.

Terminal window
mkdir -p ~/.local/share/bash-completion/completions
adr completion bash > ~/.local/share/bash-completion/completions/adr
cp ~/.local/share/bash-completion/completions/adr \
~/.local/share/bash-completion/completions/adrkit

This location is loaded by bash-completion. Reload the shell after installing the files. For only the current shell, source the generated script directly:

Terminal window
source <(adr completion bash)
Terminal window
mkdir -p ~/.zsh/completions
adr completion zsh > ~/.zsh/completions/_adr
cp ~/.zsh/completions/_adr ~/.zsh/completions/_adrkit

Add the directory to fpath before your existing compinit setup, then reload the shell:

Terminal window
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit
compinit
Terminal window
mkdir -p ~/.config/fish/completions
adr completion fish > ~/.config/fish/completions/adr.fish
ln -sf ~/.config/fish/completions/adr.fish \
~/.config/fish/completions/adrkit.fish

Copying adr.fish to adrkit.fish works too. Fish discovers either file automatically; no shell restart is required.

adr help prints the global synopsis; adr help <command> prints one command’s help (identical to adr <command> --help). adr --version (or -V) prints the @adrkit/cli version. Both exit 0.