Skip to content

Decision records

cloop adr keeps Architectural Decision Records (ADRs). Each record is a file about one significant design choice: the context that led to it, the decision, and its consequences.

Records live in .cloop/adr/ as Markdown with YAML frontmatter, so you review them in pull requests like any other change.

Why this is not the journal

cloop has two places to record why something was done, and they answer different questions.

Task journal (cloop task journal) Decision record (cloop adr)
Scope One task The project
Lifetime As long as the task is relevant Outlives the tasks that led to it
Shape Append-only entries One document with a status
Question "Why did this task go the way it did?" "Why is the system built this way?"

A journal entry explaining that a task switched from polling to a file watcher is useful for a week or so. A decision such as using SQLite with WAL as the only state store belongs in a record, which tells a reader two years later who made it, what was given up for it, and whether it still holds.

The lifecycle

You don't delete or rewrite a decision. You change its status, and you record a reversal as a new record that supersedes the old one. The statuses are:

  • Proposed: written but not yet agreed. New records start here.
  • Accepted: in force. The system works this way.
  • Deprecated: no longer how things are done, and nothing directly replaces it.
  • Superseded: replaced by a later record, which this one links to.
  • Rejected: considered and declined. Keeping it saves the next person from proposing the same thing and getting the same answer.

Keep superseded and rejected records. The reason a decision was reversed is often more useful than the decision itself, and without the record the next reader has to rediscover the constraint.

Commands

cloop adr new <title>            Create a record (Proposed by default)
cloop adr list                   List records, oldest first
cloop adr show <id>              Print one record with its metadata
cloop adr status <id> <status>   Move a record through the lifecycle
cloop adr supersede <new> <old>  Record that one decision replaces another

new accepts --deciders and --tags as comma-separated lists, --status to start a record at a status other than Proposed, and --body for the Markdown body. --body - reads the body from stdin, so you can pipe in a record you wrote in an editor. Without a body, the record gets a template with Context, Decision, Consequences and Alternatives Considered sections.

list takes --status to filter and --json for machine-readable output, and show takes --json. Statuses are case-insensitive. Any value other than the five above is refused with an error, so a typo can't be saved as a sixth status that no filter would match.

A worked example

$ cloop adr new "Use SQLite for state" --deciders alice,bob --tags storage
Created ADR-0001: Use SQLite for state
.cloop/adr/0001-use-sqlite-for-state.md

$ cloop adr status 1 accepted
ADR-0001 is now Accepted

A year later the decision changes. Write a new record and link the two. You don't edit the old record by hand:

$ cloop adr new "Move state to Postgres"
Created ADR-0002: Move state to Postgres

$ cloop adr supersede 2 1
ADR-0002 supersedes ADR-0001

$ cloop adr list
ID    STATUS      DATE        TITLE
0001  Superseded  2026-03-01  Use SQLite for state (superseded by ADR-0002)
0002  Proposed    2027-04-11  Move state to Postgres

supersede updates both files. The new record gets a supersedes link, and the old one gets the reverse link and moves to Superseded. Because both links are stored, a reader who opens either record can find the other.

File format

---
id: 1
title: "Use SQLite for state"
status: Accepted
date: 2026-03-01
deciders: ["alice", "bob"]
tags: ["storage"]
superseded_by: [2]
---

# ADR-0001: Use SQLite for state

## Context
...

Every field except id and title is optional. A file with no frontmatter at all is still listed: cloop reads the id and title from the filename, which has the form <zero-padded-id>-<slug>.md, so records written by hand or by older tools aren't skipped.

Writes are atomic (a temporary file next to the record, then a rename), so an interrupted status change can't leave half-written frontmatter that would drop the record from listings.

The implementation is pkg/adr/adr.go.