Status: Accepted (2026-08-12); records the design after amending #294's acceptance criteria, which replaced the header-gated implementation PR #333 shipped; amended 2026-08-14 — D1's "one declared install set" is one shared engine rather than one declared list, and the record file the Consequences defer to #295 is not built (#295); amended 2026-08-20 — every install function named below is deleted while every decision it held is unchanged (#381); amended 2026-08-25 — the plugin directory is found by a registry lookup, the header is one line on each installed Box and none on a Method, the install set gains a sixth action, and a step is named by its job rather than its number (#383)
Amended (2026-08-14): two corrections, both recorded while implementing #295. Neither changes a decision; each narrows a sentence that claims more than the merged code does.
CONTEXT.md's Install set and Generated header entries already state both — read those first; this amendment only says which wording here they correct.D1's "one declared install set" is one shared engine, not one declared list.
installArtefactsis the single tree-install function, and that much holds. What does not is the implication that some one place declares the whole set: two callers build entries and call it independently —installMethods(lib/methods-bundle.mjs) andinstallBoxWorkflows(lib/artefact-install.mjs) — and/setup's config step writes.archon/unic-dlc.config.yamlwith a barewriteFileSync, outside the engine altogether. Nothing iterates a declared set, and unifying the callers into one is not planned (#295's out-of-scope list). SeeCONTEXT.md§ Install set.The record file the Consequences above defer to #295 is not built. Per-file provenance is the generated header (
renderGeneratedHeader), whose first line names the Plugin and the version that wrote the file — so the "install provenance" #295 was left to carry already existed on disk when that issue came to be implemented. #295 therefore widens what the install already computes and discards:installArtefactsreturns theaddedpaths,installBoxWorkflowsreturns thepreviousVersionit reads from the header before the write loop, and/setupStep 8 prints both. No.archon/unic-dlc.install.json, no hash, no timestamp — a hash is what hand-edit detection would need, and that is a separate question whose baseline isgit diffover the committed Box YAMLs (D4 above). SeeCONTEXT.md§ Generated header.
Amended (2026-08-20) — the rules stand, the module does not.
lib/artefact-install.mjsandlib/methods-bundle.mjsare deleted with the rest oflib/, so everyinstallArtefacts/installMethods/installBoxWorkflowsreference below names a function that no longer exists. Every decision this ADR records is unchanged — the install set is name-scoped, the stale sweep retires aunic-dlc-*.yamlwhether or not it carries the generated header, a name outside that pattern is never read, and the set is discovered from the directory rather than listed by hand./setup's install step now states those rules in prose and the agent applies them with its own tools.One rule this ADR relied on the module for is now the operator's:
/setupmust locate the Plugin's own installed directory before it can copy anything out of it, because$CLAUDE_PLUGIN_ROOTis not set inside the Bash tool. The preflight finds the directory and stops rather than guessing. #383 settles the mechanism.
Amended (2026-08-25) — the mechanism is a registry lookup, and the header is one line (#383).
Locating this Plugin's own directory. Claude Code keeps
~/.claude/plugins/installed_plugins.json, which recordsinstallPath,version,scopeand — for a project-scope install — theprojectPaththe entry belongs to./setupreads that file, picks the entry matching this repository, takesinstallPath, and verifies the directory before copying: the manifest there names this Plugin at the registry's version,vendor/mattpocock-skills/is present, and.archon/workflows/holds at least oneunic-dlc-*.yaml. It stops, printing what it found, on a missing entry, a registryversionother than2, or a failed check. The registry entry agreeing with the manifest is the whole version test: the command file carries no version of its own, so there is nothing else to compare it against, and a rule that asked for one would send an agent looking for a version to invent.The interim prose it replaces searched
~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/and asked the operator to confirm. That directory holds one entry per version ever installed — nine of them for this Plugin on the machine where this was measured, of which one was installed — so the search was a guess among candidates and the confirmation existed to cover the guess. The registry names the answer, so no confirmation is needed on the happy path.The generated header is one comment line on each installed Box, naming this Plugin and the version that wrote it and stating that
/setupreplaces the file.renderGeneratedHeaderis deleted, so/setupwrites it, and reads it back asPREVIOUS_VERSIONby matching a prefix of the first line — never a search over the whole body, per D3.No Method file carries a header. The bundle is upstream text pinned to one tag, and a line added at the top forks it from that tag — the same fork that a Consumer's formatter caused on
DXP-DesignSystem. D3's "every installed file's header" therefore reads: every installed Box. The bundle's provenance is its tag, reported in the summary.The install set gains a sixth action, and it is why the fork above cannot recur.
/setupexcludes both installed trees, and every file whose name carries.generated., from whatever this project uses to format or lint — found by reading the project, never from a list of tools held in the Plugin. Where that tool's exclusion mechanism cannot carry a comment marker,/setupprints the entry the operator must add and reports it as an open item.Name a
/setupstep by the job it does, never by its number. #383 reordered the command — the install moved ahead of the config conversation — and the two numbers swapped rather than shifted, so each stale reference resolved to a real but opposite step and stopped no reader. Every live reference in this ADR was rewritten here on 2026-08-25. The numbers that remain sit in § Context and in the dated amendments, where they record what was true on the day they were written; a live claim carries none.
Nothing installed the Box workflow YAMLs into a Consumer project. commands/setup.md wrote the
config (Step 5), the Methods bundle (Step 6) and the CLAUDE.md marker block (Step 7); the only
mention of .archon/workflows/ or .archon/commands/ was an aside. README.md's own Quick start
Step 2 taught /unic-dlc-explore my-feature, an invocation surface that does not exist — the real
one is archon workflow run <name> "<slug>".
The evidence for clean-replace was historical, not live: at commit 39080fb (2026-08-05, pre-#298),
the repo-root .archon/workflows/ held four Boxes hand-seeded at 8869684 and already retired —
unic-dlc-{cleanup,plan,review,triage}.yaml — sitting runnable for two and a half months across
three redesigns, next to unic-dlc-pr-review.yaml, which existed and had never once been installed.
#298 deleted all seven (this monorepo is a Consumer of nothing — root ADR-0033),
so the defect is not currently observable in this repo, but the failure mode it demonstrates —
a retired Box still runnable a long time after retirement — is exactly what clean-replace exists to
close.
PR #333 generalised installMethods's clean-replace logic, stamped a generated header on every
installed Box YAML, and gated the stale-sweep on that header: a file was swept only if it both
matched the naming and carried the header. CI was green on nine checks; every test passed. The
criteria at the time did not say how ownership was decided, so the implementation was faithful to
underspecified criteria and still produced the wrong behaviour, in both directions:
- The four Boxes hand-seeded at
8869684carry no header — they predate this feature — so a header-gated sweep leaves them runnable. That is this issue's motivating case, unfixed. - The README already commits to an escape hatch: a team wanting a variant of a bundled Box copies it
to a name outside the
unic-dlc-*set, where name-scoped install never reaches it. A copy made the documented way — by copying an installed YAML — carries the header. Header-gating deletes it, breaking the one guarantee the escape hatch depends on.
The acceptance criteria also contradicted each other: one forbade a Box name as a literal anywhere in
lib/, a test fixture, README.md or this ADR (the discovery rule below); another required the
guarding test's set to "not shrink" and be "repointed in place," which is only satisfiable by keeping
those literals. An implementer had to violate one to satisfy the other. The 2026-08-12 amendment
replaced both with the single rule this ADR records: ownership is decided by name, never by
contents, and the guarded set is derived from what the plugin ships, never enumerated.
Grilled together with #295 (install provenance) and #296 (Step 7 ownership) under stream #315,
because all three answer one question — what /setup owns and how it replaces it.
lib/artefact-install.mjs exposes one tree-install function, installArtefacts. Every entry names a
plugin-relative source, a Consumer-relative destination, and whether it owns its whole destination
directory. .archon/methods/ is the only entry that sets that flag: it is entirely
Plugin-owned, so installMethods (lib/methods-bundle.mjs) builds one directory entry and calls
installArtefacts, unchanged in behaviour and still covered by its own pre-existing tests.
/setup installs every unic-dlc-*.yaml this Plugin ships, discovered by reading this Plugin's own
.archon/workflows/ at install time (discoverBoxWorkflowEntry). No Box name is a literal in
lib/, in a test fixture, in README.md, or here — the set is whatever the directory holds, so a
Box added to or retired from the Plugin changes what installs with no code change anywhere in this
path. test/box-staging-and-repo-pinning.test.mjs's guarded set is derived the same way, replacing
the hand-maintained "does not shrink" literal list that PR #333's own test carried.
/setup writes nothing into .archon/commands/. Every Box command doc moves to
docs/boxes/ as operator documentation — read in this Plugin's own repo, never
installed into a Consumer. .archon/commands/ keeps only its .gitkeep. Each doc's invocation is
archon workflow run <name> "<slug>"; none mentions --input, which is not a real flag.
.archon/workflows/ is not Plugin-owned the way .archon/methods/ is — it is shared with the
Consumer's own workflows, so a directory-level clean-replace there would delete unrelated files on
the first /setup run. Install is name-scoped instead: only names matching the unic-dlc-*
naming are ever written, overwritten, or swept as stale (BOX_WORKFLOW_NAME_PATTERN in
lib/artefact-install.mjs). A name outside that pattern is never even read, whatever it contains.
The stale sweep deletes a name-scoped match the current install set no longer ships regardless of
whether it carries the generated header — ownership is decided by name alone. Header-gating is
the mistake this ADR retires: see Context above for why it fails in both directions. Where the header
is read at all (hasGeneratedHeader), it is matched as a prefix of the first line, never as a
substring search over the whole body — a Consumer file that merely mentions the marker text further
down is not the same thing as a file this engine wrote.
A file that cannot be read during the sweep is reported in a skipped list, never silently dropped,
and the install reports failure rather than success while a Box it meant to retire might still be on
disk (stage: 'stale-sweep' in installArtefacts's result). One error is forgiven, precisely:
ENOENT from the deletion. A name that is absent is not "still on disk" — it is already in the
end state the sweep wanted — so it is neither a failure nor a deletion the sweep performed, and it
appears in neither list. ENOENT from the read is forgiven by nobody, because it is not proof of
absence: a dangling symlink reads ENOENT while its directory entry is still listed and still stale.
The deletion is therefore what concludes absence; the read stays a strict probe for every other
failure. For the same reason rm is called with force: false — force: true would swallow the
ENOENT inside rm, and the sweep would then report a deleted path it never deleted.
Overwrite silently rather than warn or refuse: refusing would break the upgrade path (/setup's install
step runs even when STATE = 'full', precisely so a plugin upgrade lands with no reconfigure), and
warn-then-stop leaves a half-installed tree either way. Every installed Box carries a header naming the
Plugin and its version and stating that /setup replaces it — that is what makes the overwrite
legible: an operator's edit is a tracked git diff after /setup, not a warning dialog. No Method file
carries one, per the 2026-08-25 amendment above.
No .archon/workflows.local/ override tier. A Method has an override tier because a team
legitimately owns procedure (ADR-0030,
ADR-0032). A Box YAML is the Harness — isolation, gates, red/green
integrity — and "Consumer-side opt-out flags for individual Boxes" is already on this Plugin's
Do-not-add list (ADR-0014). The escape hatch that already
exists costs nothing: copy the YAML to a name outside the unic-dlc-* set, where name-scoped
replacement never reaches — D3's own rule is what makes that copy survive every future /setup run,
including one that carries the generated header.
The installed Box YAMLs are committed, generated files — but the reason is not the one
ADR-0031 gives for Methods. Probed, not assumed: in a
scratch repo with an untracked .archon/workflows/probe.yaml, archon workflow list discovers and
lists it. Archon reads a workflow definition from the filesystem at the dispatch cwd, in the main
checkout — never from inside a node's isolated worktree. ADR-0031's argument ("an Archon node runs in
a separate worktree checkout, therefore the file must be committed so the node can see it") does not
apply here: a Box YAML is read before any node or worktree exists, to decide what to run in the first
place. Methods are worktree-bound; a Box YAML is not.
The reasons that do hold:
- Archon's own doctrine.
.archon/{commands,workflows,scripts}/"should be committed" (references/repo-init.md). Only.archon/state/and.archon/.envare gitignored. - Consistency inside the install set.
.archon/unic-dlc.config.yamland.archon/methods/are already committed and are read inside the worktree by every Box'sbootstrapnode. Committing some generated entries and ignoring others breaks the one rule that makes the set reviewable. - The review surface. A Plugin upgrade changes what an AFK run does to the repo. An ignored file gives that change no diff and no reviewer — #295 (install provenance) depends on this.
- A fresh clone can dispatch a Box without first installing this Claude Code plugin.
installMethodskeeps its exact signature and every existing test unmodified, now delegating toinstallArtefacts's directory-entry path.- Adding or retiring a Box changes what
/setupinstalls and whattest/box-staging-and-repo-pinning.test.mjsguards, with no edit to either file — both derive their set from.archon/workflows/on disk. - No document in this Plugin states a Box count or a stub count: counting the set would contradict
the reason it is discovered at install time rather than enumerated. The
CHANGELOG.mdentry for this change does not claim a Consumer workflow is untouched "regardless of its name" — a currently shipped name is always overwritten; only a name outside theunic-dlc-*set is untouched. - Provenance, upgrade reporting and drift detection stay #295's
scope; the managed block's content and its ownership claim stay #296's.
This ADR covers only what and how
/setupinstalls.