Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
},
"metadata": {
"description": "Decision memory for human- and agent-authored plans.",
"version": "0.2.0"
"version": "0.3.0"
},
"plugins": [
{
"name": "adrkit",
"source": "./packages/adapters/agent-plugin",
"description": "Load the architecture decisions that govern a change, check work against them, and audit code, documentation, and history for decisions that were never recorded. Ships decision-memory and decision-backfill skills, a read-only decision-checker agent, and five slash commands, all driven by the adr CLI. Ships no MCP server: adrkit's MCP server is configured per project, and the skills use its tools when they are connected.",
"version": "0.2.0",
"version": "0.3.0",
"author": {
"name": "Mark Beacom",
"url": "https://github.com/mbeacom"
Expand Down
22 changes: 19 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,11 @@ validation. Scope and limitations:
That functional evidence covers the v0.1.0 context/check/draft/queue baseline.
The v0.2.0 backfill skill and command are contract- and static-host-validated.
A fresh Copilot synthetic-consumer run produced the expected covered/history/new
classification and a complete handoff without changing the worktree. No
persistent reference-repository or external run exists.
classification and a complete handoff without changing the worktree. For the
v0.3.0 bootstrap-record offer, detection is measured against synthetic corpora
(missing, empty, source-only, process-record-present, and unmigrated MADR);
host surfacing behavior is unverified. No persistent reference-repository or
external run exists.

Things that are load-bearing and easy to break — each measured against the real
hosts rather than read off their docs, so a change that "looks more correct"
Expand Down Expand Up @@ -267,7 +270,20 @@ will usually be a regression:
treats source text as untrusted data, stays inside the worktree, enforces
explicit scan caps, and requires confirmation before running a CLI resolved
inside an inherited repository.
- `copilot plugin install` prints only a skill count. Version 0.2.0 should report
- **The bootstrap record is an offer, not a candidate.** A repository with no
corpus is missing the process decision (keep decisions in git) and the tooling
decision (enforce them with adrkit). Nothing proves a human ratified either,
so it stays out of the candidates table and out of every `backfillHandoff` —
it governs the corpus directory, a glob, and could never supply concrete
`candidatePaths` — and routes to plain `/adr-draft`. Adopting adrkit is never
a supersession of the decision to record decisions; `supersedes` is reserved
for a prior *tooling* record, and a MADR corpus is migrated, not superseded.
Detection reads `adr check`'s exit code **before** its `governing` bucket: an
unmigrated MADR corpus returns an empty bucket at exit `1` because nothing
parses, so reading the bucket alone offers a duplicate of a process record the
repository already has
([ADR-0038](./docs/adr/0038-offer-the-bootstrap-decision-record-as-an-offer-rather-than-a-backfill-candidate.md)).
- `copilot plugin install` prints only a skill count. Version 0.3.0 should report
two skills; that does not inventory the agent or commands — verify them in a
fresh session.

Expand Down
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ Until `1.0.0`, minor releases may include breaking changes

## [Unreleased]

### Added

- **The agent plugin offers the bootstrap decision record (`adrkit` 0.3.0).** A
repository with no ADR corpus, or one whose corpus never recorded why it keeps
decisions, is now offered the process decision (keep decisions in git) and the
tooling decision (enforce them with adrkit). It is an **offer, not a
candidate**: no source span proves a human ratified either choice, so it fails
backfill's evidence rule and is kept out of the candidates table and out of
every `backfillHandoff` — whose concrete `candidatePaths` it could never supply,
since the path it governs is the corpus directory. It routes to plain
`/adr-draft` instead. Adopting adrkit is never a supersession of the decision
to record decisions: `supersedes` is reserved for a prior *tooling* record,
a MADR corpus is migrated rather than superseded, and the governing process
record is detected through `adr check`'s `governing` bucket rather than by
hand-parsing frontmatter — reading `adr check`'s **exit code before its
`governing` bucket**, because an unmigrated MADR corpus returns an empty
bucket at exit `1` when nothing parses, and offering the process decision
there would duplicate a record the repository already has. Detection is
measured against synthetic corpora; host surfacing behavior is unverified
([ADR-0038](docs/adr/0038-offer-the-bootstrap-decision-record-as-an-offer-rather-than-a-backfill-candidate.md)).

## [spec-kit-0.1.4] - 2026-09-13

### Changed
Expand Down
5 changes: 3 additions & 2 deletions MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ hand-written.

<!-- Generated by `bun run emit:manifest`; do not hand-edit. -->

There are 37 records, ids `0001`-`0037`, alongside
`0000-template.md`: 34 accepted, 1 proposed, 2 superseded.
There are 38 records, ids `0001`-`0038`, alongside
`0000-template.md`: 34 accepted, 2 proposed, 2 superseded.

| Id | Status | Title |
| --- | --- | --- |
Expand Down Expand Up @@ -115,5 +115,6 @@ There are 37 records, ids `0001`-`0037`, alongside
| `0035` | accepted | Execute the gates that certify a pull request from the default branch |
| `0036` | accepted | Expose the governing-decisions Action through one root Marketplace entry point |
| `0037` | proposed | Treat generated knowledge systems as downstream read models, not decision authorities |
| `0038` | proposed | Offer the bootstrap decision record as an offer rather than a backfill candidate |

<!-- END GENERATED: adr-inventory -->
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
schemaVersion: 0.1.0
id: "0038"
title: "Offer the bootstrap decision record as an offer rather than a backfill candidate"
status: proposed
date: 2026-09-08
deciders:
- "@mbeacom"
tags:
- agent-plugin
- backfill
- governance
scope: component
reversibility: two-way-door
blastRadius: component
relatesTo:
- "0001"
- "0008"
- "0016"
- "0028"
- "0034"
affects:
- type: path
pattern: "packages/adapters/agent-plugin/**"
- type: path
pattern: "site/src/content/docs/backfill.mdx"
- type: path
pattern: "site/src/content/docs/quickstart.mdx"
- type: path
pattern: "docs/reference-verification-agent-plugin.md"
provenance:
authoredBy: agent-drafted
review:
tier: auto
tierReason: Guidance-only change to one adapter; sole decider.
reviewBy: 2027-09-08
---

# ADR-0038: Offer the bootstrap decision record as an offer rather than a backfill candidate

## Context

`/adr-backfill` audits a repository for decisions that were made but never
recorded. Run against a repository with no corpus at all, it finds the decisions
*in* the code and says nothing about the one decision the caller is in the
middle of making: whether to keep decision records here, and whether adrkit is
how they will be enforced.

That record is the one adrkit itself keeps as
[ADR-0001](./0001-record-architecture-decisions-in-git.md). Its absence in a
consumer repository is not cosmetic — it is the record that explains `docs/adr/`
to whoever finds the directory in a year, and the one that carries the rejected
alternatives (a database-backed tool, a wiki) that otherwise get re-proposed.

Two forces make this awkward rather than obvious:

- **It is not archaeology.** Every other backfill candidate is admitted on
evidence: a source span, a commit, a plan. Nothing in a repository proves a
human chose to keep ADRs, because at the moment backfill runs, nobody has. The
skill's own admission rule — *code proves what exists, not why it was chosen* —
excludes it, correctly.
- **It cannot travel through the handoff.** A `backfillHandoff` carries concrete
`candidatePaths`, never globs, and `/adr-draft` re-runs `adr check` over
exactly those paths before writing. The bootstrap record governs the corpus
directory itself, which is a glob and frequently does not exist yet. There are
no paths to snapshot.

There is also a live way to get the relationship backwards. "Adopt adrkit" is a
*tooling* decision that depends on the *process* decision to record decisions at
all. An agent that treats the two as one thing will propose superseding a
consumer's existing process ADR — reversing a decision the consumer never asked
to reverse, and reading as tooling imperialism — when the honest edge is
`relatesTo`.

## Decision

Backfill **offers** the bootstrap record and never mines it. The offer is
reported under existing corpus state and named in the recommended next action,
explicitly marked as an offer rather than a candidate. It stays out of the
candidates table and out of every `backfillHandoff`, and is routed to plain
`/adr-draft` — the non-backfill path, where the caller supplies the authority
the evidence cannot.

The edge is read off the corpus rather than assumed:

| Corpus state | Offer | Edge |
| --- | --- | --- |
| No corpus, or records exist but none govern the corpus directory | Process and tooling decision | `relatesTo` between them when split |
| A process record governs the corpus directory | Tooling decision only | `relatesTo` that record |
| A prior tooling record governs it | Tooling decision | `supersedes` that prior tooling record |

Adopting adrkit is never a supersession of the decision to record decisions.
`supersedes` is reserved for a prior *tooling* record (`adr-tools`,
`log4brains`, a bespoke MADR script). An existing MADR corpus is migrated by
`adr migrate --from madr`, not superseded.

Detection runs through the CLI — `adr check` over one record already inside the
corpus, reading the `governing` bucket — because this skill forbids hand-parsing
frontmatter, and an invalid record drops out of the parsed corpus, so a grep for
a meta tag can be confidently wrong.

The exit code is read before the bucket, and that ordering is load-bearing rather
than stylistic. Measured against a synthetic unmigrated MADR corpus, `adr check`
returns an empty `governing` bucket *and* `frontmatter-fence` errors at exit `1`,
because no record parses — including the one that is the process decision. The
empty bucket is a parse failure wearing the costume of an absence. Only on exit
`0` does it mean no process record exists; on exit `1` the offer is unverified
until the corpus is migrated or repaired.

## Options considered

### Option A: Offer it outside the candidate handoff (chosen)

| Dimension | Assessment |
|---|---|
| Handoff contract | Untouched — no exemption, no synthetic paths |
| Honesty | The record is labeled as what it is: a current decision |
| Cost | Guidance only; no CLI or schema change |
| Reach | Only where the plugin is installed |

### Option B: Admit it as a candidate with an exempted handoff

Model the bootstrap record like any other candidate and carve out an exception
to the `candidatePaths`-never-globs rule.

Rejected. The rule exists so `/adr-draft` can re-verify a candidate against the
corpus immediately before writing and refuse a stale one. A candidate exempt
from that check is a candidate nobody re-verifies, and the exemption would be
available to every future candidate that finds concrete paths inconvenient. It
also asserts evidence that does not exist.

### Option C: A CLI affordance (`adr init`, or `adr new --bootstrap`)

Rejected for now, and partly moot: `createAdr` already creates the corpus
directory and allocates `0001`, so the mechanism exists and the gap is the
prompt and the content. Baking one opinionated record body into `@adrkit/core`
puts prose in the hardest place to revise, and makes the tool write an opinion
rather than offer one. Reconsider if the offer proves valuable to people who do
not install the plugin.

### Option D: Leave it to the documentation

`site/src/content/docs/quickstart.mdx` already calls `adr new` the bootstrap
step. Rejected as insufficient alone — the documentation is read by the person
setting adrkit up, while the agent auditing the repository is the one holding
the empty-corpus finding. The quickstart is updated as well, not instead.

## Trade-offs

The offer reaches only repositories that install the plugin; a consumer driving
the CLI directly still gets nothing. Backfill's report grows a section that is
noise for the common case of a repository that already has a healthy corpus and
a process record. And the record being offered is worth writing only if the
caller can name a real rejected alternative — an ADR that says "we decided to
use ADRs" and nothing else is ceremony that `lint` will nag about forever, so
the guidance offers a decision to make rather than a template to accept.

## Consequences

- Easier: an agent auditing an empty repository names the missing process and
tooling decisions instead of silently reporting no candidates; the
`relatesTo`-vs-`supersedes` distinction is stated once, in a place both skills
can be tested against.
- Harder: the backfill report has one more conditional section, and the edge
table has to stay correct as adrkit's own relationship vocabulary evolves.
- **How we would know this was wrong:** a consumer's bootstrap record lands with
no rejected alternative and no `affects` matcher, an agent proposes superseding
a consumer's existing process ADR, or the offer is made to a repository that
already has a process record it could not parse. The third was found during
implementation against a MADR fixture and is now defended by the exit-code
precondition; the first two remain live risks.
- Revisit if: a CLI-level affordance is requested by someone not using the
plugin, or a functional run shows hosts do not surface the offer on an empty
corpus.

## Action items

1. [x] Add the bootstrap section to `decision-backfill`, with the edge table and
CLI-based detection.
2. [x] Add the matching no-corpus clause to `decision-memory`.
3. [x] Add a wiring test, observed failing first per ADR-0016.
4. [x] Measure the detection mechanic against synthetic corpora — empty, source-only,
process-record-present, and unmigrated MADR — and record the results in
`docs/reference-verification-agent-plugin.md`.
5. [ ] Exercise the offer in a functional host run against an empty-corpus
consumer. Detection is measured; whether a host surfaces the offer at all is
not.
54 changes: 54 additions & 0 deletions docs/reference-verification-agent-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,60 @@ because they are release-policy decisions rather than defects:
state. Pinning to a cut ref, or protecting the directory behind a single
reviewed release commit, are both real options with different costs.

## v0.3.0 bootstrap-record guidance (2026-09-08)

`decision-backfill` gained one section: a repository with no corpus, or one
whose corpus never recorded why it keeps decisions, is offered the process and
tooling decisions as an **offer rather than a candidate**, routed to plain
`/adr-draft` and excluded from every `backfillHandoff`. `decision-memory` gained
a matching clause on its no-corpus branch.

**This addition is contract- and static-host-validated only.** It has no
functional run of any kind — no Copilot synthetic-consumer exercise, no
reference repository, no external adopter.

| Check | Command | Observed |
|---|---|---|
| Contract | `bun test packages/adapters/agent-plugin/` | 40 pass, 0 fail |
| Claude Code | `claude plugin validate packages/adapters/agent-plugin` | PASS |
| Claude Code | `claude plugin validate .claude-plugin/marketplace.json` | PASS |

The new wiring test was observed failing before the guidance was written, per
[ADR-0016](./adr/0016-require-every-check-to-be-observed-failing-before-it-counts-as-coverage.md).
It asserts the two properties that are easy to regress: the bootstrap record
stays out of the candidates table and out of every `backfillHandoff`, and
adopting adrkit carries a `relatesTo` edge to a process record while reserving
`supersedes` for a *prior tooling* record.

### Detection measured against synthetic corpora

The `governing`-bucket detection was exercised directly against throwaway
repositories rather than reasoned about, using a CLI built from this worktree
and reporting `0.13.0` (an earlier pass used a stale `0.5.0` dist and was
re-run):

| Corpus | `adr check --json` over a path | Exit | `governing` | Reading |
|---|---|---|---|---|
| `docs/adr/` absent | any changed file | `2` | — (usage error) | Nothing to detect; offer both decisions |
| `docs/adr/` present but empty | any changed file | `0` | `[]` | No process record; offer both decisions |
| Records binding `src/**` only | a record inside the corpus | `0` | `[]` | No process record; offer both decisions |
| Plus a record binding `docs/adr/**` | a record inside the corpus | `0` | `["0002"]` | Detected by matcher, not by id or title |
| Unmigrated MADR (no frontmatter fence) | a record inside the corpus | `1` | `[]` + `rule: frontmatter-fence` | **Trap** — a parse failure, not an absence |

The last row changed the guidance. An unmigrated MADR corpus returns exactly the
same empty `governing` bucket as a corpus with no process record, because none of
its records parse — including, in the fixture, the process record itself. Reading
the bucket without reading the exit code first would offer a duplicate of a record
the repository already has, which is the failure ADR-0038 names as proof the
design is wrong. The skill now reads the exit code first and names the MADR case;
a wiring test covers both sentences, and was confirmed failing against the
pre-fix text.

Detection is therefore measured. What remains unverified is **host behavior**:
whether Claude Code, Copilot CLI, or opencode actually surface the offer when
backfill runs against an empty corpus. That needs a functional run in a real
host session and is tracked as an open action item on ADR-0038.

## Verdict

The plugin's six components load on Copilot CLI and function correctly against a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "adrkit",
"displayName": "adrkit — decision memory",
"version": "0.2.0",
"version": "0.3.0",
"description": "Load the architecture decisions governing a change, check plans against them, audit existing code and documentation for missing decisions, and draft new records deliberately — driven by the adr CLI.",
"author": {
"name": "Mark Beacom",
Expand Down
Loading
Loading