
Adr Skill
- 46 installs
- 61 repo stars
- Updated August 4, 2026
- joelhooks/joelclaw
Creates ADRs as executable specs for coding agents, capturing intent via Socratic questioning plus an implementation plan.
About
Produces self-contained Architecture Decision Records with explicit constraints, implementation plans, and required-skills preflight, treating ADRs as agent-executable specs. A developer uses it to propose, update, or supersede an ADR.
- ADRs as executable specifications with measurable constraints
- Socratic intent capture and agent-readiness checklist plus implementation plan
Adr Skill by the numbers
- 46 all-time installs (skills.sh)
- Ranked #828 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/joelhooks/joelclaw --skill adr-skillAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 46 |
|---|---|
| repo stars | ★ 61 |
| Last updated | August 4, 2026 |
| Repository | joelhooks/joelclaw ↗ |
What it does
Creates ADRs as executable specs for coding agents, capturing intent via Socratic questioning plus an implementation plan.
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
- The ADR must include a required-skills preflight list — which skills must be loaded before implementation starts, and why each one is needed
- When architecture shape is important, capture visual artifacts (diagrams/review pages) and link them from the ADR
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 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-NNNN references in comments and docs (see "Code ↔ ADR Linking" below). This reveals which existing decisions govern which parts of the codebase.
5. Draft required-skills preflight. Based on impacted areas, list the skills that must be loaded before implementation starts. Use canonical skill names and include a one-line reason per skill.
6. Note what you found. Carry this context into Phase 1 — it will sharpen your questions and prevent the ADR from contradicting existing decisions.
7. Capture a visual baseline when helpful. If visual-explainer is available, run /project-recap for unfamiliar systems or /generate-web-diagram for current architecture. Use this as pre-decision context, not as a replacement for written reasoning.
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. Which skills are required before implementation starts? — List canonical skill names and why each is needed. If coverage is unclear, call out the gap and queue find-skills. 5. What does success look like? — Measurable outcomes. Push past "it works" to specifics (latency, throughput, DX, maintenance burden). 6. 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. 7. What's your current lean? — Capture gut intuition early. Often reveals unstated priorities. 8. Who needs to know or approve? — Decision-makers, consulted experts, informed stakeholders. 9. 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. 10. Would visual artifacts reduce ambiguity? — If yes, decide which outputs are required (/generate-web-diagram, /plan-review, /diff-review) and where links should live.
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}
- Required skills (preflight): {skill names + why needed}
- 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}
- Visual artifacts: {which diagrams/reviews to generate, if any}
>
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
docs/decisions/(MADR default) oradr/(simpler repos).
2. Choose a filename strategy.
- If existing ADRs use numeric prefixes (
0001-...), 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 (including required skills preflight). This is the most important section for agent-first ADRs. It tells the next agent exactly what to do and which skills to load before starting. 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 visual artifacts (recommended for architecture-impacting ADRs).
- Use
/generate-web-diagramfor proposed architecture or flow. - Use
/plan-review <adr-file>after drafting to validate Implementation Plan coverage. - If superseding/changing an existing architecture, use
/diff-reviewfor before/after comparison. - Record output HTML paths and link them in
## Visual Artifacts(or## More Informationif the section is omitted).
8. 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.
For ADRs with visuals, run /fact-check <visual-or-adr-output> if available before final recommendation to verify claims against the codebase.
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-NNNNreference in a code comment
How to Consult ADRs
1. Find the ADR directory. Check 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-NNNN 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
- **Required skills (load before implementation starts)**: `inngest-steps` (durable step patterns), `system-bus` (repo conventions)
- **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-0004: Using better-sqlite3 for test database
// See: docs/decisions/0004-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 everything that needs updating
ADR ↔ Visual Linking
Architecture-changing ADRs should also link to generated visual artifacts.
ADR → Visual
Include a ## Visual Artifacts section (or use ## More Information) with stable paths to generated HTML, for example:
## Visual Artifacts
- Proposed architecture: `docs/decisions/diagrams/0007-auth-flow.html`
- Plan validation: `docs/decisions/diagrams/0007-plan-review.html`Visual → ADR
Name visual files with ADR IDs/slugs where possible (for example 0007-auth-flow.html) so future agents can map pages back to the decision quickly.
ADR Hunting — Finding the Next One to Work On
When the current task is done and it's time to pick the next ADR, use the rubric-based ranking.
Quick Path
joelclaw vault adr nextReturns the top 3 candidates from proposed/accepted ADRs, sorted by the NRC+Novelty rubric (Need × 0.5 + Readiness × 0.3 + Confidence × 0.2, adjusted by Novelty). Each candidate shows score, band, axis breakdown, and rationale.
Decision Criteria
When choosing between candidates:
1. Readiness 5/5 trumps higher score — if two ADRs are close in score but one has readiness 5/5, pick that one. It means the implementation path is clear and you can ship in one session. 2. Accepted > Proposed — accepted means Joel already approved the decision. Proposed may need design discussion first. 3. Check if already shipped — some ADRs are implemented but not marked shipped. Before starting work, grep the codebase for evidence the ADR is already done (like ADR-0104 was). 4. Compound value — prefer ADRs that build on recently shipped work. If you just shipped memory injection, the next gateway improvement compounds on that.
Full Diagnostics
# See all open ADRs with rubric ranking
joelclaw vault adr rank
# Show 10 candidates
joelclaw vault adr next --count 10
# Read a specific candidate
joelclaw vault read ADR-0194After Picking
1. Read the full ADR: joelclaw vault read ADR-XXXX 2. Accept it if still proposed: update status: accepted in frontmatter 3. Check if it's already (partially) implemented — grep codebase before building 4. Update status to shipped when done, with a verification section
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).
Mandatory: Sync to System Knowledge
After ANY ADR create, update, accept, reject, deprecate, or supersede:
joelclaw send system/adr.sync.requested -d '{"source":"adr-skill"}'This syncs the ADR to the system_knowledge Typesense collection so agents can find it via semantic search. This is not optional. If you skip it, the brain is stale and agents make decisions without current context.
For bulk sync (e.g., after editing multiple ADRs):
joelclaw knowledge syncPost-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: "Implements ADR-0004." 3. Add code references. Add ADR-NNNN 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. 6. Refresh visual artifacts if architecture drifted. Regenerate diagrams/reviews and keep ADR links current.
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/
0001-use-postgres.md
frontend/
0001-use-react.md
infrastructure/
0001-use-terraform.mdNumbers are local to each category. Choose a categorization scheme early (by layer, by domain, by team) and document it in the index.
Resources
Visual Explainer (optional companion)
If visual-explainer is installed as a skill, use it to generate rich HTML visualizations of ADR content.
Install:
pi install https://github.com/nicobailon/visual-explainerPrimary commands:
- `/diff-review` — before/after architecture diagrams when an ADR changes system design
- `/plan-review <adr-file>` — cross-reference ADR implementation plan against actual codebase
- `/project-recap` — architecture snapshot useful when writing new ADRs for unfamiliar areas
- `/generate-web-diagram` — Mermaid architecture diagrams to embed or reference from ADRs
- `/fact-check <doc-or-html>` — verify review/plan claims against the current codebase
These produce self-contained HTML pages in ~/.agent/diagrams/ with light/dark themes and interactive Mermaid diagrams.
Recommended ADR flow:
1. Phase 0: /project-recap (optional baseline) 2. Phase 2: /generate-web-diagram (proposed architecture) 3. Phase 3: /plan-review <adr-file> and /fact-check <output> 4. Save durable artifacts under the repo (for example docs/decisions/diagrams/) and link from the ADR
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.
interface:
icon_small: "./assets/small-logo.svg"
icon_large: "./assets/large-logo.png"
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-label="ADR skill logo">
<rect width="128" height="128" rx="24" fill="#1f2937"/>
<rect x="16" y="16" width="96" height="96" rx="16" fill="#f59e0b"/>
<path d="M32 84V44h10l12 26 12-26h10v40h-9V60l-10 22h-6L41 60v24h-9z" fill="#111827"/>
<circle cx="94" cy="38" r="8" fill="#111827"/>
</svg>
{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.}
- Required skills (load before implementation starts): {canonical skill names + one-line reason each}
- 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 -->
Visual Artifacts
- Proposed architecture: {path to
/generate-web-diagramoutput} - Plan validation: {path to
/plan-review <adr-file>output} - Change diff: {path to
/diff-reviewoutput, if this ADR replaces/supersedes an existing design}
<!-- 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:
- Prefer numbered files when starting fresh:
0001-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
- Required skills (load before implementation starts): {canonical skill names + one-line reason each}
- 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 -->
Visual Artifacts
- Proposed architecture: {path to
/generate-web-diagramoutput} - Plan validation: {path to
/plan-review <adr-file>output} - Change diff: {path to
/diff-reviewoutput, if this ADR replaces/supersedes an existing design}
<!-- 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): docs/decisions/, adr/, docs/adr/, docs/adrs/, decisions/.
Filename Conventions
Pattern: NNNN-title-with-dashes.md
NNNNis a zero-padded sequential number (assume max 9,999 ADRs per directory).- Title uses lowercase, dashes, present-tense imperative verb phrase.
- Examples:
0001-choose-database.md,0002-adopt-adrs.md
If a repo already uses slug-only filenames (no numeric 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
- Implementation Plan includes a required-skills preflight list (canonical names + why each skill is needed before work starts)
Status Values
Track status in YAML front matter:
---
status: proposed
date: 2025-06-15
decision-makers: Alice, Bob
---Common statuses (must be a single canonical word — never include references in the status field):
| Status | Meaning |
|---|---|
proposed | Under discussion, not yet decided |
accepted | Decision is active and should be followed |
implemented | Accepted and fully implemented in code |
rejected | Considered but explicitly not adopted |
deprecated | Was accepted but no longer applies — explain replacement path |
superseded | Replaced by a newer ADR — use superseded-by frontmatter field for the link, NOT in the status value |
Wrong: status: "superseded by 0033" — this creates junk filter values on the website. Right: status: superseded + superseded-by: "[ADR-0033](0033-foo.md)"
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:
docs/decisions/
backend/
0001-use-postgres.md
frontend/
0001-use-react.md
infrastructure/
0001-use-terraform.mdNumbers 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
* **Required skills (load before implementation starts)**: `adr-skill` (decision structure + review gate), `next-best-practices` (app-level implementation hygiene)
* **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:
````markdown --- 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: ADR-0003 Use PostgreSQL for production — 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
- Required skills (load before implementation starts):
adr-skill(decision conformance),next-best-practices(implementation constraints),nextjs-testing(verification criteria design) - Affected paths:
src/db/client.ts— new: unified database interface (DatabaseClient type + factory function)src/db/sqlite-client.ts— new: SQLite implementation of DatabaseClientsrc/db/pg-client.ts— refactor: extract current inline pg usage into DatabaseClient implementationsrc/db/repositories/*.ts— update: use DatabaseClient instead of direct pg callstests/setup.ts— update: initialize SQLite by default, readDB_ENGINEenv vartests/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 CIpackage.json— add devDependencies- Dependencies: add
better-sqlite3@11.x,@types/better-sqlite3@7.xas 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.tsfor the expected style - Patterns to avoid:
- Do NOT import
better-sqlite3orpgdirectly outsidesrc/db/ - Do NOT use PostgreSQL-specific SQL in shared queries: no
JSONBoperators (->,->>), noANY(ARRAY[...]), noON CONFLICT ... DO UPDATE - Do NOT use SQLite-specific SQL either — keep queries portable
- Configuration:
DB_ENGINEenv var (sqlite|postgres), defaults tosqlitein test,postgresin 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 testpasses (all integration tests) - [ ]
DB_ENGINE=postgres npm testpasses 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.tsexportsDatabaseClientinterface andcreateClient()factory - [ ]
.env.testsetsDB_ENGINE=sqlite - [ ] Weekly PostgreSQL compatibility CI job exists in
.github/workflows/
Pros and Cons of the Options
SQLite via better-sqlite3
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 — 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: ADR-0003 Use PostgreSQL for production
- Revisit trigger: if dialect-drift bugs exceed 2 per quarter, reconsider Docker PostgreSQL approach
- Code references: after implementation, key files will have
// ADR-0004comments at entry points
````
ADR 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
- [ ] Required skills preflight list exists with canonical skill names and one-line purpose per skill
- [ ] 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")
Visual Artifacts (when architecture changes)
- [ ] ADR includes a
Visual Artifactssection (or equivalent links inMore Information) - [ ] At least one proposed-architecture visual exists (
/generate-web-diagramor equivalent) - [ ] Implementation plan was visually validated (
/plan-review <adr-file>or equivalent evidence) - [ ] If superseding/replacing design, before/after visual diff exists (
/diff-reviewor equivalent) - [ ] Visual files have stable names/paths linked from the ADR (prefer ADR ID in filename)
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 |
| Architecture ADR has no visuals | Hard to review system impact | Generate /generate-web-diagram and /plan-review, then link outputs in ADR |
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 — required-skills preflight, 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
- Optional Visual Artifacts section — links to
/generate-web-diagram,/plan-review,/diff-reviewoutputs when architecture impact needs visual review - 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: "number",
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 number|slug|auto Filename strategy for initial ADR (default: number)",
" --json Output machine-readable JSON (default: off)",
"",
].join("\n")
);
process.exit(0);
} else {
die(`Unknown arg: ${a}`);
}
}
if (!["auto", "number", "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 \`NNNN-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
## Implementation Plan
* **Required skills (load before implementation starts)**: \`adr-skill\` (canonical ADR workflow and review checklist)
* **Affected paths**: \`${adrDir}/\`, \`${adrDir}/README.md\`
* **Dependencies**: none
* **Patterns to follow**: keep ADR files self-contained and use repository naming conventions
* **Patterns to avoid**: do not store ADR decisions in external docs without linking back into version control
### Verification
- [ ] ADR directory exists at \`${adrDir}/\`
- [ ] ADR index exists and links to the first ADR
- [ ] First ADR file exists and is readable from the repo
## 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" ? "number" : args.strategy;
let firstAdrFilename;
if (strategy === "number") {
firstAdrFilename = `0001-${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 | number | 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|number|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", "number", "slug"].includes(out.strategy)) die(`Invalid --strategy: ${out.strategy}`);
return out;
}
function detectAdrDir(repoRoot) {
const candidates = [
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) {
// Heuristic:
// - If any ADR looks numbered -> use numbering.
// - Else if there are any markdown files at all -> use slug (assume existing convention).
// - Else (empty/new) -> default to numbering.
const md = listMdFiles(adrDir);
for (const name of md) {
if (/^\d+-/.test(name)) return "number";
}
if (md.length > 0) return "slug";
return "number";
}
function detectNumberingWidth(adrDir) {
const md = listMdFiles(adrDir);
for (const name of md) {
const m = name.match(/^(\d+)-/);
if (m) return m[1].length;
}
return null;
}
function nextNumber(adrDir) {
const md = listMdFiles(adrDir);
let maxN = 0;
for (const name of md) {
const m = name.match(/^(\d+)-/);
if (!m) continue;
const n = Number.parseInt(m[1], 10);
if (Number.isFinite(n)) maxN = Math.max(maxN, n);
}
return maxN ? maxN + 1 : 1;
}
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);
let filename;
if (strategy === "number") {
const width = detectNumberingWidth(adrDir) || 4;
const n = nextNumber(adrDir);
filename = `${String(n).padStart(width, "0")}-${slug}.md`;
} else {
filename = `${slug}.md`;
}
let out = path.join(adrDir, filename);
if (fs.existsSync(out)) {
if (strategy === "number") 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 today = new Date().toISOString().slice(0, 10);
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/0001-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();