A five-agent review of718c79aandd2480b8found no skill, agent or hook regressions (39 skills before and after) and confirmed both hook removals are genuinely moot -- verified against the tree, not taken on the commit's word. It did find one functional regression (fixed separately) and this documentation drift. Counting errors, all from a git pathspec `*` crossing `/`: - 17 .bats files shipped to consumers is really 10; 17 counted tracked paths merely containing /tests/, one of them a template asset - "roughly 88s off every push" is ~92.4s; 88 omitted validate-plugins - "roughly 70% of each plugin remains live" holds only for kyberforge; the real spread is 44.3% (bin) to 70.6%, now a table - the pre-push enforcement row was half-corrected: 33 entries stood unstruck (now 27) and 14 -> 11 switched counting basis mid-sentence - the root .claude-plugin/plugin.json was described as "kept"; it has never been tracked gates.md said "Ten hooks" above a nine-row table (11 was decremented for one removal, not two), and "both need the claude CLI" for one remaining validator. Its pretty-format-json exclude rationale claimed six alternations expanding to sixteen files in a passage headed "Mind which number you are quoting" -- four alternations, two live files; the two dead ones are dropped from the pattern. check-useless-excludes could not catch this: it only flags an exclude matching nothing at all. ADR-0024 cited ADR-0006 for a patch-bump rule it does not contain and which ADR-0015 explicitly retired; stated apm's marketplace probe order backwards (.claude-plugin/ is the last candidate, not the first, so the earlier .github/plugin/ deletion only demoted resolution); undercounted apm's skill-deploying targets as seven when there are fifteen; and never recorded that validate-plugins was removed. The symlink hedge is resolved: apm_cli/security/gate.py's ignore_non_content() drops symlinks silently on deploy while apm_modules/ materialization dereferences them, so content survives that far and vanishes at install. Accepted with no replacement guard, per decision -- kyberforge/docs/hooks.md previously asserted a guard that had been deleted with its script. Four plugin READMEs still advertised `claude plugin install`; ADRs 0001, 0006, 0013, 0014, 0015 and 0019 described deleted machinery in the present tense, 0019 most consequentially as the live justification for the SessionStart hook's .apm/ path. CONTEXT.md's "apm package" entry forbade "plugin" while using it in its own body, and "Output profile" lost the antecedent for "one catalogue serves both". run-tests.sh gains the .claude/skills/ exclusion run-bats.sh already had. Latent today -- no test-*.sh lives under any .apm/skills/*/tests/ -- but apm now deploys those directories, so one would be discovered twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YR2CjVumUbEGWcMikcoXBD
19 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,289 lines, 92 cases, ~83 s |
scripts/lib/marketplace-plugins.sh |
helper, used only by the above |
check-plugin-content-sync pre-push hook |
~4.5 s per push |
validate-plugins pre-push hook |
~4.9 s per push (6 × claude plugin validate --strict) |
Roughly 22,000 lines of tracked content and tooling, and about 92 seconds on every push (83 + 4.5 +
4.9; the two hook timings are the 2026-09-10 baseline measurements recorded in
SIMPLIFICATION-AUDIT.md, not re-measured here). 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 eleven through those
two roots: ten targets resolve to .agents/skills/ — the six named above plus agent-skills,
antigravity, hermes and openclaw, which are rooted there natively rather than by an explicit
deploy_root — and claude is the eleventh at .claude/skills/. Four further targets (kiro,
grok-build, grok-cloud, copilot-cowork) deploy skills under roots of their own, for fifteen in
total; counted from apm_cli/integration/targets.py this session. 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. - Delete the
validate-pluginspre-push hook (claude plugin validate --strictover every plugin directory). It is removed on the evidence above — it reads manifests only, so with the per-plugin manifest pairs gone it has nothing left to read, and even while they existed it could not detect the empty-content defect.validate-marketplace, which validates the one manifest this repo still ships, is kept. - 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 ~92 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,289-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 probes
_MARKETPLACE_PATHS in apm_cli/marketplace/client.py — marketplace.json, then
.github/plugin/marketplace.json, then .claude-plugin/marketplace.json, first hit wins — 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.
Note the probe order: .claude-plugin/marketplace.json is the last resort, not the first, and
the .github/plugin/marketplace.json this repo deleted earlier outranked it. That deletion was
still inconsequential, but for a reason that has to be established rather than assumed — dropping a
higher-priority candidate only demotes resolution to the next one, and a reviewer reproduced
apm marketplace add against the post-deletion tree: it registered, found all 6 plugins, and
resolved via .claude-plugin/marketplace.json. What would break is deleting the last candidate,
which is exactly what this option proposed.
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). 10 .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. There is no standing rule that would require one. The patch-bump-on-content-
change convention this repo once followed was ADR-0006's, and ADR-0015 explicitly retired it as a
dual-manifest artifact: "Conventions that existed only because of hand-authored dual manifests
(ADR-0006's version-parity/patch-bump rule …) are obsolete under apm.yml's single-manifest model
and were deliberately dropped." ADR-0015 also records that apm "has no native version-bump
automation at all", so nothing mechanical demands one either. What remains is the substantive test,
and it is satisfied independently: nothing under .apm/ is touched here, only compiled artifacts are
removed, so the content every apm consumer receives is byte-identical before and after. 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 filters them out silently and the resulting content loss is invisible to any mirror-versus-mirror diff. That check dies with the script.A previous revision of this ADR left open whether
apm install's own copy path drops symlinks the way the bundle exporter does. It does, and the mechanism is now confirmed by reading the installed apm source. Deployment filters them:ignore_non_content()inapm_cli/security/gate.pyis ashutil.copytreeignore callback whose docstring states "Excludes symlinks (security)", andapm_cli/integration/skill_integrator.pypasses it — or the equivalent_build_copy_ignore()— to everycopytreethat materialises a skill (:424,:791,:1152), with further per-fileis_symlink()drops forbin/entries and the plugin manifest at:1671and:1696. None of these log the skip. Materialization intoapm_modules/, by contrast, dereferences: for git sourcesdeps/github_downloader.pycopies the checkout out withrobust_copytree/robust_copy2and no symlink filter, and for local sourcesinstall/phases/local_content.py's_copy_tree_dereferencing_validated()resolves each in-package symlink and copies its content as a real file (erroring on dangling, escaping or circular ones). So a symlink under.apm/survives intoapm_modules/as real content and is then silently dropped on deploy — the failure is at the deploy step, not the fetch, which is why it would not show up in a cache inspection.This is accepted, and no replacement guard is added. No symlink exists under any
.apm/today, and the mitigations available are worse than the exposure: a standalone repo-side checker would be a new script to maintain for a condition that has never occurred, and it could only warn, since the drop happens inside apm. The operative rule is therefore a convention rather than a gate: do not introduce a symlink under anyplugins/*/.apm/tree. If one is ever needed, the filtering above is the reason it will not reach consumers, and closing the gap properly means an upstream change in apm, not a local 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.