
Adr Skill
- 1.7k installs
- 26k repo stars
- Updated August 5, 2026
- vercel/ai
adr-skill provides documented workflows for Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows. Use when you need to propose, write, update, accept/reject, dep
About
The adr-skill skill create and maintain Architecture Decision Records ADRs optimized for agentic coding workflows Use when you need to propose write update accept reject deprecate or supersede an ADR bootstrap an adr folder and index consult existing ADRs before implementing changes or enforce ADR conventions This skill uses Socratic questioning to capture intent before drafting and validates output against an agent-readiness checklist ADR Skill Philosophy ADRs created with this skill are executable specifications for coding agents A human approves the decision an agent implements it The ADR must contain everything the agent needs to write correct code without asking follow-up questions This means Constraints must be explicit and measurable not vibes Decisions must be specific enough to act on use PostgreSQL 16 with pgvector not use a database Consequences must map to concrete follow-up tasks Non-goals must be stated to prevent scope creep The ADR must be self-contained no tribal knowledge assumptions The ADR must include an implementation plan which files to touch which patterns to follow which tests
- Constraints must be explicit and measurable, not vibes
- Decisions must be specific enough to act on ("use PostgreSQL 16 with pgvector" not "use a database")
- Consequences must map to concrete follow-up tasks
- Non-goals must be stated to prevent scope creep
- The ADR must be self-contained - no tribal knowledge assumptions
Adr Skill by the numbers
- 1,715 all-time installs (skills.sh)
- +151 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #312 of 2,203 Security skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
adr-skill capabilities & compatibility
- Capabilities
- constraints must be explicit and measurable, not · decisions must be specific enough to act on ("us · consequences must map to concrete follow up task · non goals must be stated to prevent scope creep · the adr must be self contained no tribal knowl
- Use cases
- documentation
What adr-skill says it does
# ADR Skill ## Philosophy ADRs created with this skill are **executable specifications for coding agents**.
A human approves the decision; an agent implements it.
npx skills add https://github.com/vercel/ai --skill adr-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.7k |
|---|---|
| repo stars | ★ 26k |
| Last updated | August 5, 2026 |
| Repository | vercel/ai ↗ |
How do I use adr-skill for the task described in its SKILL.md triggers?
Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows. Use when you need to propose, write, update, accept/reject, deprecate, or supersede an ADR; bootstrap.
Who is it for?
Teams invoking adr-skill when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows. Use when you need to propose, write, update, accept/reject, deprecate, or supersede an ADR; bootstrap an adr folder and ind
What you get
Step-by-step guidance grounded in adr-skill documentation and reference files.
- ADR markdown files
- adr folder index
- Agent-readiness validated decision record
Files
ADR Skill
Philosophy
ADRs created with this skill are executable specifications for coding agents. A human approves the decision; an agent implements it. The ADR must contain everything the agent needs to write correct code without asking follow-up questions.
This means:
- Constraints must be explicit and measurable, not vibes
- Decisions must be specific enough to act on ("use PostgreSQL 16 with pgvector" not "use a database")
- Consequences must map to concrete follow-up tasks
- Non-goals must be stated to prevent scope creep
- The ADR must be self-contained — no tribal knowledge assumptions
- The ADR must include an implementation plan — which files to touch, which patterns to follow, which tests to write, and how to verify the decision was implemented correctly
When to Write an ADR
Write an ADR when a decision:
- Changes how the system is built or operated (new dependency, architecture pattern, infrastructure choice, API design)
- Is hard to reverse once code is written against it
- Affects other people or agents who will work in this codebase later
- Has real alternatives that were considered and rejected
Do NOT write an ADR for:
- Routine implementation choices within an established pattern
- Bug fixes or typo corrections
- Decisions already captured in an existing ADR (update it instead)
- Style preferences already covered by linters or formatters
When in doubt: if a future agent working in this codebase would benefit from knowing _why_ this choice was made, write the ADR.
Proactive ADR Triggers (For Agents)
If you are an agent coding in a repo and you encounter any of these situations, stop and propose an ADR before continuing:
- You are about to introduce a new dependency that doesn't already exist in the project
- You are about to create a new architectural pattern (new way of handling errors, new data access layer, new API convention) that other code will need to follow
- You are about to make a choice between two or more real alternatives and the tradeoffs are non-obvious
- You are about to change something that contradicts an existing accepted ADR
- You realize you're writing a long code comment explaining "why" — that reasoning belongs in an ADR
How to propose: Tell the human what decision you've hit, why it matters, and ask if they want to capture it as an ADR. If yes, run the full four-phase workflow. If no, note the decision in a code comment and move on.
Creating an ADR: Four-Phase Workflow
Every ADR goes through four phases. Do not skip phases.
Phase 0: Scan the Codebase
Before asking any questions, gather context from the repo:
1. Find existing ADRs. Check contributing/decisions/, docs/decisions/, adr/, docs/adr/, decisions/ for existing records. Read them. Note:
- Existing conventions (directory, naming, template style)
- Decisions that relate to or constrain the current one
- Any ADRs this new decision might supersede
2. Check the tech stack. Read package.json, go.mod, requirements.txt, Cargo.toml, or equivalent. Note relevant dependencies and versions.
3. Find related code patterns. If the decision involves a specific area (e.g., "how we handle auth"), scan for existing implementations. Identify the specific files, directories, and patterns that will be affected by the decision.
4. Check for ADR references in code. Look for ADR references in comments and docs (see "Code ↔ ADR Linking" below). This reveals which existing decisions govern which parts of the codebase.
5. Note what you found. Carry this context into Phase 1 — it will sharpen your questions and prevent the ADR from contradicting existing decisions.
Phase 1: Capture Intent (Socratic)
Interview the human to understand the decision space. Ask questions one at a time, building on previous answers. Do not dump a list of questions.
Core questions (ask in roughly this order, skip what's already clear from context or Phase 0):
1. What are you deciding? — Get a short, specific title. Push for a verb phrase ("Choose X", "Adopt Y", "Replace Z with W"). 2. Why now? — What broke, what's changing, or what will break if you do nothing? This is the trigger. 3. What constraints exist? — Tech stack, timeline, budget, team size, existing code, compliance. Be concrete. Reference what you found in Phase 0 ("I see you're already using X — does that constrain this?"). 4. What does success look like? — Measurable outcomes. Push past "it works" to specifics (latency, throughput, DX, maintenance burden). 5. What options have you considered? — At least two. For each: what's the core tradeoff? If they only have one option, help them articulate why alternatives were rejected. 6. What's your current lean? — Capture gut intuition early. Often reveals unstated priorities. 7. Who needs to know or approve? — Decision-makers, consulted experts, informed stakeholders. 8. What would an agent need to implement this? — Which files/directories are affected? What existing patterns should it follow? What should it avoid? What tests would prove it's working? This directly feeds the Implementation Plan.
Adaptive follow-ups: Based on answers, probe deeper where the decision is fuzzy. Common follow-ups:
- "What's the worst-case outcome if this decision is wrong?"
- "What would make you revisit this in 6 months?"
- "Is there anything you're explicitly choosing NOT to do?"
- "What prior art or existing patterns in the codebase does this relate to?"
- "I found [existing ADR/pattern] — does this new decision interact with it?"
When to stop: You have enough when you can fill every section of the ADR — including the Implementation Plan — without making things up. If you're guessing at any section, ask another question.
Intent Summary Gate: Before moving to Phase 2, present a structured summary of what you captured and ask the human to confirm or correct it:
Here's what I'm capturing for the ADR:
>
- Title: {title}
- Trigger: {why now}
- Constraints: {list}
- Options: {option 1} vs {option 2} [vs ...]
- Lean: {which option and why}
- Non-goals: {what's explicitly out of scope}
- Related ADRs/code: {what exists that this interacts with}
- Affected files/areas: {where in the codebase this lands}
- Verification: {how we'll know it's implemented correctly}
>
Does this capture your intent? Anything to add or correct?
Do NOT proceed to Phase 2 until the human confirms the summary.
Phase 2: Draft the ADR
1. Choose the ADR directory.
- If one exists (found in Phase 0), use it.
- If none exists, create
contributing/decisions/(ifcontributing/exists),docs/decisions/(MADR default), oradr/(simpler repos).
2. Choose a filename strategy.
- If existing ADRs use date prefixes (
YYYY-MM-DD-...), continue that. - Otherwise use slug-only filenames (
choose-database.md).
3. Choose a template.
- Use
assets/templates/adr-simple.mdfor straightforward decisions (one clear winner, minimal tradeoffs). - Use
assets/templates/adr-madr.mdwhen you need to document multiple options with structured pros/cons/drivers. - See
references/template-variants.mdfor guidance.
4. Fill every section from the confirmed intent summary. Do not leave placeholder text. Every section should contain real content or be removed (optional sections only).
5. Write the Implementation Plan. This is the most important section for agent-first ADRs. It tells the next agent exactly what to do. See the template for structure.
6. Write Verification criteria as checkboxes. These must be specific enough that an agent can programmatically or manually check each one.
7. Generate the file.
- Preferred: run
scripts/new_adr.js(handles directory, naming, and optional index updates). - If you can't run scripts, copy a template from
assets/templates/and fill it manually.
Phase 3: Review Against Checklist
After drafting, review the ADR against the agent-readiness checklist in references/review-checklist.md.
Present the review as a summary, not a raw checklist dump. Format:
ADR Review
>
✅ Passes: {list what's solid — e.g., "context is self-contained, implementation plan covers affected files, verification criteria are checkable"}
>
⚠️ Gaps found:
>
- {specific gap 1 — e.g., "Implementation Plan doesn't mention test files — which test suite should cover this?"}
- {specific gap 2}
>
Recommendation: {Ship it / Fix the gaps first / Needs more Phase 1 work}
Only surface failures and notable strengths — do not recite every passing checkbox.
If there are gaps, propose specific fixes. Do not just flag problems — offer solutions and ask the human to approve.
Do not finalize until the ADR passes the checklist or the human explicitly accepts the gaps.
Consulting ADRs (Read Workflow)
Agents should read existing ADRs before implementing changes in a codebase that has them. This is not part of the create-an-ADR workflow — it's a standalone operation any agent should do.
When to Consult ADRs
- Before starting work on a feature that touches architecture (auth, data layer, API design, infrastructure)
- When you encounter a pattern in the code and wonder "why is it done this way?"
- Before proposing a change that might contradict an existing decision
- When a human says "check the ADRs" or "there's a decision about this"
- When you find an ADR reference in a code comment
How to Consult ADRs
1. Find the ADR directory. Check contributing/decisions/, docs/decisions/, adr/, docs/adr/, decisions/. Also check for an index file (README.md or index.md).
2. Scan titles and statuses. Read the index or list filenames. Focus on accepted ADRs — these are active decisions.
3. Read relevant ADRs fully. Don't just read the title — read context, decision, consequences, non-goals, AND the Implementation Plan. The Implementation Plan tells you what patterns to follow and what files are governed by this decision.
4. Respect the decisions. If an accepted ADR says "use PostgreSQL," don't propose switching to MongoDB without creating a new ADR that supersedes it. If you find a conflict between what the code does and what the ADR says, flag it to the human.
5. Follow the Implementation Plan. When implementing code in an area governed by an ADR, follow the patterns specified in its Implementation Plan. If the plan says "all new queries go through the data-access layer in src/db/," do that.
6. Reference ADRs in your work. Add ADR references in code comments and PR descriptions (see "Code ↔ ADR Linking" below).
Code ↔ ADR Linking
ADRs should be bidirectionally linked to the code they govern.
ADR → Code (in the Implementation Plan)
The Implementation Plan section names specific files, directories, and patterns:
## Implementation Plan
- **Affected paths**: `src/db/`, `src/config/database.ts`, `tests/integration/`
- **Pattern**: all database queries go through `src/db/client.ts`Code → ADR (in comments)
When implementing code guided by an ADR, add a comment referencing it:
// ADR: Using better-sqlite3 for test database
// See: docs/decisions/2025-06-15-use-sqlite-for-test-database.md
import Database from 'better-sqlite3';Keep these lightweight — one comment at the entry point, not on every line. The goal is discoverability: when a future agent reads this code, they can find the reasoning.
Why This Matters
- An agent working in
src/db/can find which ADRs govern that area - An agent reading an ADR can find the code that implements it
- When an ADR is superseded, the code references make it easy to find all code that needs updating
Other Operations
Update an Existing ADR
1. Identify the intent:
- Accept / reject: change status, add any final context.
- Deprecate: status →
deprecated, explain replacement path. - Supersede: create a new ADR, link both ways (old → new, new → old).
- Add learnings: append to
## More Informationwith a date stamp. Do not rewrite history.
2. Use scripts/set_adr_status.js for status changes (supports YAML front matter, bullet status, and section status).
Post-Acceptance Lifecycle
After an ADR is accepted:
1. Create implementation tasks. Each item in the Implementation Plan and each follow-up in Consequences should become a trackable task (issue, ticket, or TODO). 2. Reference the ADR in PRs. Link to the ADR in PR descriptions, e.g. "Implements contributing/decisions/2025-06-15-use-sqlite-for-test-database.md." 3. Add code references. Add ADR path comments at key implementation points. 4. Check verification criteria. Once implementation is complete, walk through the Verification checkboxes. Update the ADR with results in ## More Information. 5. Revisit when triggers fire. If the ADR specified revisit conditions ("if X happens, reconsider"), monitor for those conditions.
Index
If the repo has an ADR index/log file (often README.md or index.md in the ADR dir), keep it updated.
Preferred: let scripts/new_adr.js --update-index do it. Otherwise:
- Add a bullet entry for the new ADR.
- Keep ordering consistent (numeric if numbered; date or alpha if slugs).
Bootstrap
When introducing ADRs to a repo that has none:
node /path/to/adr-skill/scripts/bootstrap_adr.jsThis creates the directory, an index file, and a filled-out first ADR ("Adopt architecture decision records") with real content explaining why the team is using ADRs. Use --json for machine-readable output. Use --dir to override the directory name.
Categories (Large Projects)
For repos with many ADRs, organize by subdirectory:
docs/decisions/
backend/
2025-06-15-use-postgres.md
frontend/
2025-06-20-use-react.md
infrastructure/
2025-07-01-use-terraform.mdDate prefixes are local to each category. Choose a categorization scheme early (by layer, by domain, by team) and document it in the index.
Resources
scripts/
scripts/new_adr.js— create a new ADR file from a template, using repo conventions.scripts/set_adr_status.js— update an ADR status in-place (YAML front matter or inline). Use--jsonfor machine output.scripts/bootstrap_adr.js— create ADR dir,README.md, and initial "Adopt ADRs" decision.
references/
references/review-checklist.md— agent-readiness checklist for Phase 3 review.references/adr-conventions.md— directory, filename, status, and lifecycle conventions.references/template-variants.md— when to use simple vs MADR-style templates.references/examples.md— filled-out short and long ADR examples with implementation plans.
assets/
assets/templates/adr-simple.md— lean template for straightforward decisions.assets/templates/adr-madr.md— MADR 4.0 template for decisions with multiple options and structured tradeoffs.assets/templates/adr-readme.md— default ADR index scaffold used byscripts/bootstrap_adr.js.
Script Usage
From the target repo root:
# Simple ADR
node /path/to/adr-skill/scripts/new_adr.js --title "Choose database" --status proposed
# MADR-style with options
node /path/to/adr-skill/scripts/new_adr.js --title "Choose database" --template madr --status proposed
# With index update
node /path/to/adr-skill/scripts/new_adr.js --title "Choose database" --status proposed --update-index
# Bootstrap a new repo
node /path/to/adr-skill/scripts/bootstrap_adr.js --dir docs/decisionsNotes:
- Scripts auto-detect ADR directory and filename strategy.
- Use
--dirand--strategyto override. - Use
--jsonto emit machine-readable output.
{short title, representative of solved problem and found solution}
Context and Problem Statement
{Describe the context and problem statement. Frame it as a question when possible. Link to relevant issues, tickets, or prior ADRs. Include enough background that someone (or an agent) encountering this for the first time can understand why this decision exists without asking follow-up questions.}
<!-- Optional — remove if not needed -->
Decision Drivers
- {decision driver 1, e.g., a constraint, requirement, or force}
- {decision driver 2}
- …
Considered Options
- {title of option 1}
- {title of option 2}
- {title of option 3}
- …
Decision Outcome
Chosen option: "{title of option 1}", because {justification — reference drivers and tradeoffs}.
Consequences
- Good, because {positive consequence}
- Bad, because {negative consequence}
- Neutral, because {consequence that is neither positive nor negative}
- …
Implementation Plan
{This section tells an agent exactly what to do to implement this decision. Be specific — an agent should be able to start coding from this without asking follow-up questions.}
- Affected paths: {list files and directories that need to change, e.g.,
src/db/,src/config/database.ts,tests/integration/} - Dependencies: {packages to add/remove/update, e.g., "add
better-sqlite3@11.x, removepg-mem"} - Patterns to follow: {reference existing code patterns, e.g., "follow the repository pattern in
src/db/repositories/"} - Patterns to avoid: {what NOT to do, e.g., "do not use raw SQL outside the data-access layer"}
- Configuration: {env vars, config files, feature flags to add/change}
- Migration steps: {if replacing something, what's the migration path? Can it be done incrementally?}
Verification
{Checkboxes an agent can validate after implementation. Each must be specific and testable.}
- [ ] {verification criterion 1, e.g., "
npm testpasses with SQLite as the test database"} - [ ] {verification criterion 2, e.g., "no direct
pgimports outsidesrc/db/client.ts"} - [ ] {verification criterion 3, e.g., "CI pipeline completes in under 60 seconds"}
- …
<!-- Optional — remove if not needed -->
Pros and Cons of the Options
{title of option 1}
{Brief description or link to more information}
- Good, because {argument a}
- Good, because {argument b}
- Neutral, because {argument c}
- Bad, because {argument d}
- …
{title of option 2}
{Brief description or link to more information}
- Good, because {argument a}
- Good, because {argument b}
- Neutral, because {argument c}
- Bad, because {argument d}
- …
<!-- Optional — remove if not needed -->
More Information
{Additional context, links to related ADRs, team agreements, implementation notes, or conditions that would trigger revisiting this decision. This section is a catch-all for anything that helps future readers (human or agent) understand the full picture.}
Architecture Decision Records (ADR)
An Architecture Decision Record (ADR) captures an important architecture decision along with its context and consequences.
Conventions
- Directory:
{ADR_DIR} - Naming:
- Use date-prefixed files:
YYYY-MM-DD-choose-database.md - If the repo already uses slug-only names, keep that:
choose-database.md - Status values:
proposed,accepted,rejected,deprecated,superseded
Workflow
- Create a new ADR as
proposed. - Discuss and iterate.
- When the team commits: mark it
accepted(orrejected). - If replaced later: create a new ADR and mark the old one
supersededwith a link.
ADRs
{short title, representative of solved problem and found solution}
Context and Problem Statement
{Why does this decision need to happen now? What constraints exist? Include enough background that someone (or an agent) reading this for the first time can understand without follow-up questions.}
Decision
{What are we choosing to do? Be specific — include scope and non-goals.}
Consequences
- Good, because {positive consequence}
- Bad, because {negative consequence}
- …
Implementation Plan
- Affected paths: {files and directories that change}
- Dependencies: {packages to add/remove/update}
- Patterns to follow: {existing code patterns to match}
- Patterns to avoid: {what NOT to do}
Verification
- [ ] {how to confirm the decision was implemented correctly}
- [ ] {another verification criterion}
<!-- Optional — remove if not needed -->
Alternatives Considered
- {Alternative 1}: {Why it was rejected, in one or two sentences.}
- {Alternative 2}: {Why it was rejected.}
<!-- Optional — remove if not needed -->
More Information
{Related ADRs, PRs, issues, docs, or conditions that would trigger revisiting this decision.}
ADR Conventions (Reference)
Directory
If the repo already has an ADR directory, keep it.
If the repo has no ADR directory, choose based on project size:
- `docs/decisions/` — MADR default, recommended for projects with existing
docs/structure. - `adr/` — simpler alternative for smaller repos.
Detection order (used by scripts): contributing/decisions/, docs/decisions/, adr/, docs/adr/, docs/adrs/, decisions/.
Filename Conventions
Pattern: YYYY-MM-DD-title-with-dashes.md
YYYY-MM-DDis the ADR creation date (matches thedatefrontmatter field).- Title uses lowercase, dashes, present-tense imperative verb phrase.
- Examples:
2025-06-15-choose-database.md,2025-07-01-adopt-adrs.md - Multiple ADRs on the same date are fine — the slug suffix disambiguates them.
If a repo already uses slug-only filenames (no date prefix), follow that convention.
Minimal Sections
At minimum, every ADR must clearly include:
1. Context: why the decision exists now, what constraints/drivers apply. 2. Decision: what is chosen. 3. Consequences: what becomes easier/harder, risks, costs, follow-ups.
For agent-first ADRs, also ensure:
- Constraints are explicit and measurable
- Non-goals are stated
- Follow-up tasks are identified
Status Values
Track status in YAML front matter:
---
status: proposed
date: 2025-06-15
decision-makers: Alice, Bob
---Common statuses:
| Status | Meaning |
|---|---|
proposed | Under discussion, not yet decided |
accepted | Decision is active and should be followed |
rejected | Considered but explicitly not adopted |
deprecated | Was accepted but no longer applies — explain replacement path |
superseded by [title](link) | Replaced by a newer ADR — always link both ways |
YAML Front Matter Fields
| Field | Required | Description |
|---|---|---|
status | Yes | Current lifecycle state |
date | Yes | Date of last status change (YYYY-MM-DD) |
decision-makers | Yes | People who own the decision |
consulted | No | Subject-matter experts consulted (two-way communication) |
informed | No | Stakeholders kept up-to-date (one-way communication) |
The consulted and informed fields follow the RACI model and are useful for audit trails in larger teams.
Mutability
- Prefer appending new information with a date stamp over rewriting existing content.
- If a decision is replaced, create a new ADR and explicitly supersede the old one.
- Status changes and after-action notes are fine to edit in-place.
Categories (Large Projects)
For repos accumulating many ADRs, use subdirectories:
contributing/decisions/ # or docs/decisions/
backend/
2025-06-15-use-postgres.md
frontend/
2025-06-20-use-react.md
infrastructure/
2025-07-01-use-terraform.mdDate prefixes are local to each category. Choose a categorization scheme early (by architectural layer, by domain, by team) and document it in the index.
Alternative: use tags or a flat structure with a searchable index. Subdirectories are simpler and work with all tools.
ADR Examples
These are filled-out examples showing the same decision at two levels of detail. Use these as reference when drafting ADRs — never leave placeholder text in a real ADR.
Short Version (Simple Template)
---
status: accepted
date: 2025-06-15
decision-makers: Sarah Chen, Joel
---
# Use SQLite for local development database
## Context and Problem Statement
Our integration tests require a database but currently hit a shared PostgreSQL instance, causing flaky tests from concurrent writes and slow CI (3+ minute setup per run). We need a fast, isolated database for local dev and CI that doesn't require infrastructure provisioning.
## Decision
Use SQLite (via better-sqlite3) for local development and CI test runs. Production remains on PostgreSQL. We'll use a thin data-access layer that abstracts the database engine, tested against both SQLite and PostgreSQL in CI.
Non-goals: we are NOT migrating production to SQLite or building a full ORM abstraction.
## Consequences
- Good, because CI setup drops from 3+ minutes to ~2 seconds (no DB provisioning)
- Good, because tests are fully isolated — no shared state between runs
- Good, because developers can run the full test suite offline
- Bad, because we must maintain compatibility between SQLite and PostgreSQL SQL dialects
- Bad, because some PostgreSQL-specific features (JSONB operators, array columns) can't be tested locally
## Implementation Plan
- **Affected paths**: `src/db/client.ts` (new abstraction layer), `src/db/sqlite-client.ts` (new), `src/db/pg-client.ts` (refactored from current inline usage), `tests/setup.ts`, `package.json`
- **Dependencies**: add `better-sqlite3@11.x` and `@types/better-sqlite3@7.x` as devDependencies; no production dependency changes
- **Patterns to follow**: existing repository pattern in `src/db/repositories/` — all queries go through repository methods, never raw SQL in business logic
- **Patterns to avoid**: do not import `better-sqlite3` or `pg` directly outside `src/db/`; do not use PostgreSQL-specific SQL (JSONB operators, `ANY()`, array literals) in shared queries
### Verification
- [ ] `npm test` passes with `DB_ENGINE=sqlite` (default for test env)
- [ ] `npm test` passes with `DB_ENGINE=postgres` against a real PostgreSQL instance
- [ ] No imports of `better-sqlite3` or `pg` outside `src/db/`
- [ ] CI pipeline total time under 90 seconds (was 5+ minutes)
- [ ] `src/db/client.ts` exports a unified interface used by all repositories
## Alternatives Considered
- Docker PostgreSQL per CI run: Reliable parity, but adds 90s+ startup and requires Docker-in-Docker on CI.
- In-memory PostgreSQL (pg-mem): Good API compatibility, but incomplete support for our schema (triggers, CTEs) and unmaintained.
## More Information
- Follow-up: create weekly CI job running full suite against real PostgreSQL (#348)
- Revisit trigger: if dialect-drift bugs exceed 2 per quarter, reconsider Docker PostgreSQL approachLong Version (MADR Template)
The same decision with full options analysis:
---
status: accepted
date: 2025-06-15
decision-makers: Sarah Chen, Joel
consulted: Alex (DBA), Platform team
informed: Frontend team, QA
---
# Use SQLite for local development database
## Context and Problem Statement
Our integration tests require a database but currently hit a shared PostgreSQL instance. This causes two problems:
1. Flaky tests from concurrent writes (multiple developers and CI jobs sharing one DB)
2. Slow CI — each run spends 3+ minutes provisioning and seeding the database
How can we provide a fast, isolated database for local development and CI without sacrificing confidence in production compatibility?
Related: [Use PostgreSQL for production](2025-05-01-use-postgresql-for-production.md) — this decision must not compromise production database choice.
## Decision Drivers
- CI speed: current 3+ minute DB setup is the bottleneck in our 5-minute pipeline
- Test isolation: zero shared state between parallel test runs
- Production parity: must catch SQL dialect issues before they hit production
- Developer experience: should work offline, no external dependencies for `npm test`
- Maintenance cost: solution should not require a dedicated owner
## Considered Options
- SQLite via better-sqlite3
- Docker PostgreSQL per CI run
- In-memory PostgreSQL (pg-mem)
## Decision Outcome
Chosen option: "SQLite via better-sqlite3", because it eliminates the CI bottleneck (2s vs 3+ min), provides full isolation, works offline, and has minimal maintenance cost. The dialect-drift risk is mitigated by a weekly CI job against real PostgreSQL.
### Consequences
- Good, because CI database setup drops from 3+ minutes to ~2 seconds
- Good, because each test run is fully isolated (file-based DB, no shared state)
- Good, because developers can run the full test suite offline with zero infrastructure
- Bad, because we must maintain a data-access abstraction layer to paper over SQL dialect differences
- Bad, because PostgreSQL-specific features (JSONB operators, array columns, advisory locks) cannot be tested locally
- Neutral, because the abstraction layer adds ~200 lines of code but also makes future DB migrations easier
## Implementation Plan
- **Affected paths**:
- `src/db/client.ts` — new: unified database interface (DatabaseClient type + factory function)
- `src/db/sqlite-client.ts` — new: SQLite implementation of DatabaseClient
- `src/db/pg-client.ts` — refactor: extract current inline pg usage into DatabaseClient implementation
- `src/db/repositories/*.ts` — update: use DatabaseClient instead of direct pg calls
- `tests/setup.ts` — update: initialize SQLite by default, read `DB_ENGINE` env var
- `tests/fixtures/seed.sql` — update: ensure all seed SQL is dialect-neutral
- `.env.test` — new: `DB_ENGINE=sqlite`
- `.github/workflows/ci.yml` — update: remove PostgreSQL service container from main CI
- `package.json` — add devDependencies
- **Dependencies**: add `better-sqlite3@11.x`, `@types/better-sqlite3@7.x` as devDependencies
- **Patterns to follow**:
- Repository pattern in `src/db/repositories/` — all database access goes through repository methods
- Use parameterized queries exclusively (no string interpolation)
- Reference implementation: `src/db/repositories/users.ts` for the expected style
- **Patterns to avoid**:
- Do NOT import `better-sqlite3` or `pg` directly outside `src/db/`
- Do NOT use PostgreSQL-specific SQL in shared queries: no `JSONB` operators (`->`, `->>`), no `ANY(ARRAY[...])`, no `ON CONFLICT ... DO UPDATE`
- Do NOT use SQLite-specific SQL either — keep queries portable
- **Configuration**: `DB_ENGINE` env var (`sqlite` | `postgres`), defaults to `sqlite` in test, `postgres` in production
- **Migration steps**:
1. Create `DatabaseClient` interface and SQLite implementation
2. Refactor existing pg code into pg implementation
3. Update repositories one at a time (each can be a separate PR)
4. Update test setup last, once all repositories use the abstraction
5. Remove PostgreSQL service container from CI workflow
### Verification
- [ ] `DB_ENGINE=sqlite npm test` passes (all integration tests)
- [ ] `DB_ENGINE=postgres npm test` passes against a real PostgreSQL 16 instance
- [ ] `grep -r "from 'better-sqlite3'" src/ --include='*.ts' | grep -v 'src/db/'` returns no results
- [ ] `grep -r "from 'pg'" src/ --include='*.ts' | grep -v 'src/db/'` returns no results
- [ ] CI pipeline completes in under 90 seconds (measured on main branch)
- [ ] `src/db/client.ts` exports `DatabaseClient` interface and `createClient()` factory
- [ ] `.env.test` sets `DB_ENGINE=sqlite`
- [ ] Weekly PostgreSQL compatibility CI job exists in `.github/workflows/`
## Pros and Cons of the Options
### SQLite via better-sqlite3
[better-sqlite3](https://github.com/WiseLibs/better-sqlite3) — synchronous SQLite bindings for Node.js.
- Good, because zero infrastructure — just an npm dependency
- Good, because synchronous API makes test setup/teardown trivial
- Good, because file-based DBs enable parallelism (one file per test worker)
- Neutral, because requires a thin abstraction layer (~200 LOC)
- Bad, because SQL dialect differences (no JSONB, different date handling, no arrays)
- Bad, because does not exercise PostgreSQL-specific query plans or extensions
### Docker PostgreSQL per CI run
Spin up a fresh PostgreSQL container for each CI job.
- Good, because perfect production parity — same engine, same version
- Good, because no abstraction layer needed
- Bad, because adds 90+ seconds to every CI run (image pull + startup + healthcheck)
- Bad, because requires Docker-in-Docker on CI, adding complexity and security surface
- Bad, because developers need Docker running locally for `npm test`
### In-memory PostgreSQL (pg-mem)
[pg-mem](https://github.com/oguimbal/pg-mem) — in-memory PostgreSQL emulator for testing.
- Good, because better SQL compatibility than SQLite
- Good, because no infrastructure needed
- Bad, because incomplete support for our schema features (triggers, CTEs, lateral joins)
- Bad, because last published release is 8+ months old — maintenance risk
- Bad, because debugging failures requires understanding pg-mem's emulation quirks
## More Information
- Follow-up task: create data-access abstraction layer — #347
- Follow-up task: set up weekly PostgreSQL CI job — #348
- Related: [Use PostgreSQL for production](2025-05-01-use-postgresql-for-production.md)
- Revisit trigger: if dialect-drift bugs exceed 2 per quarter, reconsider Docker PostgreSQL approach
- Code references: after implementation, key files will have `// ADR: 2025-06-15-use-sqlite-for-test-database` comments at entry pointsADR Review Checklist
Use this checklist in Phase 3 to validate an ADR before finalizing. The goal: could a coding agent read this ADR and start implementing the decision immediately, without asking any clarifying questions?
Agent-Readiness Checks
Context & Problem
- [ ] A reader with no prior context can understand why this decision exists
- [ ] The trigger is clear (what changed, broke, or is about to break)
- [ ] No tribal knowledge is assumed — acronyms are defined, systems are named explicitly
- [ ] Links to relevant issues, PRs, or prior ADRs are included
Decision
- [ ] The decision is specific enough to act on (not "use a better approach" but "use X for Y")
- [ ] Scope is bounded — what's in AND what's out (non-goals)
- [ ] Constraints are explicit and measurable where possible (e.g., "< 200ms p95" not "fast enough")
Consequences
- [ ] Each consequence is concrete and actionable, not aspirational
- [ ] Follow-up tasks are identified (migrations, config changes, documentation, new tests)
- [ ] Risks are stated with mitigation strategies or acceptance rationale
- [ ] No consequence is a disguised restatement of the decision
Implementation Plan
- [ ] Affected files/directories are named explicitly (not "the database code" but "src/db/client.ts")
- [ ] Dependencies to add/remove are specified with version constraints
- [ ] Patterns to follow reference existing code (not abstract descriptions)
- [ ] Patterns to avoid are stated (what NOT to do)
- [ ] Configuration changes are listed (env vars, config files, feature flags)
- [ ] If replacing something, migration steps are described
Verification
- [ ] Criteria are checkboxes, not prose
- [ ] Each criterion is testable — an agent could write a test or run a command to check it
- [ ] Criteria cover both "it works" (functional) and "it's done right" (structural/architectural)
- [ ] No criterion is vague ("it performs well" → "p95 latency < 200ms under 100 concurrent requests")
Options (MADR template)
- [ ] At least two options were genuinely considered (not just "do the thing" vs "do nothing")
- [ ] Each option has real pros AND cons (not a straw-man comparison)
- [ ] The justification for the chosen option references specific drivers or tradeoffs
- [ ] Rejected options explain WHY they were rejected, not just what they are
Meta
- [ ] Status is set correctly (usually
proposedfor new ADRs) - [ ] Date is set
- [ ] Decision-makers are listed
- [ ] Title is a verb phrase describing the decision (not the problem)
- [ ] Filename follows repo conventions
Quick Scoring
Count the checked items. This isn't a gate — it's a conversation tool.
- All checked: Ship it.
- 1–3 unchecked: Discuss the gaps with the human. Most can be fixed in a minute.
- 4+ unchecked: The ADR needs more work. Go back to Phase 1 for the fuzzy areas.
Common Failure Modes
| Symptom | Root Cause | Fix |
|---|---|---|
| "Improve performance" as a consequence | Vague intent | Ask: "improve which metric, by how much, measured how?" |
| Only one option listed | Decision already made, ADR is post-hoc | Ask: "what did you reject and why?" — capture the reasoning |
| Context reads like a solution pitch | Skipped problem framing | Rewrite context as the problem, move solution to Decision |
| Consequences are all positive | Cherry-picking | Ask: "what gets harder? what's the maintenance cost?" |
| "We decided to use X" with no why | Missing justification | Ask: "why X over Y?" — the 'over Y' forces comparison |
| Implementation Plan says "update the code" | Too abstract | Ask: "which files, which functions, what pattern?" |
| Verification says "it works" | Not testable | Ask: "what command would you run to prove it works?" |
| No affected paths listed | Implementation Plan is hand-wavy | Agent should scan the codebase and propose specific paths |
Template Variants
This skill ships two templates in assets/templates/.
Simple
File: assets/templates/adr-simple.md
Use this when:
- The decision is straightforward (one clear winner, minimal tradeoffs)
- You mainly need "why, what, consequences, how to implement"
- Alternatives are few and can be dismissed in a sentence each
- Speed matters more than exhaustive comparison
Sections: Context and Problem Statement → Decision → Consequences → Implementation Plan → Verification → Alternatives Considered (optional) → More Information (optional).
MADR (Options-Heavy)
File: assets/templates/adr-madr.md
Use this when:
- You have multiple real options and want to document structured tradeoffs
- You need to capture decision drivers explicitly (what criteria mattered)
- The decision is likely to be revisited and the comparison needs to survive
- Stakeholders need to see the reasoning process, not just the outcome
Sections: Context and Problem Statement → Decision Drivers (optional) → Considered Options → Decision Outcome → Consequences → Implementation Plan → Verification → Pros and Cons of the Options (optional) → More Information (optional).
This template aligns with MADR 4.0 and extends it with agent-first sections.
Both Templates Share
- YAML front matter for metadata (status, date, decision-makers, consulted, informed)
- Implementation Plan — affected paths, dependencies, patterns to follow/avoid, configuration, migration steps. This is what makes the ADR an executable spec for agents.
- Verification as checkboxes — testable criteria an agent can validate after implementation
- Agent-first framing: placeholder text prompts you to be specific, measurable, and self-contained
- "More Information" section for cross-links, follow-ups, and revisit triggers
- "Neutral, because..." as a third argument category alongside Good and Bad
Choosing Between Them
| Signal | Use Simple | Use MADR |
|---|---|---|
| Number of real options | 1–2 | 3+ |
| Team size affected | Small / solo | Cross-team |
| Reversibility | Easily reversed | Hard to undo |
| Expected lifetime | Months | Years |
| Needs stakeholder review | No | Yes |
When in doubt, start with Simple. You can always expand to MADR if the discussion reveals more complexity.
#!/usr/bin/env node
/**
* Bootstrap ADRs in a repo:
* - create ADR directory
* - create adr/README.md (index) using a template
* - create first ADR: "Adopt architecture decision records"
*/
const fs = require('node:fs');
const path = require('node:path');
function die(msg) {
process.stderr.write(`${msg}\n`);
process.exit(1);
}
function parseArgs(argv) {
const out = {
repoRoot: '.',
dir: 'adr',
forceIndex: false,
indexFile: null,
firstTitle: 'Adopt architecture decision records',
firstStatus: 'accepted',
deciders: '',
technicalStory: '',
strategy: 'date',
json: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
const next = () => {
if (i + 1 >= argv.length) die(`Missing value for ${a}`);
return argv[++i];
};
if (a === '--repo-root') out.repoRoot = next();
else if (a === '--dir') out.dir = next();
else if (a === '--force-index') out.forceIndex = true;
else if (a === '--index-file') out.indexFile = next();
else if (a === '--first-title') out.firstTitle = next();
else if (a === '--first-status') out.firstStatus = next();
else if (a === '--deciders') out.deciders = next();
else if (a === '--technical-story') out.technicalStory = next();
else if (a === '--strategy') out.strategy = next();
else if (a === '--json') out.json = true;
else if (a === '--help' || a === '-h') {
process.stdout.write(
[
'Usage: node bootstrap_adr.js [options]',
'',
'Options:',
' --repo-root <path> Repo root (default: .)',
' --dir <path> ADR directory (default: adr)',
' --index-file <path> Override index file path (relative to repo root unless absolute)',
' --force-index Overwrite index file if it exists',
' --first-title <text> Title for initial ADR',
' --first-status <text> Status for initial ADR (default: accepted)',
' --strategy date|slug|auto Filename strategy for initial ADR (default: date)',
' --json Output machine-readable JSON (default: off)',
'',
].join('\n'),
);
process.exit(0);
} else {
die(`Unknown arg: ${a}`);
}
}
if (!['auto', 'date', 'slug'].includes(out.strategy))
die(`Invalid --strategy: ${out.strategy}`);
return out;
}
function loadReadmeTemplate() {
const skillRoot = path.resolve(__dirname, '..');
const templatePath = path.join(
skillRoot,
'assets',
'templates',
'adr-readme.md',
);
if (!fs.existsSync(templatePath))
die(`README template not found: ${templatePath}`);
return fs.readFileSync(templatePath, 'utf8');
}
function writeIndex(indexFile, adrDirName, { force }) {
if (fs.existsSync(indexFile) && !force) return;
const content = loadReadmeTemplate().replaceAll('{ADR_DIR}', adrDirName);
fs.mkdirSync(path.dirname(indexFile), { recursive: true });
fs.writeFileSync(indexFile, `${content.trimEnd()}\n`, 'utf8');
}
function slugify(text) {
const t = String(text || '')
.trim()
.toLowerCase();
const noQuotes = t.replace(/['"`]/g, '');
const dashed = noQuotes.replace(/[^a-z0-9]+/g, '-').replace(/-{2,}/g, '-');
const trimmed = dashed.replace(/^-+/, '').replace(/-+$/, '');
return trimmed || 'decision';
}
function toPosix(p) {
return p.split(path.sep).join('/');
}
function generateFirstAdr({ title, status, date, deciders, adrDir }) {
const deciderLine = deciders
? String(deciders)
.split(',')
.map(s => s.trim())
.filter(Boolean)
.join(', ')
: '';
return `---
status: ${status}
date: ${date}
decision-makers: ${deciderLine}
---
# ${title}
## Context and Problem Statement
Architecture decisions in this project are made implicitly — through code, conversations, and tribal knowledge. When a new contributor (human or AI agent) joins the codebase, there is no record of *why* things are built the way they are. This makes it hard to:
- Understand whether a pattern is intentional or accidental
- Know if a past decision still applies or has been superseded
- Avoid relitigating decisions that were already carefully considered
We need a lightweight, version-controlled way to capture decisions where the code lives.
## Decision
Adopt Architecture Decision Records (ADRs) using the MADR 4.0 format, stored in \`${adrDir}/\`.
Conventions:
- One ADR per file, named \`YYYY-MM-DD-title-with-dashes.md\`
- New ADRs start as \`proposed\`, move to \`accepted\` or \`rejected\`
- Superseded ADRs link to their replacement
- ADRs are written to be self-contained — a coding agent should be able to read one and implement the decision without further context
## Consequences
* Good, because decisions are discoverable and version-controlled alongside the code
* Good, because new contributors (human or agent) can understand the "why" behind architecture choices
* Good, because the team builds a shared decision log that prevents relitigating settled questions
* Bad, because writing ADRs takes time — though a good ADR saves more time than it costs
* Neutral, because ADRs require periodic review to mark outdated decisions as deprecated or superseded
## Alternatives Considered
* No formal records: Continue making decisions in conversations and code comments. Rejected because context is lost and decisions get relitigated.
* Wiki or Notion pages: Capture decisions outside the repo. Rejected because they drift out of sync with the code and are not version-controlled.
* Lightweight RFCs: More heavyweight process with formal review cycles. Rejected as overkill for most decisions — ADRs can scale up to RFC-level detail when needed.
## More Information
* MADR: <https://adr.github.io/madr/>
* Michael Nygard, "Documenting Architecture Decisions": <https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions>`;
}
function updateIndexFile(indexFile, { relLink, title, status, date }) {
if (!fs.existsSync(indexFile)) return;
let content = fs.readFileSync(indexFile, 'utf8');
if (content.includes(relLink)) return;
const entryLine = `- [${title}](${relLink}) (${status}, ${date})`;
// Append after "## ADRs" heading if found, otherwise append at end
const normalized = content.replace(/\r\n/g, '\n');
const lines = normalized.split('\n');
const headingIdx = lines.findIndex(l => /^##\s+ADRs\s*$/i.test(l));
if (headingIdx !== -1) {
// Insert after the heading (and any blank line after it)
let insertAt = headingIdx + 1;
while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;
lines.splice(insertAt, 0, entryLine);
} else {
lines.push(entryLine);
}
fs.writeFileSync(indexFile, lines.join('\n'), 'utf8');
}
function main() {
const args = parseArgs(process.argv);
const repoRoot = path.resolve(process.cwd(), args.repoRoot);
if (!fs.existsSync(repoRoot)) die(`Repo root does not exist: ${repoRoot}`);
const adrDir = path.resolve(repoRoot, args.dir);
fs.mkdirSync(adrDir, { recursive: true });
const indexFile = args.indexFile
? path.isAbsolute(args.indexFile)
? args.indexFile
: path.resolve(repoRoot, args.indexFile)
: path.join(adrDir, 'README.md');
const indexExistedBefore = fs.existsSync(indexFile);
writeIndex(indexFile, args.dir, { force: args.forceIndex });
const indexWritten =
fs.existsSync(indexFile) && (!indexExistedBefore || args.forceIndex);
// Create the first ADR as a filled-out decision (not a blank template).
const relIndex = path.isAbsolute(indexFile)
? path.relative(repoRoot, indexFile)
: indexFile;
const today = new Date().toISOString().slice(0, 10);
const firstAdrContent = generateFirstAdr({
title: args.firstTitle,
status: args.firstStatus,
date: today,
deciders: args.deciders,
adrDir: args.dir,
});
// Determine filename using same logic as new_adr.js
const strategy = args.strategy === 'auto' ? 'date' : args.strategy;
let firstAdrFilename;
if (strategy === 'date') {
firstAdrFilename = `${today}-${slugify(args.firstTitle)}.md`;
} else {
firstAdrFilename = `${slugify(args.firstTitle)}.md`;
}
const firstAdrPath = path.join(adrDir, firstAdrFilename);
fs.writeFileSync(firstAdrPath, `${firstAdrContent.trimEnd()}\n`, 'utf8');
// Update index
const relLink = toPosix(path.relative(path.dirname(indexFile), firstAdrPath));
updateIndexFile(indexFile, {
relLink,
title: args.firstTitle,
status: args.firstStatus,
date: today,
});
if (args.json) {
const payload = {
repoRoot,
adrDir,
adrDirRelPath: toPosix(path.relative(repoRoot, adrDir)),
indexPath: indexFile,
indexRelPath: toPosix(relIndex),
indexExistedBefore,
indexWritten,
firstAdr: {
createdAdrPath: firstAdrPath,
createdAdrRelPath: toPosix(path.relative(repoRoot, firstAdrPath)),
title: args.firstTitle,
status: args.firstStatus,
strategy,
date: today,
},
date: today,
};
process.stdout.write(`${JSON.stringify(payload)}\n`);
return;
}
process.stdout.write(`${firstAdrPath}\n`);
process.stdout.write(`Bootstrapped ADRs at ${adrDir} (${today})\n`);
process.stdout.write(`Index: ${indexFile}\n`);
}
main();
#!/usr/bin/env node
/**
* Create a new ADR markdown file using repo conventions and a template.
*
* Design goals:
* - Safe defaults (auto-detect adr directory + numbering)
* - No external deps
* - Works even if the repo has no ADRs yet
*/
const fs = require('node:fs');
const path = require('node:path');
function die(msg) {
process.stderr.write(`${msg}\n`);
process.exit(1);
}
function slugify(text) {
const t = String(text || '')
.trim()
.toLowerCase();
const noQuotes = t.replace(/['"`]/g, '');
const dashed = noQuotes.replace(/[^a-z0-9]+/g, '-').replace(/-{2,}/g, '-');
const trimmed = dashed.replace(/^-+/, '').replace(/-+$/, '');
return trimmed || 'decision';
}
function toPosix(p) {
return p.split(path.sep).join('/');
}
function parseArgs(argv) {
const out = {
repoRoot: '.',
dir: null,
noCreateDir: false,
title: null,
status: 'proposed',
template: 'simple', // simple | madr
strategy: 'auto', // auto | date | slug
deciders: '',
consulted: '',
informed: '',
technicalStory: '',
chosenOption: '',
updateIndex: false,
indexFile: null,
json: false,
};
for (let i = 2; i < argv.length; i++) {
const a = argv[i];
const next = () => {
if (i + 1 >= argv.length) die(`Missing value for ${a}`);
return argv[++i];
};
if (a === '--repo-root') out.repoRoot = next();
else if (a === '--dir') out.dir = next();
else if (a === '--no-create-dir') out.noCreateDir = true;
else if (a === '--title') out.title = next();
else if (a === '--status') out.status = next();
else if (a === '--template') out.template = next();
else if (a === '--strategy') out.strategy = next();
else if (a === '--deciders') out.deciders = next();
else if (a === '--consulted') out.consulted = next();
else if (a === '--informed') out.informed = next();
else if (a === '--technical-story') out.technicalStory = next();
else if (a === '--chosen-option') out.chosenOption = next();
else if (a === '--update-index') out.updateIndex = true;
else if (a === '--index-file') out.indexFile = next();
else if (a === '--json') out.json = true;
else if (a === '--help' || a === '-h') {
process.stdout.write(
[
'Usage: node new_adr.js --title "Choose database" [options]',
'',
'Options:',
' --repo-root <path> Repo root (default: .)',
' --dir <path> ADR directory (default: auto-detect, else adr/)',
' --no-create-dir Do not create ADR directory if missing',
' --status <value> ADR status (default: proposed)',
' --template simple|madr Template (default: simple)',
' --strategy auto|date|slug Filename strategy (default: auto)',
' --deciders "a,b" Deciders list',
' --consulted "a,b" Consulted experts (RACI)',
' --informed "a,b" Informed stakeholders (RACI)',
' --technical-story <x> Issue/ticket/PR link or short ref',
' --chosen-option <x> MADR template: chosen option label',
' --update-index Update adr/README.md (or existing index)',
' --index-file <path> Override index file (relative to repo root unless absolute)',
' --json Output machine-readable JSON (default: off)',
'',
].join('\n'),
);
process.exit(0);
} else {
die(`Unknown arg: ${a}`);
}
}
if (!out.title) die('Missing required --title');
if (!['simple', 'madr'].includes(out.template))
die(`Invalid --template: ${out.template}`);
if (!['auto', 'date', 'slug'].includes(out.strategy))
die(`Invalid --strategy: ${out.strategy}`);
return out;
}
function detectAdrDir(repoRoot) {
const candidates = [
path.join(repoRoot, 'contributing', 'decisions'),
path.join(repoRoot, 'docs', 'decisions'),
path.join(repoRoot, 'adr'),
path.join(repoRoot, 'docs', 'adr'),
path.join(repoRoot, 'docs', 'adrs'),
path.join(repoRoot, 'decisions'),
];
for (const p of candidates) {
try {
if (fs.statSync(p).isDirectory()) return p;
} catch {
// ignore
}
}
return null;
}
function listMdFiles(dir) {
let entries = [];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return [];
}
return entries
.filter(e => e.isFile() && e.name.toLowerCase().endsWith('.md'))
.map(e => e.name);
}
function detectStrategy(adrDir) {
const md = listMdFiles(adrDir);
for (const name of md) {
if (/^\d{4}-\d{2}-\d{2}-/.test(name)) return 'date';
}
if (md.length > 0) return 'slug';
return 'date';
}
function todayISO() {
return new Date().toISOString().slice(0, 10);
}
function loadTemplate(templateName) {
const skillRoot = path.resolve(__dirname, '..');
const templatePath = path.join(
skillRoot,
'assets',
'templates',
`adr-${templateName}.md`,
);
if (!fs.existsSync(templatePath)) die(`Template not found: ${templatePath}`);
return fs.readFileSync(templatePath, 'utf8');
}
function renderTemplate(raw, vars) {
// Handle YAML front matter placeholders (quoted and unquoted)
let out = raw;
// YAML front matter fields — replace the whole placeholder pattern
// e.g. status: "{proposed | accepted | ...}" → status: proposed
out = out.replace(
/^(status:\s*)["']?\{[^}]*\}["']?\s*$/m,
`$1${vars.status}`,
);
out = out.replace(/^(date:\s*)\{[^}]*\}\s*$/m, `$1${vars.date}`);
out = out.replace(
/^(decision-makers:\s*)["']?\{[^}]*\}["']?\s*$/m,
`$1${vars.deciders || ''}`,
);
// consulted / informed: replace if a value was provided, otherwise remove the
// entire line so we don't leak placeholder text like "{list everyone...}"
if (vars.consulted) {
out = out.replace(
/^(consulted:\s*)["']?\{[^}]*\}["']?\s*$/m,
`$1${vars.consulted}`,
);
} else {
out = out.replace(/^consulted:\s*["']?\{[^}]*\}["']?\s*\n/m, '');
}
if (vars.informed) {
out = out.replace(
/^(informed:\s*)["']?\{[^}]*\}["']?\s*$/m,
`$1${vars.informed}`,
);
} else {
out = out.replace(/^informed:\s*["']?\{[^}]*\}["']?\s*\n/m, '');
}
// Replace MADR-style heading placeholder
out = out.replace(/^(#\s+)\{short title[^}]*\}\s*$/m, `$1${vars.title}`);
// Inline placeholders (title in heading, etc.)
out = out
.replaceAll('{TITLE}', vars.title)
.replaceAll('{STATUS}', vars.status)
.replaceAll('{DATE}', vars.date)
.replaceAll('{DECIDERS}', vars.deciders)
.replaceAll('{TECHNICAL_STORY}', vars.technicalStory)
.replaceAll('{CHOSEN_OPTION}', vars.chosenOption);
return out;
}
function chooseIndexFile(adrDir) {
for (const name of ['README.md', 'index.md']) {
const p = path.join(adrDir, name);
if (fs.existsSync(p)) return p;
}
return path.join(adrDir, 'README.md');
}
function insertIndexEntryUnderHeading(lines, headingRegex, entryLine) {
// Returns { lines, inserted }
const headingIndex = lines.findIndex(l => headingRegex.test(l));
if (headingIndex === -1) return { lines, inserted: false };
let sectionEnd = lines.length;
for (let i = headingIndex + 1; i < lines.length; i++) {
if (/^##\s+/.test(lines[i])) {
sectionEnd = i;
break;
}
}
// Prefer inserting at end of list in this section if there is a list.
let lastListItem = -1;
for (let i = sectionEnd - 1; i > headingIndex; i--) {
if (/^[-*]\s+/.test(lines[i])) {
lastListItem = i;
break;
}
}
const insertAt = lastListItem !== -1 ? lastListItem + 1 : sectionEnd;
const out = [...lines];
// Ensure there's a blank line after the heading if we're inserting immediately after it.
if (insertAt === headingIndex + 1 && out[insertAt] !== '') {
out.splice(insertAt, 0, '');
}
out.splice(insertAt, 0, entryLine);
return { lines: out, inserted: true };
}
function updateIndex(indexFile, { relLink, title, status, date }) {
let content = '';
if (fs.existsSync(indexFile)) content = fs.readFileSync(indexFile, 'utf8');
else content = '# ADR Log\n\n';
if (content.includes(relLink)) return false;
const normalized = content.replace(/\r\n/g, '\n');
const hadTrailingNewline = normalized.endsWith('\n');
let lines = normalized.split('\n');
// Normalize away the trailing empty split element so insertion math is sane.
if (
hadTrailingNewline &&
lines.length > 0 &&
lines[lines.length - 1] === ''
) {
lines = lines.slice(0, -1);
}
const entryLine = `- [${title}](${relLink}) (${status}, ${date})`;
// Prefer inserting under "## ADRs" if it exists, otherwise append at EOF.
const r = insertIndexEntryUnderHeading(lines, /^##\s+ADRs\s*$/i, entryLine);
const nextLines = r.inserted ? r.lines : [...lines, entryLine];
let next = nextLines.join('\n');
if (hadTrailingNewline) next += '\n';
fs.mkdirSync(path.dirname(indexFile), { recursive: true });
fs.writeFileSync(indexFile, next, 'utf8');
return true;
}
function main() {
const args = parseArgs(process.argv);
const repoRoot = path.resolve(process.cwd(), args.repoRoot);
if (!fs.existsSync(repoRoot)) die(`Repo root does not exist: ${repoRoot}`);
let adrDir;
if (args.dir) adrDir = path.resolve(repoRoot, args.dir);
else adrDir = detectAdrDir(repoRoot) || path.join(repoRoot, 'adr');
if (!fs.existsSync(adrDir)) {
if (args.noCreateDir) die(`ADR directory does not exist: ${adrDir}`);
fs.mkdirSync(adrDir, { recursive: true });
}
let strategy = args.strategy;
if (strategy === 'auto') strategy = detectStrategy(adrDir);
const title = String(args.title).trim();
const slug = slugify(title);
const today = todayISO();
let filename;
if (strategy === 'date') {
filename = `${today}-${slug}.md`;
} else {
filename = `${slug}.md`;
}
let out = path.join(adrDir, filename);
if (fs.existsSync(out)) {
if (strategy === 'date') die(`ADR already exists: ${out}`);
let i = 2;
while (true) {
const candidate = path.join(adrDir, `${slug}-${i}.md`);
if (!fs.existsSync(candidate)) {
out = candidate;
break;
}
i++;
}
}
const deciders = String(args.deciders || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
.join(', ');
const consulted = String(args.consulted || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
.join(', ');
const informed = String(args.informed || '')
.split(',')
.map(s => s.trim())
.filter(Boolean)
.join(', ');
const raw = loadTemplate(args.template);
const rendered = renderTemplate(raw, {
title,
status: String(args.status).trim(),
date: today,
deciders,
consulted,
informed,
technicalStory: String(args.technicalStory || '').trim(),
chosenOption: String(args.chosenOption || '').trim(),
});
fs.writeFileSync(out, `${rendered.trimEnd()}\n`, 'utf8');
let updatedIndexPath = null;
let indexChanged = false;
if (args.updateIndex) {
let indexFile;
if (args.indexFile) {
indexFile = path.isAbsolute(args.indexFile)
? args.indexFile
: path.resolve(repoRoot, args.indexFile);
} else {
indexFile = chooseIndexFile(adrDir);
}
const relLink = toPosix(path.relative(path.dirname(indexFile), out));
indexChanged = updateIndex(indexFile, {
relLink,
title,
status: String(args.status).trim(),
date: today,
});
updatedIndexPath = indexFile;
}
if (args.json) {
const payload = {
repoRoot,
adrDir,
createdAdrPath: out,
createdAdrRelPath: toPosix(path.relative(repoRoot, out)),
title,
status: String(args.status).trim(),
template: args.template,
strategy,
date: today,
indexUpdated: Boolean(updatedIndexPath),
indexChanged,
indexPath: updatedIndexPath,
indexRelPath: updatedIndexPath
? toPosix(path.relative(repoRoot, updatedIndexPath))
: null,
};
process.stdout.write(`${JSON.stringify(payload)}\n`);
} else {
process.stdout.write(`${out}\n`);
}
}
main();
#!/usr/bin/env node
/**
* Update an ADR's status in-place.
*
* Supported patterns:
* - Bullet status: "- Status: proposed" or "* Status: proposed"
* - Nygard-style section: "## Status" followed by a single-line status value
*/
const fs = require('node:fs');
const path = require('node:path');
function die(msg) {
process.stderr.write(`${msg}\n`);
process.exit(1);
}
function toPosix(p) {
return p.split(path.sep).join('/');
}
function parseArgs(argv) {
if (argv.includes('--help') || argv.includes('-h')) {
process.stdout.write(
[
'Usage: node set_adr_status.js <path> --status <value> [--json]',
'',
'Example:',
' node set_adr_status.js adr/2025-06-15-foo.md --status accepted',
'',
].join('\n'),
);
process.exit(0);
}
if (argv.length < 3) die('Missing <path>');
const file = argv[2];
let status = null;
let json = false;
for (let i = 3; i < argv.length; i++) {
const a = argv[i];
if (a === '--status') {
if (i + 1 >= argv.length) die('Missing value for --status');
status = argv[++i];
} else if (a === '--json') {
json = true;
} else {
die(`Unknown arg: ${a}`);
}
}
if (!status) die('Missing required --status');
return { file, status: String(status).trim(), json };
}
function setYamlFrontMatterStatus(lines, newStatus) {
// YAML front matter: starts with '---', ends with next '---'
if (lines.length < 2 || lines[0].trim() !== '---')
return { lines, changed: false };
let changed = false;
const out = [];
let inFrontMatter = true;
let passedOpening = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (i === 0 && line.trim() === '---') {
passedOpening = true;
out.push(line);
continue;
}
if (passedOpening && inFrontMatter && line.trim() === '---') {
inFrontMatter = false;
out.push(line);
continue;
}
if (passedOpening && inFrontMatter && /^status\s*:/.test(line)) {
out.push(`status: ${newStatus}`);
changed = true;
continue;
}
out.push(line);
}
return { lines: out, changed };
}
function setBulletStatus(lines, newStatus) {
let changed = false;
const out = lines.map(line => {
const m = line.match(/^([*-])\s*Status:\s*(.*)$/);
if (!m) return line;
changed = true;
return `${m[1]} Status: ${newStatus}`;
});
return { lines: out, changed };
}
function setSectionStatus(lines, newStatus) {
let changed = false;
const out = [];
for (let i = 0; i < lines.length; i++) {
out.push(lines[i]);
if (!/^##\s+Status\s*$/.test(lines[i])) continue;
// Replace next non-empty, non-heading line. If not found, insert.
let j = i + 1;
while (j < lines.length && lines[j].trim() === '') {
out.push(lines[j]);
j++;
}
if (j < lines.length && !/^##\s+/.test(lines[j])) {
out.push(newStatus);
changed = true;
i = j; // skip original status line
continue;
}
out.push(newStatus);
changed = true;
i = j - 1;
}
return { lines: out, changed };
}
function main() {
const args = parseArgs(process.argv);
const filePath = path.resolve(process.cwd(), args.file);
if (!fs.existsSync(filePath)) die(`File not found: ${filePath}`);
const content = fs.readFileSync(filePath, 'utf8');
const hadTrailingNewline = content.endsWith('\n');
const lines = content.replace(/\r\n/g, '\n').split('\n');
let r = setYamlFrontMatterStatus(lines, args.status);
if (!r.changed) r = setBulletStatus(lines, args.status);
if (!r.changed) r = setSectionStatus(lines, args.status);
if (!r.changed) {
die(
"Could not find a status to update. Expected YAML front matter 'status:', '- Status:'/'* Status:', or a '## Status' section.",
);
}
const newContent = r.lines.join('\n') + (hadTrailingNewline ? '\n' : '');
fs.writeFileSync(filePath, newContent, 'utf8');
if (args.json) {
process.stdout.write(
`${JSON.stringify({
filePath,
fileRelPath: toPosix(path.relative(process.cwd(), filePath)),
status: args.status,
changed: true,
})}\n`,
);
} else {
process.stdout.write(`${filePath}\n`);
}
}
main();
Related skills
FAQ
What does adr-skill do?
Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows. Use when you need to propose, write, update, accept/reject, deprecate, or supersede an ADR; bootstrap an adr folder and ind
When should I use adr-skill?
Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows. Use when you need to propose, write, update, accept/reject, deprecate, or supersede an ADR; bootstrap an adr folder and ind
What are common prerequisites?
--- name: adr-skill description: Create and maintain Architecture Decision Records (ADRs) optimized for agentic coding workflows.