Skip to content

fix(agent-plugin): read every bucket, bind the corpus directory, and ratify ADR-0038 - #208

Merged
mbeacom merged 2 commits into
mainfrom
fix/bootstrap-detection-all-buckets
Sep 15, 2026
Merged

mbeacom merged 2 commits into
mainfrom
fix/bootstrap-detection-all-buckets

Conversation

@mbeacom

@mbeacom mbeacom commented Sep 15, 2026

Copy link
Copy Markdown
Owner

What and why

Follow-up to #199. A deep review of that PR — run after it merged — found three
defects in the 0.3.0 bootstrap offer, each found by executing the CLI rather
than reasoning about it, and each of which offers a repository a decision record
it does not need. The plugin is catalogued from the repo-root marketplace, so
0.3.0 is already reachable by consumers; this is a fix-forward to 0.3.1.

The decision ADR-0038 records is unchanged, so it is amended, not superseded.
What was wrong was the detection mechanic and the reachability of the write path.

1. governing holds accepted alone — blocking

Detection read governing by itself, so a process record that was proposed or
rejected came back as absence. Measured against a CLI built from this branch:

Corpus Probe Exit governing activeProposals history
process record status: proposed, affects: docs/adr/** that record 0 [] ["0001"] []
same record status: rejected that record 0 [] [] ["0001"]

The consequence was circular: backfill offers the bootstrap record, /adr-draft
writes it proposed, and the next run offers the same decision again — the
tool re-offering its own output. A rejected record was re-proposed, which
decision-memory/SKILL.md:31 names as the third failure the plugin exists to
prevent, and which contradicted decision-backfill/SKILL.md:50's own rule to
reconcile against "accepted, proposed, rejected, and superseded records".

Detection now reads all three buckets and distinguishes already proposed
(ratify it, offer nothing) from settled against (report it, never re-propose).

2. The exit-code guard read a path-scoped signal as corpus-wide — major

adr check's exit code covers the paths it was handed, not the corpus. Measured:
a malformed process record beside a healthy unrelated record returns exit 0
with empty findings when the healthy record is probed, while corpus-wide
adr lint on the same repository exits 1. The corpus-wide gate is now
adr lint.

3. The offer's write path did not work — blocking

The offer named /adr-draft, whose gate stops on adr lint exit 2 — precisely
what a repository with no corpus returns, which is the offer's entire audience.
Measured on a fresh repo: adr lint --dir docs/adr exits 2; adr new "<title>" --dir docs/adr exits 0, creates the directory and allocates 0001, because
createAdr calls mkdir recursively. ADR-0038's own Option C already recorded
this. The gate is narrowed to the absent-corpus case; a corpus that exists and
does not parse stays a hard stop.

4. The detection procedure was unreachable from the entry point — major

/adr-backfill loads the command, and the whole detection procedure lived in
the skill. The command also never stated the edge table's middle row. Both are
ported, and the safety-policy checker now enforces the bootstrap rules against
both surfaces, so guidance cannot regress into the skill alone again.

Evidence

Per ADR-0016,
the checks were observed failing first — four assertions red against 0.3.0
guidance, then green after the fix. The retained contradictory fixture
(test/fixtures/unsafe-backfill-guidance.md) gained matching negative cases, so
the new rules have a permanent executable counter-example rather than an
attestation that one was once seen. That gap was itself a review finding against
0.3.0.

docs/reference-verification-agent-plugin.md gains a re-measured table covering
the proposed, rejected, and mixed-validity corpora the original five-shape
matrix never exercised.

Checklist

  • Commits are DCO signed off
  • ADR-0038 amended (still proposed); two new action items left open
  • Schema unchanged — N/A
  • packages/ci/src and @adrkit/core unchanged — N/A; packages/ci/dist deliberately untouched
  • Tests observed failing before passing, per ADR-0016
  • typecheck, test (2814 pass / 0 fail), lint, check:changelog, check:site-grammar, check:doc-pins, check:freeze-hashes, check:clause8, check:no-spike-heuristics, check:deps, adr lint (38 records, 0 errors), both claude plugin validate targets, and the site build all pass
  • Version 0.3.1 across all eight version-bearing surfaces

Known limitations, deliberately left open

  • Host surfacing and the write path end to end are still unverified. Action
    item 5 is widened to cover the write path, not just whether a host surfaces
    the offer.
  • ADR_DIR resolution still ignores step-1 discovery (new action item 7). A
    pre-adrkit MADR corpus at a non-default path — docs/decisions/ is MADR's own
    convention — resolves to a nonexistent docs/adr, exits 2, and reads as "no
    corpus" instead of routing to migration. Narrowed in guidance here; the
    resolution order is unchanged.
  • /adr-context and /adr-check do not carry the no-corpus clause that
    decision-memory gained (new action item 8). Left as a scope call for the
    maintainer rather than decided here.

… work

Three defects shipped in 0.3.0, each found by executing the CLI rather than
reasoning about it, and each of which offers a repository a decision record it
does not need.

`adr check`'s `governing` bucket holds `accepted` records alone. Detection read
it by itself, so a process record that was `proposed` -- which is exactly what
`/adr-draft` writes -- or one that had been `rejected` came back as absence.
Measured: that record returns exit 0 with `governing: []` and
`activeProposals: ["0001"]`, or `history: ["0001"]` when rejected. The
consequence was circular: backfill offered the bootstrap record, `/adr-draft`
wrote it `proposed`, and the next run offered the same decision again. A
`rejected` record was re-proposed, which decision-memory names as the third
failure it exists to prevent, and it contradicted this skill's own rule to
reconcile against accepted, proposed, rejected and superseded records.
Detection now reads all three buckets and distinguishes already-proposed
(ratify it) from settled-against (never re-propose).

The exit-code guard read `adr check`'s exit code as though it certified the
corpus. It is scoped to the paths it was handed, so a malformed process record
beside a healthy one returns exit 0 with empty findings while corpus-wide
`adr lint` exits 1. The corpus-wide gate is now `adr lint`.

The offer named `/adr-draft`, whose gate stopped on `adr lint` exit 2 --
precisely what a repository with no corpus returns -- so the headline case could
not be written at all. `adr new` exits 0 there and creates the corpus, because
`createAdr` makes the directory itself. That gate is narrowed to the absent-
corpus case; an unparseable corpus stays a hard stop.

Detection guidance now lives in `commands/adr-backfill.md`, not the skill alone:
`/adr-backfill` loads the command, so a procedure only the skill carried was
unreachable from the entry point people invoke. The safety-policy checker
enforces it against both surfaces.

Per ADR-0016 the checks were observed failing first: four new assertions red
against 0.3.0 guidance, and the retained contradictory fixture gained matching
negative cases so the new rules have a permanent executable counter-example
rather than an attestation.

ADR-0038 is amended rather than superseded -- it is still `proposed`, and the
decision it records is unchanged; what was wrong was the detection mechanic and
the reachability of the write path. Two action items are added and left open:
`ADR_DIR` resolution ignoring step-1 discovery, and mirroring the no-corpus
clause into `/adr-context` and `/adr-check`.

Signed-off-by: Mark Beacom <m@beacom.dev>
Copilot AI lite review requested due to automatic review settings September 15, 2026 02:26
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

Decisions governing this change

  • 0001 — Record architecture decisions as versioned markdown in git
    • via path: docs/adr/**
  • 0007 — Isolate integrations as optional adapters and build only against public surfaces
    • via path: packages/adapters/**
  • 0011 — Host the canonical JSON Schema at its $id on adrkit.dev
    • via path: site/**
  • 0016 — Require every check to be observed failing before it counts as coverage
    • via path: packages/adapters/*/test/**
  • 0028 — Ship decision memory as a portable agent plugin, and omit the MCP wiring hosts cannot honor
    • via path: .claude-plugin/**
    • via path: packages/adapters/agent-plugin/**
  • 0030 — Keep extension surfaces that carry a dependency tree outside this repository
    • via path: packages/adapters/**
  • 0033 — Select interactive graph presentation at the CLI boundary while preserving piped DOT
    • via path: site/src/content/docs/**
  • 0034 — Extend the portable agent plugin with decision backfill
    • via path: .claude-plugin/**
    • via path: docs/reference-verification-agent-plugin.md
    • via path: packages/adapters/agent-plugin/**
    • via path: site/src/content/docs/backfill.mdx
  • 0038 — Offer the bootstrap decision record as an offer rather than a backfill candidate
    • via path: docs/reference-verification-agent-plugin.md
    • via path: packages/adapters/agent-plugin/**
    • via path: site/src/content/docs/backfill.mdx

Active proposals touching this change

These are not yet ratified and do not bind this change:

  • 0037 — Treat generated knowledge systems as downstream read models, not decision authorities (proposed)
    • via path: site/src/content/docs/**

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The bootstrap write path and backfill/public guidance still have unresolved issues.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR releases agent plugin 0.3.1 and fixes bootstrap detection, corpus validation, and draft scaffolding.

Changes:

  • Reads all decision buckets and uses corpus-wide linting.
  • Makes backfill detection reachable and updates absent-corpus handling.
  • Refreshes tests, documentation, ADR evidence, and version metadata.
File summaries
File Summary
site/src/content/docs/backfill.mdx Updates public guidance; status coverage and the corpus-wide lint prerequisite need alignment.
packages/adapters/agent-plugin/test/wiring.test.ts Expands guidance regression tests.
packages/adapters/agent-plugin/test/fixtures/unsafe-backfill-guidance.md Adds contradictory guidance cases.
packages/adapters/agent-plugin/skills/decision-memory/SKILL.md Updates skill version and decision-memory guidance.
packages/adapters/agent-plugin/skills/decision-backfill/SKILL.md Revises reconciliation across decision buckets.
packages/adapters/agent-plugin/package.json Bumps the package to 0.3.1; expected-version references need alignment.
packages/adapters/agent-plugin/commands/adr-draft.md Adds absent-corpus handling, but checks still precede scaffolding.
packages/adapters/agent-plugin/commands/adr-backfill.md Ports detection to the command; missing-directory handling needs source-inventory qualification.
packages/adapters/agent-plugin/apm.yml Updates the plugin version.
packages/adapters/agent-plugin/.claude-plugin/plugin.json Updates the plugin version.
docs/reference-verification-agent-plugin.md Adds re-measured verification scenarios.
docs/adr/0038-offer-the-bootstrap-decision-record-as-an-offer-rather-than-a-backfill-candidate.md Amends the bootstrap decision guidance.
CHANGELOG.md Documents the 0.3.1 fixes.
bun.lock Aligns workspace version metadata.
AGENTS.md Updates plugin status and repository guidance.
.claude-plugin/marketplace.json Updates marketplace version metadata.
Review details

Suppressed comments (2)

site/src/content/docs/backfill.mdx:196

  • The public matrix omits two statuses that the new bucket contract treats as decisive: draft is in activeProposals, and deprecated is in history. As written, a process record in either state matches no row, so this page does not tell the agent to ratify an existing draft or never re-propose a deprecated decision. Include both statuses in the corresponding rows.
| A process record covering it is already proposed | Nothing — ratify the one you have | none |
| A process record covering it was rejected or superseded | Nothing — it is settled against | none; never re-proposed |

site/src/content/docs/backfill.mdx:202

  • This new site guidance still treats an empty bucket result as enough to offer bootstrap, but does not require the corpus-wide exit-code check. For an unmigrated MADR corpus, adr check returns empty buckets at exit 1 because records do not parse, so readers following this page will offer a duplicate instead of migrating. Mirror the command/skill rule: run adr lint first and interpret empty buckets only after exit 0.
Detection reads all three of `adr check`'s buckets, not `governing` alone —
`governing` holds `accepted` records only, so a process decision that is merely
*proposed* (which is what `/adr-draft` writes) or one that was *rejected* would
otherwise look like no decision at all.
  • Files reviewed: 15/16 changed files
  • Comments generated: 4
  • Review effort level: Lite

Comment on lines +58 to +62
error and also stops — **except** when the corpus directory does not exist
yet. There is nothing to lint in that case, and `adr new` creates the
corpus directory and allocates `0001` on its own, so proceed to the
scaffolding step instead of stopping. Distinguish the two: `Corpus
directory not found` is the bootstrap case; any other exit `2` is a real
Comment on lines +133 to +135
corpus directory that does not exist exits `2`; one that exists but
holds no record exits `0` with an empty result. Both mean the same
thing here, and the offer is both decisions. Once at least one record
{
"name": "@adrkit/agent-plugin",
"version": "0.3.0",
"version": "0.3.1",
| No corpus, or no record in any bucket covers the corpus directory | Process and tooling decisions | `relatesTo` between them when split |
| An accepted process record governs the corpus directory | Tooling decision only | `relatesTo` that record |
| A process record covering it is already proposed | Nothing — ratify the one you have | none |
| A process record covering it was rejected or superseded | Nothing — it is settled against | none; never re-proposed |
…ctory, and ratify ADR-0038

The first functional run of the offer's write path, in an ephemeral consumer
repository, found a fourth defect that three static review lenses and the
0.3.1 corrections all missed.

`adr new` scaffolds `affects: []` and `status: draft`. So the bootstrap record
written through the offer's own prescribed path binds nothing, and a record
that binds nothing is invisible to detection however many buckets are read:
every bucket comes back empty and the next audit offers the same decision
again. The three-bucket fix only engages once the record carries a matcher
covering the corpus directory.

Measured end to end:

  adr lint  --dir docs/adr                 -> exit 2 (bootstrap case)
  adr new "<title>" --dir docs/adr         -> exit 0, creates 0001
  adr lint  --dir docs/adr                 -> exit 0
  adr check --dir docs/adr -- 0001-*.md    -> exit 0, ALL THREE buckets empty
  ...after hand-adding affects docs/adr/** -> activeProposals ["0001"]

ADR-0038 had listed exactly this under "how we would know this was wrong" --
"a consumer's bootstrap record lands with no rejected alternative and no
`affects` matcher" -- and it turned out to be the default behavior of the path
the offer prescribes, not a hypothetical. Both the skill and the command now
require the offer to state the matcher. The command says "drafting scaffolds"
rather than naming `adr new`, because the write-boundary test forbids the
read-only audit command from naming the writer; that guard caught the first
wording.

ADR-0038 is ratified: accepted by @mbeacom, with `provenance.ratifiedBy` set,
which the corpus's own `agent-accepted-requires-ratifier` rule requires of an
agent-drafted record and which rejected the first attempt. The decision is
unchanged from the original proposal -- what was wrong was the mechanic
implementing it, now recorded in Consequences.

Action items 5, 7 and 8 stay open and are tracked rather than closed by
ratification: a host run confirming the offer is surfaced at all, `ADR_DIR`
resolution ignoring repository discovery, and mirroring the no-corpus clause
into /adr-context and /adr-check.

Signed-off-by: Mark Beacom <m@beacom.dev>
@mbeacom mbeacom changed the title fix(agent-plugin): read every bucket, and make the bootstrap offer's write path work fix(agent-plugin): read every bucket, bind the corpus directory, and ratify ADR-0038 Sep 15, 2026
@mbeacom

mbeacom commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

Update: a fourth defect, found by the functional run — and ratification

Before ratifying I exercised the offer's write path end to end for the first
time, in an ephemeral consumer repository. It found a defect that three static
review lenses and the corrections above all missed.

adr new scaffolds affects: [] and status: draft. The bootstrap record
written through the offer's own prescribed path therefore binds nothing, and
a record that binds nothing is invisible to detection however many buckets are
read
— every bucket empty, and the next audit offers the same decision again.
The three-bucket fix only engages once the record carries a matcher covering the
corpus directory.

adr lint  --dir docs/adr                 -> exit 2  (bootstrap case)
adr new "<title>" --dir docs/adr         -> exit 0, creates 0001
adr lint  --dir docs/adr                 -> exit 0
adr check --dir docs/adr -- 0001-*.md    -> exit 0, ALL THREE buckets empty
...after hand-adding affects docs/adr/** -> activeProposals ["0001"]

ADR-0038 had already listed this under how we would know this was wrong — "a
consumer's bootstrap record lands with no rejected alternative and no affects
matcher". It was not hypothetical; it was the default behavior of the path the
offer prescribes. Both surfaces now require the offer to state the matcher, with
an assertion observed failing first.

Two guardrails in this repository caught my own mistakes while doing it, which is
worth recording:

  • The write-boundary test rejected my first command wording because it named
    adr new in the read-only audit command. Reworded to "drafting scaffolds".
  • The corpus's agent-accepted-requires-ratifier rule rejected the first
    ratification attempt: an agent-drafted record cannot reach accepted without
    provenance.ratifiedBy. Now set to @mbeacom.

Ratification

ADR-0038 is accepted, ratified by @mbeacom on 2026-09-15. The decision is
unchanged from the original proposal; what was wrong was the mechanic
implementing it, now recorded in Consequences with the executed evidence.

Three action items stay open, tracked rather than closed by ratification:

  1. A host run confirming the offer is actually surfaced in a real session.
  2. ADR_DIR resolution ignoring step-1 repository discovery.
  3. Mirroring the no-corpus clause into /adr-context and /adr-check.

Verification

2814 pass / 0 fail, typecheck, lint, seven check scripts, adr lint (38 records,
0 errors), both claude plugin validate targets, site build (51 pages, generated
ADR index now shows accepted). No packages/ci/dist drift. Version 0.3.1
across all eight version-bearing surfaces.

@mbeacom
mbeacom merged commit 88b6403 into main Sep 15, 2026
17 checks passed
@mbeacom
mbeacom deleted the fix/bootstrap-detection-all-buckets branch September 15, 2026 02:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants