Files
holocron/docs/adr/0024-apm-is-the-only-supported-install-path.md
Defame1297 c84f1f4145 docs: close the self-contradictions left by the branch's own cuts
CONTEXT.md used two terms it no longer defines. This branch deleted the
Preload tax and Skill context contract entries as audit finding 31, but
the Hand-invoked skill definition and the example dialogue still used
both, bolded, which is this file's convention for a defined term. The
definitional file contradicted itself while AGENTS.md tells every
session to read it as authoritative. Rephrased in place, the way
e2e957e handled the one the audit's own note records.

ADR-0024 said 10 .bats files deploy across 6 skills; ADR-0025 merged
two of those directories the next day, on this branch, leaving 5. It
was also the only ADR ADR-0025 invalidated without an amendment banner,
as was ADR-0016, which still named agent-audit in the present tense as
the live enforcer. Both get the banner the other nine carry, and the
figure and names are corrected in place as well, since these sit in
text asserting present fact rather than a superseded decision.

ADR-0019's correction block from 1614bce was inserted mid-paragraph and
swallowed the original's trailing sentence, leaving the quote malformed
and the next line starting lowercase mid-sentence. gates.md took the
same correction and is not affected.

In the audit note: two of §12's five open follow-ups were already
closed (e4ed343 repointed the a8cd5e8 citations at 598a7c3; #101 closed
2026-09-16, so Closes #101 is a no-op), the same stale hash sat at :330
with a wrong line number, the vale-wrap counts had drifted from 63/19
to 65/14 and are now pinned to a commit per §1's own convention, and
the deleted-suite tally said eight where the diff shows nine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
2026-09-20 12:34:28 +00:00

20 KiB
Raw Blame History

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.

Amended by ADR-0025 (2026-09-15). The decision stands unchanged — apm is the only supported install path, and .apm/ still ships the per-skill tests/ directories this ADR accepted as dev-fixture leakage. What moved is consequence 2's skill count. skill-audit and agent-audit merged into factory-audit, collapsing two .bats-carrying skill directories into one, so the same 10 .bats files now deploy across 5 skills, not the six counted here on 2026-09-14. The figure below is corrected in place; "all six apm.yml files" in the same paragraph counts plugins, not skills, and is unaffected.

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 docs/notes/simplification-audit-2026-09.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 add still registers all 6 packages. It detects .claude-plugin/marketplace.json and reads the catalogue from there.
  • apm install still deploys 40 SKILL.md files across 39 skill directories, 4 agents, and kyberforge's SessionStart hook — identical to the baseline install from the unmodified tree.
  • apm pack --check-versions --check-clean --dry-run exits 0 ("Version alignment OK", "Marketplace working tree clean"), because it governs only the root .claude-plugin/ outputs. The six per-plugin plugin.json pairs were never apm-pack-governed: they were generated by apm pack --format plugin invoked from inside sync-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 the check-plugin-content-sync pre-push hook.
  • Delete the validate-plugins pre-push hook (claude plugin validate --strict over 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 in apm.yml and 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:

  1. 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.
  2. It requires per-package tagging discipline this repo does not practise. git tag lists repo-level tags (v1.0.0, v2.0.0, v2.0.1) matching no package version under the per_package versioning mode the six plugins use. The tagging convention that would make ref-pinning meaningful would have to be invented first.
  3. 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 5 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 a standing rule, and it is not triggered here. plugins/kyberforge/.apm/skills/apm-workflow/references/configure.md states it: bump a package's own apm.yml version: whenever anything that reaches its compiled output changes — either its .apm/ content (a new or removed skill/agent/hook, or a substantive edit to one) or its own manifest metadata (description, keywords, author, license, homepage, repository, all compiled verbatim into plugin.json). This change touches neither: nothing under .apm/ is edited, no manifest metadata changes, and only compiled artifacts are removed, so the content every apm consumer receives is byte-identical before and after. apm also "has no native version-bump automation at all" (ADR-0015), so nothing mechanical demands one either. 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 mcpServers re-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.json path-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() in apm_cli/security/gate.py is a shutil.copytree ignore callback whose docstring states "Excludes symlinks (security)", and apm_cli/integration/skill_integrator.py passes it — or the equivalent _build_copy_ignore() — to every copytree that materialises a skill (:424, :791, :1152), with further per-file is_symlink() drops for bin/ entries and the plugin manifest at :1671 and :1696. None of these log the skip. Materialization into apm_modules/, by contrast, dereferences: for git sources deps/github_downloader.py copies the checkout out with robust_copytree/robust_copy2 and no symlink filter, and for local sources install/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 into apm_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 any plugins/*/.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.