adrpy-ai
ADR lifecycle CLI for humans and AI agents alike — JSON-only, no wizard, zero dependencies.
adrpy-ai manages Architecture Decision Records (ADRs) from the command line: create, approve, reject, undo, supersede, version, and revise decisions, check that a repository is consistent, and migrate legacy hand-written files into the tool's own format. Every command takes flags in and returns JSON out — no interactive prompts, ever — so it works identically whether you're typing it yourself or an AI coding agent is driving it through a shell tool. For people who prefer screens, the sibling project adrpy-tui puts a terminal interface on top of it.
Table of Contents
- Motivation and Benefits
- Features
- Installation
- Quick Start
- A terminal interface for people (
adrpy-tui) - Commands
- Checking a repository (
adrpy check) - One owner per working copy
- Using adrpy-ai with AI Coding Agents
- Installing the judgment layer (
adrpy-skills) - Configuration
- Adopting adrpy on an existing repository
- Architecture and Design Decisions
- Contributing
- License
Motivation and Benefits
Architecture decisions are worth keeping only while they stay true and findable. adrpy-ai keeps them as plain Markdown files in your repository, with a lifecycle the tool enforces, so the record never drifts into a folder of half-updated notes:
- No wizard, ever. Every command is fully driven by flags. Nothing waits for a keypress, so it's safe to script and safe for an agent to call without a human in the loop.
- JSON in, JSON out. Every response is a single JSON object on stdout (
{"success": true/false, "data"/"code": ...}), with a fixed, documented set of failure codes per command — no output your own tooling has to guess the shape of. A failure also carriesdetail, a human-readable explanation to show a person; decide oncode/data, not ondetail's wording. The same text is copied to stderr for terminal use, outside the contract. The exit code is 0 on success, 1 on a failure and 2 on a malformed call (usage-error,unknown-command). The one exception to JSON is--version(adrpy --version,adrpy-skills --version), which prints plain text for a person. - Self-documenting.
adrpy help <command>returns the exact same structured contract (arguments, types, failure codes) this README describes — the documentation and the code can't silently drift apart, because they're the same artifact. - Zero runtime dependencies.
pip install adrpy-ai(or install from source) pulls in nothing else. - A full lifecycle, not just file creation.
init,new,approve,reject,undo,supersede,version,revise,migrate,check,config,installconfig— the whole decision lifecycle, not a one-shot generator. - Validates before acting. Every lifecycle command (
new,approve,reject,undo,supersede,version,revise) first validates the whole repository — headers, numbering, family and supersede rules — and, if anything is broken, lists every problem with a repair hint and changes nothing.adrpy checkruns the same validation on its own, for a pre-commit hook or CI. Seedoc/lifecycle.md. - One owner per working copy. adrpy does no locking: git coordinates people, and one person (or agent) at a time runs adrpy on a given working copy. Each file write is atomic and a new file is never created over an existing one; beyond that, the tool detects an inconsistent repository and reports it, it does not prevent one. See One owner per working copy.
Features
- Create and evolve decisions:
new, thenapproveorreject,undo,version(a new major version),revise(a wording fix) andsupersede(a successor that replaces it) -- numbers, versions and revisions assigned by the tool. - A repository that stays consistent: every lifecycle command validates the whole repository first, and
adrpy checkdoes the same for a pre-commit hook or CI. - An index that is always true: every command that writes a decision, and
configafter a field write, regeneratesINDEX.mdin the decisions folder, one table of every decision with its title, state, scope and domain; anINDEX.mdyou wrote yourself is never replaced (ADR0013V01R02). - Adopt what you already have:
migrategives hand-written decision files a header in one run, keeping their names. - Your repository's own conventions: prefix, number and version widths, separator, case, header and status labels and template, all in
.adrpy.jsonand edited withadrpy config; a per-user default seeds every new repository. - A decision log next to the ADRs, for findings and trade-offs that are not architectural decisions (
adrpy log). - Built for AI coding agents: JSON on stdout, stable failure codes, a machine-readable
help, andadrpy-skillsto install the operating instructions for Claude Code, Cursor, GitHub Copilot or a genericAGENTS.md. - 11 languages for the header and status labels and the default template (
en-us,pt-br,de-de,es-es,fr-fr,it-it,ja-jp,ko-kr,nl-be,ru-ru,zh-cn); the decisions themselves can be written in any language. - Windows, macOS and Linux, Python 3.11 to 3.14.
Installation
Requires Python 3.11 or later, on Windows, macOS or Linux.
pip install adrpy-ai
adrpy help
It installs two commands and nothing else (no runtime dependencies): adrpy, and adrpy-skills (see Installing the judgment layer). As a command-line tool in its own environment, with pipx: pipx install adrpy-ai. Straight from GitHub, a branch or a commit, without cloning: pip install git+https://github.com/FRACerqueira/adrpy-ai.git.
The package is adrpy-ai; ADRpy on PyPI is an unrelated project. Don't install both in the same environment: on Windows and macOS their import folders (adrpy and ADRpy) are the same folder, and their files mix.
To install from a clone instead:
git clone https://github.com/FRACerqueira/adrpy-ai.git
cd adrpy-ai
pip install .
adrpy help
The source install needs a git clone: the version is read from git, so a folder from GitHub's "Download ZIP" does not install. On Windows, some file names under doc/ are long; if git clone reports "Filename too long", clone with git clone -c core.longpaths=true https://github.com/FRACerqueira/adrpy-ai.git.
To work on adrpy-ai itself (running the test suite), see Contributing.
Prefer screens to flags and JSON? See A terminal interface for people (adrpy-tui).
Quick Start
# Initialize a new ADR repository in the current structure
adrpy init --path .
# Create a new decision, status Proposed
adrpy new --path . --title "Use PostgreSQL for the primary datastore" --domain data --scope backend
# Approve it -- status becomes Accepted
adrpy approve --file doc/adr/ADR0001V01R01-use-postgre-sql-for-the-primary-datastore.md
# List every decision in the repository
adrpy explore --path .
Every call above returns JSON on stdout. For example, explore after the steps above returns:
{
"success": true,
"data": {
"decisions": [
{
"filename": "ADR0001V01R01-use-postgre-sql-for-the-primary-datastore.md",
"scheme": "current",
"number": 1,
"version": 1,
"revision": 1,
"title": "use-postgre-sql-for-the-primary-datastore",
"header": {
"is_valid": true,
"scope": "backend",
"domain": "data",
"state": "valid",
"status_create": "Proposed",
"status_update": "Accepted"
}
}
],
"consistency": {"errors": []},
"warnings": []
}
}
(Trimmed for readability — the real response includes a few more fields per decision.)
Only files with an ADR name are decisions: the configured prefix (ADR by default, any case), the number, a mandatory V version, an optional R revision, then the separator and the title — ADR0001V01R01-use-postgre-sql.md — plus a --NNN suffix on a successor. Any other .md in the decisions folder (a README, 0001-use-postgres.md, 2024-01-15-meeting.md) is ignored, unless migrationpattern describes it as a legacy name; check and explore warn about one whose name starts with a digit. A legacy name is a decision only while the repository is not adopted yet or when it has a header: once any file has a valid header migrate did not write (created by the tool, or copied by hand — from then on migrate no longer runs), a legacy name without a header is not a decision — every command ignores it, and check, explore and every lifecycle command name it in warnings. Headers migrate wrote do not end the adoption: after a partial run, the files left keep blocking until migrate finishes them. The exact rule is under "ADR names" in doc/lifecycle.md.
A terminal interface for people (adrpy-tui)
adrpy-tui is a sibling project: menus, forms, lists and previews on top of adrpy-ai, for people who would rather not type flags and read JSON. Every change still goes through adrpy -- the interface shows the exact command before it runs, and adrpy's rules are the only ones that apply. It installs adrpy-ai with it:
pip install adrpy-tui
adrpy-tui
A repository is the same for both: switch between them, or use both, at any time. Each adrpy-tui release is validated against one adrpy-ai series; its README says which.
Commands
| Command | Purpose |
|---|---|
init |
Initializes an ADR repository: writes .adrpy.json and creates the decisions folder. |
new |
Creates a new decision, status Proposed. |
approve |
Marks a Proposed decision Accepted. |
reject |
Marks a Proposed decision Rejected. |
undo |
Reverts a decision's Accepted/Rejected status back to Proposed. |
supersede |
Marks an Accepted decision Superseded and creates its successor. |
version |
Creates a new major version of an Accepted/Rejected decision. |
revise |
Creates a new revision (wording fix) of an Accepted/Rejected decision. |
migrate |
Adds an adrpy-compliant header to existing, hand-written decision files. |
explore |
Lists every decision file in the repository, on a best-effort basis. |
check |
Validates every decision in the repository and lists every inconsistency found. |
config |
Reads or updates an existing repository's own .adrpy.json. |
installconfig |
Reads or updates the per-user, install-level default config (seeds new repositories, supplies a migrate fallback). |
log |
Writes a decision-log entry -- the lighter-weight sibling of a formal ADR. It refuses while the log folder holds a .md that is not an entry, which check and explore warn about. |
help |
Lists every command, or describes one of them in full. |
Bare adrpy help (or running adrpy with no arguments at all) lists every command's name and a one-line summary only, plus a curated preview of the config values a fresh init on this machine would actually produce (defaults, sourced from this machine's own installconfig when one is set up, or the built-in default otherwise — not every field; template, migrationpattern, headerdisclaimer, folderlog and the 11 header-row labels are all left out of this quick-glance preview on purpose, adrpy installconfig/adrpy config return every field including those) -- kept short on purpose, since the full contract of all 15 commands at once is a lot to read. A specific command's full argument list, types, and every failure code it can return is available at any time via:
adrpy help <command>
adrpy help --full returns every command's full contract in one call, if that's genuinely what's needed. The same contract is also available as reference pages under doc/commands/, one per command, generated from describe() (a test fails when a page drifts from it). Which status each command can move a decision to, and what stops it, is on one page: doc/lifecycle.md.
Checking a repository (adrpy check)
adrpy check --path .
check validates the whole repository and writes nothing. It also warns, without failing, about a decision whose name is longer than the 234 bytes the tool can rewrite (renamed by hand, or written by another tool): the commands that change it refuse with filename-too-long until it is renamed. It warns the same way about a .md in the decisions folder that is a symbolic link (commands refuse to write through it, with target-is-a-link) or that leads outside the repository. It exits 0 with {"success": true, "data": {"decisions": <count>, ...}} when every rule holds, and 1 with repository-inconsistent otherwise; data.errors lists every broken rule, each with its code, file, related_files, detail (null when the code says it all) and a repair hint. The rules are listed in doc/lifecycle.md. Every lifecycle command runs the same validation first and refuses the same way, so a repository check accepts is one every command can act on.
As a git pre-commit hook (.git/hooks/pre-commit, made executable) — the JSON goes to /dev/null, the human-readable detail still reaches the terminal on stderr. check reads the working tree, not what is staged: to check what you commit, stash the unstaged changes and untracked files first (git stash push --keep-index --include-untracked, then git stash pop after the commit) or use a hook manager that does it for you:
#!/bin/sh
adrpy check --path . > /dev/null || {
echo "adrpy check failed; run 'adrpy check --path .' to see every error and its hint." >&2
exit 1
}
As a GitHub Actions workflow (.github/workflows/adr-check.yml):
name: ADR check
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install adrpy-ai
- run: adrpy check --path .
One owner per working copy
adrpy has no concurrency control, by design:
- Git coordinates people. Each person or agent works on their own clone or branch; adrpy never calls git. A merge that breaks a rule — two branches that each created
ADR0005, say — is exactly whatadrpy checkreports, with the hint for the repair. - One adrpy command at a time on a working copy. Don't run adrpy (or
adrpy-skills) commands in parallel on the same working copy: nothing locks it, and the last command to write a file wins. - What is still guaranteed. Every file write is atomic (a reader sees the old file or the new one, never a partial one), a new decision is never created over an existing file (
file-already-exists), and temp files left behind by an interrupted write are cleaned up later. - Detects, does not prevent. A repository left inconsistent — by a hand edit, a merge, parallel commands, or a multi-file write that stopped halfway — is refused by every lifecycle command until it is repaired, and reported by
adrpy check.
Using adrpy-ai with AI Coding Agents
adrpy-ai was designed for this from the start, not adapted to it afterward:
- Every response is a single, well-formed JSON object — safe to parse without scraping human-readable text.
- Failure codes are stable, documented strings (e.g.
repository-inconsistent,config-already-exists), not free-text messages an agent has to pattern-match. - Don't let two agents (or an agent and a person) run
adrpycommands in parallel on the same working copy — see One owner per working copy. adrpy help <command>is the same machine-readable contract an agent can fetch at runtime, instead of relying on documentation baked into its own training data (which can drift out of date).- No command ever blocks on a prompt. An agent driving
adrpythrough a shell tool never has to detect and answer an interactive question.
Working with an AI agent. To have an agent follow these rules without repeating them in every prompt, install the adrpy skill in the repository for your assistant: adrpy-skills install --skill adrpy --provider claude (or cursor, copilot, agentsmd; without --provider it writes files for every provider). It tells the agent to run adrpy help and adrpy check --path . before touching the decisions folder, to follow each error's hint, to change decision files only through the commands (never renaming, hand-writing or hand-editing them, and never removing a successor's --NNN suffix), and to run one command at a time. See Installing the judgment layer.
adrpy itself is deliberately mechanical: it manages the ADR/decision-log record, never the judgment (when a decision needs recording, when a hardening review is due, when to close a review cycle). That judgment layer ships separately, as adrpy-skills — see ADR0009V01 and doc/skills/.
Installing the judgment layer (adrpy-skills)
A separate console script, installed by the same pip install adrpy-ai — opt-in, and never called by adrpy itself. It installs three vendor-neutral skills for whichever AI coding assistants you use: adrpy (how an agent drives the CLI itself), decision-log and pre-release-audit:
adrpy-skills install --provider claude # every bundled skill, for one assistant
adrpy-skills install --skill decision-log --provider claude,cursor
adrpy-skills install # every bundled skill, every supported provider
adrpy-skills list # what's installed where, and whether any of it has drifted
Supported providers: claude (Claude Code, project or global scope), cursor, copilot (GitHub Copilot), and agentsmd (a generic AGENTS.md, editing only its own marked block). Every file adrpy-skills writes carries a content-hash marker, so a plain re-run after pip install --upgrade picks up updates safely, while anything you hand-edited since is left alone unless you pass --force. Full command reference: doc/skills/. How well an agent follows the skills depends on its model: they were tested with Claude Code, where a model at least as capable as Claude Sonnet 5 is recommended for any task that writes decisions; the other providers were not tested with a real agent (see "Which model to use" there).
Configuration
A repository's own settings (ADR numbering, naming scheme, header labels, status labels) live in .adrpy.json at its root -- a dotfile, hidden on Linux and macOS (ls -a lists it) -- edited via adrpy config. A new repository names its decisions with a 4-digit number, a 2-digit version and a 2-digit revision (ADR0001V01R01-...); lenseq, lenversion and lenrevision change that. For a new repository, adrpy init seeds those settings from, in order: an explicit --seed <file>, a per-user install-level default (adrpy installconfig, if one has been set up on this machine), or a built-in default. Status labels, the naming-scheme separator, the prefix and the header's fields label (headertablefields, which marks the header of every decision) can only be changed while doing so wouldn't break recognition of an already-written decision (see ADR0004) — in practice this means the four status labels, the separator, the prefix and headertablefields become permanently fixed the moment the repository has its first decision of any kind, while the legacy-scheme migrationpattern becomes permanently fixed only once the repository has its first migrated legacy-scheme decision (one with a valid header; until then it can also be changed or cleared with adrpy config --migrationpattern ""); adrpy help config documents the exact failure codes.
adrpy installconfig manages that per-user default directly — the same schema as a repository's own config, so it doubles as a way to keep every new repository on a machine consistent without repeating flags every time. Both init (while the machine has no install-level config) and installconfig also accept --language (e.g. pt-br), which seeds the built-in header/status labels and default template from a bundled language pack instead of the English defaults; it's a bootstrapping-only convenience — an already-initialized repository's own config has no equivalent flag, since its labels are already concrete values on disk, not something to re-derive from a language choice.
Adopting adrpy on an existing repository
Point adrpy at the repository and run
adrpy check --path .
A repository edited by hand, or merged from two branches, can hold a few states adrpy's rules do not allow. Every lifecycle command refuses the repository while one of them is there, so repair them by hand once, commit, and adrpy keeps the repository consistent from then on. Each error in data.errors names the file, the related files and the repair in its hint:
| State | Reported as | Repair (by hand) |
|---|---|---|
An older version still Superseded next to a newer version that is not Rejected |
superseded-not-live |
Move the Superseded cell to the live (latest) member, or set the newer member's Changed cell to Rejected. |
Two Superseded members in one family |
superseded-duplicate |
Keep the Superseded cell on the live member and clear it on the others (their successors then need their suffix or status repaired too). |
Two successors that are not Rejected naming the same predecessor |
multiple-live-successors |
Keep the one the predecessor's Superseded cell points at; set the others' Changed cell to Rejected, or remove them. |
A Superseded cell pointing at a successor that was rejected |
superseded-without-successor |
Clear the Superseded cell, or fix its number. |
A version or revision of a successor that was rejected, itself not Rejected |
rejected-successor-family-not-final |
Set its Changed cell to Rejected, or remove it. If the predecessor's Superseded cell still points at the rejected successor, clear it too (otherwise superseded-without-successor); the line then continues by superseding the predecessor again. |
A file with an ADR name but no header gets no-header; while no decision has a valid header that migrate did not write, adrpy migrate gives every such file a header in one run. migrate always needs a migrationpattern, set first with adrpy config --migrationpattern ... (config tolerates the no-header files for exactly this) or taken from the install-level config; it is needed even when every file already has an ADR name (the ADR name is read first). The pattern also makes a decision of every other name it matches — a dated note like 2024-01-15-meeting.md — while the repository is not adopted yet (once a decision has a valid header migrate did not write, a matched name without one is ignored and warned about instead), so choose one that matches nothing else in the folder. Preview what a pattern reads with adrpy explore --path . --migrationpattern <pattern> (migrationpattern_preview, written nowhere), then set it with adrpy config --migrationpattern (which writes the config: check then fails with no-header on each matched file until migrate runs; --migrationpattern "" backs out). A re-run of migrate after a partial one migrates the files still without a header. The migrationpattern syntax (N##:##T##[V##:##][R##:##][P##:##], with examples) is under "migrationpattern syntax" on the config page. Once any decision has a valid header migrate did not write, migrate refuses (already-tool-created-adrs-exist): give the remaining files a header by hand. A .md whose name is not an ADR name (a README, an index) is ignored.
The 12 lines at the top of every decision -- the status read from its label and the hidden <!-- Accepted --> marker after the date (a header without the marker is read from the label alone):
<!-- Do not edit or remove this comment, lines and table (1-12) -->
|Fields|Values|
|--|--|
|File title md|Use PostgreSQL|
|Version|01|
|Revision|01|
|Scope||
|Domain||
|Created|Proposed (2026-01-10) <!-- Proposed -->|
|Changed|Accepted (2026-01-12) <!-- Accepted -->|
|Superseded||
<!-- Do not edit or remove this comment, lines and table (1-12) -->
A file migrate brought in says so on line 2, |Fields|Values Migrated <!-- Migrated -->|, and may have blank Version and status cells.
Architecture and Design Decisions
This project records its own architectural decisions as it makes them:
doc/architecture.md— how the codebase is put together and why: module layout, request lifecycle, the single-owner model, configuration layering, and the decision lifecycle, with diagrams.doc/adr/— formal Architecture Decision Records, written using adrpy-ai itself (this project dogfoods its own tool), listed with their state, scope and domain in the index adrpy regenerates at each write.doc/decision-log/— the project's audit trail, written first for tooling and for AI agents calibrating a review: audit findings, documentation corrections, and deferred/accepted trade-offs, indexed from individual entries that are never hand-edited. To understand the design, read the ADRs.doc/decision-log-workflow.md— the step-by-step workflow for deciding whether something belongs in an ADR or in the decision log, and how to write either one.doc/skills/— howadrpy-skillsinstalls that same judgment layer for AI coding agents, and how it decides what to write, per provider.
If you're evaluating this project's engineering rigor rather than just its feature set, doc/adr/ and doc/decision-log/ are the primary evidence, not this README.
Contributing
Contributions are welcome. See CONTRIBUTING.md for development setup, the test/verification discipline this project expects of every change, and how to submit a pull request. Please also read the Code of Conduct.
Found a security issue? See SECURITY.md instead of opening a public issue.
Released changes are tracked in CHANGELOG.md.
License
MIT — Copyright (c) 2026 Fernando Cerqueira.
Metadata
Release files for adrpy-ai 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| adrpy_ai-0.1.0.tar.gz | 553.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| adrpy_ai-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 794.0 kB
Release files / adrpy_ai-0.1.0.tar.gz
| Download URL | adrpy_ai-0.1.0.tar.gz |
|---|---|
| Size | 553.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8b1ed8644d2bdccbbf33c733741df455bc74e524069b0e60c3e5ff8feb5f177a
|
|
BLAKE2b-256 checksum How to use checksums |
6f86ee9e20e2f445aa58e44d88db6fb520a0dae2a1d9caffb8c552917d9a482b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / adrpy_ai-0.1.0-py3-none-any.whl
| Download URL | adrpy_ai-0.1.0-py3-none-any.whl |
|---|---|
| Size | 240.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8dc926877e2844caed77bb8b495bfb34c73722cc236d655cd3ba4bed92168601
|
|
BLAKE2b-256 checksum How to use checksums |
160b1fa60b1ed9b9a3d3d07a6f53b607e72225a1977fcad2c41a5d5d926e5666
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log