fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of .apm/ nesting -- it convention-scans only flat skills/, agents/, commands/, hooks.json at each plugin's root. Confirmed via strings on the installed claude binary and live installs of git@holocron/gitea@holocron/kyberforge@ holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json compiler deliberately strips skills/agents/commands keys, assuming the host already auto-discovers those convention directories -- it has no model of .apm/ being host-visible at all. Separately, apm's own bundle exporter (apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`) implements the correct .apm/ -> flat mapping, but only ever targeted build/<name>-<version>/, a path nothing in marketplace.json's source: points at. scripts/sync-plugin-content.sh wraps that bundle exporter and copies its agents/, skills/, commands/, instructions/, extensions/, and merged hooks.json back into each plugin's own root as a second tracked compiled-output category -- same governance status as .claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/ subdirectories are excluded from the mirror (dev fixtures, not host-visible runtime content; several hardcode a relative repo-root walk-up sized for the .apm/-nested depth, which breaks when duplicated one level shallower). Applied for real across all 6 plugins and verified two ways: `claude plugin validate --strict` passes on every real plugin directory, and a live `claude --plugin-dir <path> -p "list skills/agents"` behavioral test confirms content is now actually discovered. Also, from the same issue #90 review round: - scripts/check-manifests.sh pointed at each plugin's root-level plugin.json (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a stale near-duplicate of .claude-plugin/plugin.json nothing else read or wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to validate .claude-plugin/plugin.json instead, and drops the pointer-field checks entirely (nothing to check -- those fields are correctly absent by design). Content-presence drift is now check-plugin-content-sync's job, a new pre-push hook wired in .pre-commit-config.yaml. docs/adr/0017 records the root cause and decision in full, including two rejected alternatives (patching plugin.json's path fields directly -- apm's compiler strips them on every run; pointing marketplace.json at apm pack's build/ output -- a version-suffixed non-source directory nothing can install from without an extra build step). ADR-0015 and CONTEXT.md are updated to point at it. Refs: #90
This commit is contained in:
@@ -122,3 +122,11 @@ correction) sorted what they document into three buckets:
|
||||
- Two follow-up issues tracked the remaining work, both done: #89 (`skill-author`/`agent-author`
|
||||
routing adaptation, merged in #93) and #90 (the actual repo conversion, which also deleted
|
||||
`plugin-author`/`marketplace-author`).
|
||||
- **Caveat on "Status: executed" above:** issue #90's own execution comment flagged, before merge,
|
||||
that Claude Code's ability to actually load content out of `.apm/` was unverified — that caveat
|
||||
turned out to be a real defect, not a formality: the native installer has zero awareness of
|
||||
`.apm/` and reported `Skills (0) Agents (0) Hooks (0)` on every plugin installed from this
|
||||
marketplace. The manifest-compilation deliverable this ADR describes was genuinely complete;
|
||||
runtime discoverability was not. Fixed in ADR-0017 (a second, compiled flat-directory content
|
||||
mirror at each plugin root, generated by `scripts/sync-plugin-content.sh`) — see that ADR for
|
||||
the root cause and the fix.
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
# Plugin roots gain a compiled flat-directory mirror of `.apm/` content so Claude Code can discover it
|
||||
|
||||
This ADR is a follow-on correction to ADR-0015 (Microsoft APM replaces hand-authored
|
||||
plugin/marketplace authoring), discovered during issue #90's post-execution review. It does not
|
||||
restate ADR-0015's rationale for adopting `.apm/` as the authoring source of truth — see that ADR
|
||||
for the parent decision. It resolves the one question ADR-0015's own execution flagged as open but
|
||||
did not block on: whether Claude Code's installer can actually load content out of `.apm/`. It
|
||||
could not.
|
||||
|
||||
**Status: executed (2026-08-13, issue #90).** `scripts/sync-plugin-content.sh` has been run
|
||||
against all 6 plugins; flat `agents/`, `skills/`, `commands/` (etc., wherever `.apm/` populates
|
||||
them), and a merged `hooks.json` now exist at each plugin root as tracked, generated files.
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0015's execution comment on issue #90 (2026-08-12) flagged, before merge: "it's currently
|
||||
unverified whether Claude Code can actually discover any skill/agent content in these plugins...
|
||||
This needs to be checked... before treating this conversion as functionally complete, not just
|
||||
manifest-complete." That caveat did not block ADR-0015 from shipping "Status: executed" — the
|
||||
manifest-compilation deliverable (`.claude-plugin/marketplace.json`/`plugin.json` generated from
|
||||
`apm.yml` + `.apm/`) was genuinely complete, and every automated gate (`apm audit --ci`,
|
||||
`claude plugin validate --strict` ×6, `apm marketplace check`) passed clean — so the ADR merged
|
||||
with the caveat noted but unresolved.
|
||||
|
||||
The caveat turned out to be a real defect, not a formality. `claude plugin install` against all
|
||||
three plugins tested (`git@holocron`, `gitea@holocron`, `kyberforge@holocron`) reported
|
||||
`Skills (0) Agents (0) Hooks (0)`. Root cause, confirmed two independent ways:
|
||||
|
||||
1. **Claude Code's installer scans flat convention directories only.** `strings` on the installed
|
||||
`claude` binary finds zero references to `.apm/` or `apm.yml` anywhere. The installed plugin
|
||||
cache (`~/.claude/plugins/cache/holocron/kyberforge/1.3.1/`) mirrors the pre-conversion flat
|
||||
`skills/`/`agents/`/`hooks/` layout verbatim — that is what the installer actually copies and
|
||||
reads. `plugins/kyberforge/docs/research/docs/claude-code-plugins/configuration.md`'s own
|
||||
"Plugin Directory Layout" table documents the same flat convention (`skills/<name>/SKILL.md`,
|
||||
`agents/`, `hooks/hooks.json`, all "at the plugin root, not inside `.claude-plugin/`") — this
|
||||
was accurate before ADR-0015 and never stopped being accurate; ADR-0015 moved plugin content
|
||||
without adding a bridge to it.
|
||||
2. **apm's own manifest compiler has no `.apm/` → host-path bridge, by design.**
|
||||
`apm_cli/core/plugin_manifest.py`'s `build_plugin_manifest` docstring states directly:
|
||||
"Convention directories (`agents/`, `skills/`, `commands/`) are auto-discovered by the host, so
|
||||
they are never listed explicitly in the manifest." apm's Claude/Copilot compiler assumes plugin
|
||||
content already lives in those flat root-level directories; it has no model of `.apm/` nesting
|
||||
being host-visible at all, so it never emits anything that would point a host at `.apm/`.
|
||||
|
||||
Separately, `apm_cli/bundle/plugin_exporter.py`'s `export_plugin_bundle` (the engine behind
|
||||
`apm pack --format plugin`) *does* implement the correct mapping — `.apm/agents` → `agents/`,
|
||||
`.apm/skills` → `skills/` (subdirs preserved), `.apm/prompts` + `.apm/commands` → `commands/`
|
||||
(`*.prompt.md` renamed to `*.md`), `.apm/instructions` → `instructions/`, `.apm/extensions` →
|
||||
`extensions/`, and `.apm/hooks/*.json` merged into one `hooks.json`. But it was only ever wired to
|
||||
produce a distributable bundle under `build/<name>-<version>/` — a path nothing in root
|
||||
`apm.yml`'s per-package `marketplace.packages[].source:` fields (e.g. `./plugins/bin`) or
|
||||
`marketplace.json`'s equivalent points at. The correct mapping existed in apm's own codebase the
|
||||
whole time; it was simply never connected to the path this repo's marketplace actually installs
|
||||
plugins from.
|
||||
|
||||
## Decision
|
||||
|
||||
Each plugin root gains a second, generated content category, produced by
|
||||
`scripts/sync-plugin-content.sh` (wraps `apm pack --format plugin`, copies the resulting bundle's
|
||||
`agents/`, `skills/`, `commands/`, `instructions/`, `extensions/`, and merged `hooks.json` back to
|
||||
the plugin root) — same governance status as `.claude-plugin/plugin.json`/`marketplace.json`:
|
||||
**compiled output of `.apm/`, never hand-edited.**
|
||||
|
||||
- `.apm/` remains the sole hand-edited authoring source, unchanged from ADR-0015.
|
||||
- The flat mirror is what Claude Code's (and Copilot's) installer actually convention-scans at
|
||||
install time — it exists purely to satisfy the host's discovery contract, a contract apm's own
|
||||
manifest compiler deliberately does not bridge.
|
||||
- `plugin.json`/`apm.lock.yaml`/`.mcp.json` from the bundle are excluded from the copy:
|
||||
`plugin.json` is already correctly generated by a separate, already-verified apm code path
|
||||
(`build_plugin_manifest`, run in the same `apm pack` invocation); `.mcp.json` is hand-authored
|
||||
at the plugin root per ADR-0015 and is not an `.apm/` primitive.
|
||||
- Drift is enforced by a pre-push gate (`scripts/sync-plugin-content.sh --check`, wired into
|
||||
`.pre-commit-config.yaml` as hook id `check-plugin-content-sync` by a parallel workstream on
|
||||
issue #90) — the same enforcement model `check-manifests.sh` already applies to the other
|
||||
compiled-output category.
|
||||
- Verified two ways before landing: `claude plugin validate --strict` passes on all 6 real
|
||||
(non-scratch) plugin directories, and a live behavioral test
|
||||
(`claude --plugin-dir plugins/kyberforge -p "list your skills and agents"`) against the real
|
||||
committed directory confirms `kyberforge:*` skills and the `kyberforge:apm-orchestrate` agent
|
||||
are now actually discovered — they were not, before this fix.
|
||||
- The stale root-level `plugins/<name>/plugin.json` files (a near-duplicate of
|
||||
`.claude-plugin/plugin.json` that nothing read or wrote, flagged separately in issue #90's
|
||||
review) were deleted across all 6 plugins as part of the same cleanup.
|
||||
|
||||
## Considered options
|
||||
|
||||
**Patch `plugin.json`'s `skills`/`agents`/`commands`/`hooks` fields to point directly at `.apm/`
|
||||
paths (rejected).** Claude Code's manifest schema documents these as legitimate override fields
|
||||
that accept custom paths —
|
||||
`plugins/kyberforge/docs/research/docs/claude-code-plugins/configuration.md` shows a real example
|
||||
(`"skills": "./custom/skills/"`, `"agents": ["./custom/agents/reviewer.md"]`), so the host side of
|
||||
this would work. Rejected because apm's compiler is not a passive pass-through: `build_plugin_manifest`
|
||||
unconditionally strips these keys from every manifest it generates, on the stated assumption that
|
||||
convention directories are always host-auto-discovered and therefore never need an explicit
|
||||
pointer. Honoring this option would mean post-processing apm's compiled output on every
|
||||
`apm pack` run to re-inject fields apm actively removes — fighting a stable, intentional apm code
|
||||
path indefinitely — rather than reusing `plugin_exporter.py`'s bundle-export mapping, which already
|
||||
does the right thing and only needed its output redirected to a path the installer reads.
|
||||
|
||||
**Point `marketplace.json`'s `source:` at `apm pack`'s `build/<name>-<version>/` output directly
|
||||
(rejected).** Would reuse the bundle exporter's correct mapping without adding a new script.
|
||||
Rejected: `build/` is a version-suffixed, regenerate-on-every-pack directory — pointing the
|
||||
marketplace at it would mean either committing a moving-target build artifact to version control
|
||||
(defeating the point of it being generated) or requiring every consumer's marketplace to run
|
||||
`apm pack` before install, a build step Claude Code's installer has no hook for — it clones/fetches
|
||||
source and scans directories; it does not execute a package manager's build command first.
|
||||
Copying the relevant subset back to the stable `plugins/<name>/` path — where `marketplace.json`
|
||||
already points — needed no change to the marketplace source model at all.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Git now tracks real, visible duplication: `.apm/skills/<name>/SKILL.md` and
|
||||
`skills/<name>/SKILL.md` both exist and must match, likewise `.apm/agents/*.agent.md` vs.
|
||||
`agents/*.agent.md`, and `.apm/hooks/*.json` vs. the merged `hooks.json`. This is an accepted
|
||||
tradeoff of bridging a gap apm itself doesn't close, not a bug — `.apm/` stays the single
|
||||
hand-edited source, and the drift gate (`check-plugin-content-sync`) is what keeps the mirror
|
||||
honest rather than trusting authors to remember to regenerate it by hand.
|
||||
- `scripts/check-manifests.sh`'s existing blind spot (flagged in the same issue #90 review round:
|
||||
it validated `plugin.json` fields that ADR-0015 already stopped populating, so a plugin shipping
|
||||
zero content could pass it silently) is fixed as part of the same workstream: those field checks
|
||||
are removed (nothing to check — the fields are correctly absent by design), and the
|
||||
content-presence question they were standing in for is now answered by
|
||||
`check-plugin-content-sync`, not re-implemented inside `check-manifests.sh`.
|
||||
- ADR-0015's "Status: executed" now carries a pointer to this ADR (see that ADR's Consequences)
|
||||
rather than being rewritten — the manifest-compilation half of its execution was correct and
|
||||
stands; this ADR fixes the second, previously-unverified half.
|
||||
- `CONTEXT.md`'s "Plugin" and "Plugin marketplace" glossary entries are updated to describe the
|
||||
flat mirror as a second compiled-output category, alongside the existing
|
||||
`.claude-plugin/plugin.json`/`marketplace.json` description.
|
||||
- A future apm release that ships a native `.apm/`-aware plugin.json compiler (closing this gap
|
||||
upstream) would let `sync-plugin-content.sh` and its drift gate be deleted outright — nothing in
|
||||
this ADR's decision depends on the flat mirror existing beyond satisfying the current installer's
|
||||
convention-scan contract.
|
||||
- Reference: issue #90 (https://git.dev.rkdr.net/Defame1297/holocron/issues/90).
|
||||
Reference in New Issue
Block a user