Skip to main content
adrpy-ai icon

adrpy-ai

CI PyPI Downloads License: MIT

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

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 carries detail, a human-readable explanation to show a person; decide on code/data, not on detail'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 check runs the same validation on its own, for a pre-commit hook or CI. See doc/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, then approve or reject, undo, version (a new major version), revise (a wording fix) and supersede (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 check does the same for a pre-commit hook or CI.
  • An index that is always true: every command that writes a decision, and config after a field write, regenerates INDEX.md in the decisions folder, one table of every decision with its title, state, scope and domain; an INDEX.md you wrote yourself is never replaced (ADR0013V01R02).
  • Adopt what you already have: migrate gives 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.json and edited with adrpy 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, and adrpy-skills to install the operating instructions for Claude Code, Cursor, GitHub Copilot or a generic AGENTS.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 what adrpy check reports, 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 adrpy commands 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 adrpy through 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/ — how adrpy-skills installs 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)

Source distribution for adrpy-ai 0.1.0
File Size Uploaded
adrpy_ai-0.1.0.tar.gz 553.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for adrpy-ai 0.1.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page