docs: correct the apm-only change after a five-agent review

A five-agent review of 718c79a and d2480b8 found 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
This commit is contained in:
2026-09-14 18:09:03 +00:00
parent d2480b8191
commit 061bb3d5b4
18 changed files with 290 additions and 238 deletions

View File

@@ -28,12 +28,15 @@ What that audience costs is measurable:
|---|---|
| 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 |
| `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 | ~5 seconds per push |
| `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 88 seconds on every push. The test
alone is close to 30% of `run-tests`' wall time — the single largest item in it.
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
@@ -50,8 +53,13 @@ not that the mirror works.
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.
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:
@@ -81,6 +89,11 @@ load-bearing for apm resolution; they were load-bearing only for the native inst
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.
@@ -92,7 +105,7 @@ content is what ends native support; keeping the catalogue is what preserves apm
## Considered options
**Status quo — keep mirroring on every branch (rejected).** Pays ~22,000 tracked lines and ~88
**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.
@@ -106,7 +119,7 @@ exact shape: the `mattpocock-skills` entry removed from root `apm.yml` on 2026-0
Rejected on three grounds, stacking:
1. It keeps the 813-line script and the 1,291-line test alive in full. It reduces how often they
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
@@ -121,11 +134,20 @@ Rejected on three grounds, stacking:
**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.
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
@@ -145,7 +167,7 @@ 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
`<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
@@ -157,9 +179,15 @@ now discoverable in the install output and would otherwise be found and double-r
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`
**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.
@@ -195,14 +223,32 @@ because each was a decision someone spent real effort on:
- 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'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. Whether `apm install`'s own copy path
drops symlinks the same way the bundle exporter does was **not verified this session** — the
exporter's `is_file() and not is_symlink()` test is in `apm_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.
`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