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 waye2e957ehandled 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 from1614bcewas 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 (e4ed343repointed thea8cd5e8citations 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
269 lines
20 KiB
Markdown
269 lines
20 KiB
Markdown
# 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.
|