apm becomes the only supported install path. The flat mirror at each plugin root existed solely so Claude Code's native `claude plugin install` could convention-scan plugin content (ADR-0017). With no native consumers, it cost ~20,000 tracked lines plus ~2,100 lines of sync tooling and ~88s of every push to guard content apm never reads — and its only automated gate, `claude plugin validate --strict`, passes on a plugin with zero content, so it could not detect the defect ADR-0017 was created to fix. Removes the mirror (213 files), the six per-plugin manifest pairs, sync-plugin-content.sh, its 1,289-line test, the orphaned marketplace-plugins.sh, and the check-plugin-content-sync and validate-plugins pre-push hooks. The root `marketplace:` block and .claude-plugin/ catalogue stay: apm's own marketplace consumers read that same file, so `<name>@holocron` short names keep working. tests/run-bats.sh now excludes .claude/skills/. apm installs from .apm/, which carries the tests/ dirs the mirror stripped, so deployed .bats files would otherwise be discovered and double-run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
15 KiB
apm is the only supported install path; the flat content mirror is deleted
Supersedes ADR-0017 (plugin roots gain a compiled flat-directory mirror of .apm/ content so
Claude Code can discover it). ADR-0017's diagnosis was correct and is not in dispute: Claude Code's
native installer convention-scans flat skills//agents//hooks/ directories at the plugin root
and has no model of .apm/ at all, so without a mirror a natively-installed holocron plugin reports
Skills (0) Agents (0) Hooks (0). What changes here is not the mechanism but the premise — that the
native install path is worth supporting. It is not, because nobody uses it.
Status: accepted (2026-09-14). The mirror, its generator, its test suite, its helper library and
its pre-push gate are removed. .apm/ remains the sole hand-edited authoring source, unchanged from
ADR-0015. The root marketplace: block in apm.yml and the compiled
.claude-plugin/marketplace.json it produces are kept — see "Also delete the marketplace
catalogue" under considered options.
Context
ADR-0018 moved this repo's own consumption of its own plugins onto apm install. From that point
the flat mirror had no consumer inside this repo: it existed entirely for a hypothetical third party
running claude plugin install <name>@holocron. No such consumer has ever been observed. The
marketplace is on a private Gitea instance, and the repo has no telemetry, no issue traffic and no
external clone record suggesting otherwise. The honest statement is that the native path has been
maintained for an audience of zero.
What that audience costs is measurable:
| Artifact | Size |
|---|---|
Tracked mirror files under plugins/*/{skills,agents,hooks}/ |
213 files, ~20,000 lines |
scripts/sync-plugin-content.sh |
813 lines |
tests/test-sync-plugin-content.sh |
1,291 lines, 92 cases, ~83 seconds |
scripts/lib/marketplace-plugins.sh |
helper, used only by the above |
check-plugin-content-sync pre-push hook |
~5 seconds per push |
Roughly 22,000 lines of tracked content and tooling, and about 88 seconds on every push. The test
alone is close to 30% of run-tests' wall time — the single largest item in it.
The native path's automated gate does not gate anything. ADR-0017 cites
claude plugin validate --strict passing on all six plugins as one of two verifications. That
verification was re-run this session against a plugin directory with every content directory
deleted, and it passed. validate reads the manifest; it never inspects content. It therefore
cannot detect the exact Skills (0) Agents (0) Hooks (0) defect ADR-0017 was written to fix. The
other half of ADR-0017's verification — the live behavioral test
(claude --plugin-dir plugins/kyberforge -p "list your skills and agents") — is a manual step,
run by hand once in August 2026 and never since. So native-install correctness has been unguarded
for a month, and the drift gate that runs on every push guards only that the mirror matches .apm/,
not that the mirror works.
Dropping native install does not reduce host coverage. This is the fact that makes the decision
cheap rather than a trade. apm's skills convergence deploys skills to .agents/skills/<name>/SKILL.md,
the shared path read by Copilot, Cursor, Codex, Gemini, OpenCode and Windsurf, with Claude Code as
the special case at .claude/skills/. The mirror served two hosts (Claude Code and Copilot, the
latter only ever partially — see ADR-0017's own hooks amendment). apm serves seven. A consumer who
installs holocron through apm gets strictly more than one who installed it natively.
Verified empirically, not reasoned about. In a scratch clone with the mirror and the six per-plugin manifest pairs deleted:
apm marketplace addstill registers all 6 packages. It detects.claude-plugin/marketplace.jsonand reads the catalogue from there.apm installstill deploys 40SKILL.mdfiles across 39 skill directories, 4 agents, and kyberforge'sSessionStarthook — identical to the baseline install from the unmodified tree.apm pack --check-versions --check-clean --dry-runexits 0 ("Version alignment OK", "Marketplace working tree clean"), because it governs only the root.claude-plugin/outputs. The six per-pluginplugin.jsonpairs were never apm-pack-governed: they were generated byapm pack --format plugininvoked from insidesync-plugin-content.sh, so deleting the script deletes their producer and nothing is left asserting they should exist.
Source-level proof the per-plugin manifests are droppable.
apm_cli/deps/github_downloader_validation.py probes package markers in a fixed order —
apm.yml, then SKILL.md, then plugin.json, then .github/plugin/plugin.json, then
.claude-plugin/plugin.json — and returns on the first hit. Every plugin here keeps its apm.yml,
which is the first probe, so no plugin.json path is ever reached. The per-plugin manifests are not
load-bearing for apm resolution; they were load-bearing only for the native installer.
Decision
apm is the only supported install path. Concretely:
- Delete the flat mirror at every plugin root (
plugins/<name>/skills/,agents/,hooks/) and the six per-plugin manifest pairs (.claude-plugin/plugin.json,.github/plugin/plugin.json). - Delete
scripts/sync-plugin-content.sh,tests/test-sync-plugin-content.sh,scripts/lib/marketplace-plugins.sh, and thecheck-plugin-content-syncpre-push hook. - Keep the root
marketplace:block inapm.ymland the compiled.claude-plugin/marketplace.json. apm's own marketplace consumers read that same file; removing it would stop holocron being an apm marketplace at all.
The asymmetry between those last two bullets is the whole subtlety of this ADR, and it exists because apm deliberately reuses Claude Code's catalogue format rather than inventing one. The catalogue is shared between the two ecosystems; the per-plugin content contract is not. Deleting the content is what ends native support; keeping the catalogue is what preserves apm support.
Considered options
Status quo — keep mirroring on every branch (rejected). Pays ~22,000 tracked lines and ~88 seconds per push for a path with no users and no working gate. It is not free in author attention either: ADR-0017 accrued four amendments in two days, every one of them about a detail of the mirroring mechanism rather than about the content being mirrored.
Generate the mirror only at release, from a tag or a release branch (rejected). Technically
supported, and it is worth recording why it was rejected rather than leaving it to look like an
oversight. Claude Code marketplace entries accept ref-pinned git sources, and apm already emits that
exact shape: the mattpocock-skills entry removed from root apm.yml on 2026-09-13 compiled to
{"source": "github", "repo": ..., "ref": "v1.2.3", "sha": ..., "tag_pattern": "v{version}"} — a
git-subdir source pinned to a ref. So a release-only mirror would install correctly.
Rejected on three grounds, stacking:
- It keeps the 813-line script and the 1,291-line test alive in full. It reduces how often they run, not how much there is to maintain — and the maintenance, not the runtime, is what ADR-0017's amendment history shows to be the real cost.
- It requires per-package tagging discipline this repo does not practise.
git taglists repo-level tags (v1.0.0,v2.0.0,v2.0.1) matching no package version under theper_packageversioning mode the six plugins use. The tagging convention that would make ref-pinning meaningful would have to be invented first. - There is no CI in this repo at all. Every gate here is a git hook on a developer's machine. A release-time regeneration step would therefore depend on a human remembering to run it, and its failure mode is silent: a release tag whose tree contains a stale or absent mirror installs natively and reports zero skills, which is precisely the ADR-0017 defect, reintroduced on the release path where it is hardest to notice.
Also delete the marketplace catalogue (proposed, then rejected on evidence). The initial shape
of this decision deleted .claude-plugin/marketplace.json along with everything else, on the
reasoning that it is a Claude Code artifact. That is wrong. apm marketplace add reads
.claude-plugin/marketplace.json — falling back to .github/plugin/marketplace.json, which this
repo already deleted — and that read is what makes holocron an apm marketplace and what gives
consumers the <name>@holocron short-name form. Deleting it would have broken apm consumers in
order to remove a file whose format Claude Code merely happens to share.
Declare holocron an apm marketplace as a new step (moot). Considered as a follow-on to the
above, and found to be already done: the marketplace: block in root apm.yml is the
declaration, and apm marketplace init produces exactly that block. There is nothing to add.
Consequences
1. Native claude plugin install no longer works, and the failure is silent. This is accepted,
not overlooked. Because apm reuses Claude Code's catalogue format by design — an APM-based
marketplace stays consumable by Claude Code's existing marketplace mechanism — a Claude Code user
can still register holocron natively, and will then install six plugins containing zero skills,
zero agents and zero hooks. No error is raised at any point; the manifests are valid and the
directories are simply empty. There is no schema change available that would prevent this, because
the catalogue format cannot express "this marketplace is not for you" — the compatibility is
structural, and it is the same compatibility that makes keeping the catalogue correct for apm. A
README note is the only available mitigation, and a note is not a gate.
2. Consumers now receive dev-fixture files. apm installs from .apm/, and .apm/ contains the
per-skill tests/ directories the mirror explicitly stripped (ADR-0017's depth-scoped
<category>/<name>/tests exclusion). 17 .bats files across 6 skills therefore now deploy into
every consumer's skill directories. Suppressing them would mean switching all six apm.yml files
from includes: auto to explicit include lists — and an explicit list that is wrong silently drops
content, which is the same failure class ADR-0017 was written to fix. Trading a cosmetic problem for
a correctness problem is a bad trade, so this is deferred deliberately rather than fixed in passing.
3. tests/run-bats.sh must exclude .claude/skills/ from both its find walk and the
git ls-files set-equality check that derives the expected test list. Deployed .bats files are
now discoverable in the install output and would otherwise be found and double-run against a root
they do not belong to — exactly the apm_modules/ problem ADR-0018 recorded, arriving by a second
route. Any future script that walks this repo's tree needs both exclusions.
4. No version bumps. ADR-0006 patch-bumps a plugin when its shipped content changes. Nothing
under .apm/ is touched here; only compiled artifacts are removed. The shipped content is
byte-identical, so no bump is owed. This also avoids triggering the executables.allow
kyberforge#<version> pin cascade ADR-0019 describes, which would otherwise turn a cleanup into a
multi-file coordinated edit for no functional gain.
5. Reintroduction recipe. This is the insurance that made the decision acceptable, so it is
stated concretely rather than left as "it's in git". To restore native install support: recover
scripts/sync-plugin-content.sh from git history (git log --diff-filter=D -- scripts/sync-plugin-content.sh
finds the deleting commit; git show <sha>^:scripts/sync-plugin-content.sh recovers it) and re-run
it with --all; it regenerates both the mirror and the per-plugin manifest pairs, because
apm pack --format plugin produces them together. Separately, apm resolves a marketplace at a git
ref — default main, with --ref pinning — so a consumer who pins an older ref still gets a tree
containing the mirror and is unaffected until they move forward.
6. A negative result, pinned so it is not re-litigated: this does not relax the self-containment
constraint. The natural next thought is that with the native installer gone, the no-cross-skill-
file-sharing rule (the rule that forced ADR-0014's Vale config duplication) could be relaxed,
because that rule was read as a property of Claude Code's plugin cache-install. It is not.
plugins/kyberforge/.apm/skills/skill-author/references/deployment-modes.md, sourced from the
agentskills.io spec, states the constraint independently for APM package mode: file references
inside .apm/skills/<name>/ must not reach outside that skill's own directory, and the spec defines
no cross-skill sharing mechanism. So cross-skill file sharing remains impossible under the only
install path that survives, and ADR-0014's duplication rationale stands unchanged.
7. Three of ADR-0017's four amendments become moot, and one loses its enforcement. Recorded because each was a decision someone spent real effort on:
- The
mcpServersre-injection amendment (2026-08-14) is moot. Its target was.github/plugin/plugin.json, which no longer exists;reinject_mcp_servers()dies with the script that called it. Its reasoning — a path string, never an inlined object, because inlining bypasses apm's credential sanitizer — is worth carrying forward as a general rule if per-plugin Copilot manifests ever return. - The
hooks-pointer amendment (2026-08-14) is moot in the same way, and its outcome was to change nothing, so nothing is lost. - The
hooks/hooks.jsonpath-correction amendment (2026-08-14) is moot: there is no mirrored hooks file to place. - The symlink amendment (2026-08-14) is not moot, and this is the one real regression.
check_apm_symlinks()read the.apm/source tree directly to report symlinks, because apm's bundle exporter filters them out silently and the resulting content loss is invisible to any mirror-versus-mirror diff. That check dies with the script. Whetherapm install's own copy path drops symlinks the same way the bundle exporter does was not verified this session — the exporter'sis_file() and not is_symlink()test is inapm_cli/bundle/plugin_exporter.py, a different code path from dependency installation. If it does, symlinked content under.apm/is now silently lost with nothing reporting it. No symlink exists under any.apm/today, so this is a latent gap rather than an active one, but it should be re-checked before anyone adds one.
8. ADR-0017's own exit condition was different from this one, and that is worth noting. Its
final consequence anticipated deletion, but conditioned it on an upstream fix: "a future apm release
that ships a native .apm/-aware plugin.json compiler ... would let sync-plugin-content.sh and
its drift gate be deleted outright." That release has not happened. The mirror is being deleted
because the path it bridges has no users, not because apm closed the gap — the gap is still open,
and a consumer who installs natively still hits it. ADR-0017's anticipated exit remains available
and unclaimed; this ADR takes a different one.