Group 3 of the validated PR #135 review fixes. Every figure and commit citation below was re-verified at HEAD before being written. ADR and architecture: - #7 ADR-0025 cited 61b0b9c, which no published branch reaches. Repointed to620f20b(identical parent tree, reachable from the PR branch), with a note that neither is reachable from origin/main. The parser-drift paragraph now credits598a7c3(the reachable PR #129 squash) and keeps484357aonly as a pre-squash parenthetical. - #8 architecture.md dropped the pointer at the LESSONS.md entry this branch deleted. - #9 architecture.md's ADR entry points now name ADR-0015 (the one compiler) and ADR-0024, and list ADR-0024 as superseding ADR-0017. - #10 ADR-0024 section 4 rewritten: the standing patch-bump rule is apm-workflow's configure.md, not ADR-0006's, and this change does not trigger it. ADR-0015:93 carries a correction for the misattribution. - N5 ADR-0021 gained a Correction note for the deleted scripts/check-manifests.sh (e647f14). gates.md: - #11a the four ADR-0020 constants live in lib-checks-skill.sh:313-316 and lib-checks-agent.sh:164-165, not in validate.sh. - #11b the pretty-format-json exclude is two alternations expanding to three tracked files, including .claude/apm-hooks.json. - #11c the ADR-0020 contract suite runs 28 -> 27 -> 29 (620f20b,4de5b6b,ef27c97), 29 at HEAD; the unverifiable 25 is dropped. - #11d the boundary resolver is one copy sinceef27c97. - #12 apm-audit-ci documents the 10 root checks and the 1 plugin check apm 0.28.0 actually runs, that content-integrity IS the hidden-Unicode scan, that manifest-parse is not a named check, and that the hook needs a completed apm install. The offline claim is qualified accordingly. - N9 gates.md:142-146 verified to still match the hook description. AGENTS.md: - #12 the no-network session rule is qualified to a populated apm_modules/. Audit note: - A1 hook counts corrected to 27/9 -> 26/8 -> 27/9 -> 26/8 (26 and 8 at HEAD) and the dangling pointer dropped. - A2 skill-size-check.sh is 509 lines with the resolver sourced, not 1,522 embedded; citations repointed to skill-size-check.sh:323-335 and lib-checks-skill.sh:235-283 (fail() at :265 and :280), and that library is 627 lines. - A3 consumers receive 15 test files across 5 skills; 16 tracked test paths repo-wide. - A4 the "do not run apm update on this branch" instruction is marked superseded, with the branch-aware guidance in its place. - Finding 31's "true orphans" claim corrected for HOTL and Sycophancy, both still used in core/ai-constitution.md. Same class, found during group 2: - skill-author's deployment-modes.md no longer points at .mcp.json configs (deleted inc96ca9c); metadata.version 1.0.2 -> 1.0.3. - git-orchestrate's context contract clarifies that user_config_overrides is caller-supplied session state, not a config read. The field name is unchanged. - B3 root apm.yml's executables.allow comment: grants are version-blind in apm 0.28.0, so the #2.0.0 suffix is cosmetic to apm and a bump does not break the hook; the suffix stays because check-executables-allow-sync.sh requires it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
261 lines
19 KiB
Markdown
261 lines
19 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.
|
||
|
||
## 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 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* 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.
|