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:
2026-08-13 16:59:03 +00:00
parent 7910b8b12c
commit 38f1ba4e03
217 changed files with 14455 additions and 175 deletions

View File

@@ -0,0 +1,33 @@
# apm-workflow
Authors, scaffolds, compiles, and audits apm packages and marketplaces.
## What it does
Covers the apm.yml lifecycle a session moves through repeatedly: configuring/scaffolding a package manifest, resolving/fetching its declared dependencies, building or registering a marketplace, compiling/packing/publishing a distributable, and validating integrity via apm audit. Dispatches by requested action to one of five reference files, each self-contained for its concern.
## Before you start
Requires the `apm` binary and (for runtime-driven scripts) an agent runtime already installed — use `apm-install` first if either is missing.
## Usage
```
/apm-workflow configure
/apm-workflow install
/apm-workflow marketplace
/apm-workflow compile
/apm-workflow audit
```
## Files
| File | Purpose |
|------|---------|
| `SKILL.md` | Dispatch table and cross-cutting gotchas |
| `references/configure.md` | apm.yml schema, apm plugin init, dependency forms, MCP secrets, registries |
| `references/install.md` | apm install, apm install [PACKAGE_REF], --update, --target agent-skills |
| `references/marketplace.md` | Building/registering a marketplace, package registration, versioning, Claude Code reserved-name/publish-confirm gotchas |
| `references/compile.md` | apm compile / pack / publish / run, claude plugin validate agents/ gotcha |
| `references/audit.md` | apm audit, apm audit --ci, apm marketplace check, CI wiring, frozen installs, claude plugin validate terminal check |
| `references/sources.md` | Provenance chain — research sources that informed this skill |

View File

@@ -0,0 +1,45 @@
---
name: apm-workflow
description: >
Use when the user wants to author or edit an apm.yml manifest
(dependencies, scripts, compilation, policy, registries), scaffold a new
apm package or marketplace (apm plugin init, apm marketplace init/package
add), install or resolve dependencies declared in apm.yml (apm install,
apm install [PACKAGE_REF]), register a marketplace as a consumer,
compile/pack/publish an apm package for distribution, or validate/audit
apm.yml and installed content (apm audit, apm marketplace check) — even if
the user doesn't say "apm" explicitly, e.g. "set up the package manifest",
"scaffold this as an apm package", "install my apm dependencies", "resolve
apm.yml deps", "build the distributable", "check this passes CI". Do not
use for installing the apm binary itself or setting up an agent runtime —
use apm-install for those.
metadata:
category: apm
source_keys:
- context7-microsoft-apm
---
## Gotchas
- `apm.yml`'s `type:` field (`instructions`, `skill`, `hybrid`, `prompts`) constrains what `.apm/` may contain — set it before scaffolding content, not after. Changing it later doesn't retroactively validate what's already on disk.
- `includes: auto` publishes the authoritative local layout as-is. Anything narrower needs an explicit repo-path list — don't assume `auto` means "scoped down to what's relevant." Note: `auto` still excludes generic root-level passthrough files (README.md, docs/, sources.md, config files) from the `apm pack` distribution bundle — see `references/compile.md`.
- `apm marketplace add` (registering a marketplace as a *consumer*, pointing at someone else's catalog) and `apm marketplace package add` (registering a package by remote reference — `owner/repo`, host URL, or full URL — into a marketplace you're building) are opposite directions of the same command family — don't conflate them. `package add` does NOT accept local paths; a local package is registered by hand-editing `apm.yml`'s `marketplace.packages[]` directly — see `references/marketplace.md`.
- MCP server secrets (headers, env vars) inside `apm.yml` must use `${VAR}` indirection, never literal values, so they're resolved at install/runtime and never committed to the manifest.
- `apm experimental enable registries` must run before any `registry.*` config takes effect. Declaring a `registries:` block or running `apm config set registry.*` without it silently does nothing — no error, no warning.
- Plain `apm audit` and `apm audit --ci` check different things: plain `apm audit` scans deployed files for hidden Unicode only; `--ci` additionally runs lockfile-consistency checks, install-replay drift detection, and org policy checks. A clean plain `apm audit` is not a CI-equivalent pass.
## Step 1 — Dispatch
| Invocation | Action | Reference |
|---|---|---|
| `/apm-workflow configure` | Author/edit `apm.yml`; scaffold a new package (`apm plugin init`) | `references/configure.md` |
| `/apm-workflow install` | Resolve/fetch dependencies declared in `apm.yml` (`apm install`, `apm install [PACKAGE_REF]`) | `references/install.md` |
| `/apm-workflow marketplace` | Build a marketplace, register packages into it (local: hand-edit `apm.yml`; remote: `apm marketplace package add`), or register a marketplace as a consumer (`apm marketplace init/check/package add/add`) | `references/marketplace.md` |
| `/apm-workflow compile` | Generate per-target output, bundle, or publish (`apm compile`, `apm pack`, `apm publish`) | `references/compile.md` |
| `/apm-workflow audit` | Validate integrity/policy or wire a CI gate (`apm audit`, `apm audit --ci`) | `references/audit.md` |
Read only the reference file matching the requested action — each is self-contained for its concern.
## Step 2 — Execute
Follow the matched reference file's instructions. Report back which `apm` command(s) were run (or drafted, if the user asked for a plan rather than execution) and their outcome.

View File

@@ -0,0 +1,55 @@
---
topic: audit
source_keys:
- context7-microsoft-apm
- context7-websites-code-claude
- claude-code-plugins-docs
---
## `apm audit`
```bash
apm audit # local: scan deployed files for hidden Unicode
apm audit --ci # CI gate: lockfile consistency + drift replay + policy
apm audit --file <path> # standalone: scan an arbitrary file
```
Exit code `0` on success, `1` on any violation — composes as a normal CI gate step. It does not replace general lint/test/security-scan CI steps, it sits alongside them.
## Policy checks
`apm audit --ci` auto-discovers an org policy from the git remote if `--policy`/`--policy-source` isn't given explicitly; `--no-policy` skips policy discovery for a single invocation.
## Marketplace ref validation
Separate from `apm audit`: `apm marketplace check` validates that every package reference declared in a marketplace's `apm.yml` actually resolves (correct path/ref, manifest present) — run before `apm pack`/publish, to catch a typo'd local path or stale pinned tag before it ships.
## CI integration example (GitHub Actions)
```yaml
jobs:
apm-audit:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install apm-cli==X.Y.Z # pin to the version standardized on
- run: apm install
- run: apm audit --ci -f sarif --output apm-audit.sarif
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: apm-audit.sarif }
```
`-f sarif --output <file>` emits SARIF for GitHub Code Scanning's inline PR annotations.
## Frozen installs
`apm install --frozen` fails instead of silently re-resolving if the lockfile is out of date — the CI equivalent of `npm ci` vs `npm install`. Use in any CI job that must not be allowed to drift the lockfile.
## Claude Code validation is a separate terminal check
For packages compiling to the `claude` target, also run `claude plugin validate [--strict]` against the compiled output as a terminal check. `apm audit`/`apm audit --ci` do not check Claude Code-marketplace-specific schema rules (reserved prefixes, the `agents/` stray-file behavior — see `references/compile.md` — etc.), so they don't substitute for it.

View File

@@ -0,0 +1,65 @@
---
topic: compile
source_keys:
- context7-microsoft-apm
---
## Compile
```bash
apm compile # fails on skill/agent-only packages — see Gotchas
apm compile --clean # zero-write sanity check; use for skill/agent-only packages
apm compile --clean --dry-run # pure preview, no writes
```
Compiles `.apm/instructions/` + `.apm/agents/*.agent.md` primitives into consumer-side context files (AGENTS.md/CLAUDE.md CONTEXT files) for the deployment target, per the `compilation:` block in `apm.yml`. This is the consumer/deployment side — it is NOT the producer of `plugin.json`/`marketplace.json`; that's `apm pack`'s job (below). Run `apm compile` after any change to `.apm/instructions/`/`.apm/agents/` content or to `compilation:`/`targets:` in `apm.yml`.
## Pack
```bash
apm pack --dry-run # resolve and print; do not write
apm pack --offline # cached refs only
apm pack --include-prerelease # allow pre-release tags
apm pack -v # per-entry resolution detail
apm pack --marketplace=claude --json # JSON output for CI pipelines
```
Bundles a producer package into a distributable artifact. Default to `--dry-run -v` first when packing something for the first time or after a dependency change — resolution errors surface before anything is written.
### `.mcp.json` merge mechanics
`apm pack` reads a package's root-level `.mcp.json` directly — no `dependencies.mcp` entry in `apm.yml` is needed for simple passthrough. Behavior is content-aware:
- An empty `.mcp.json` (`{"mcpServers": {}}`) is NOT included in the packed output at all.
- A populated one gets its `mcpServers` content merged directly into the compiled `plugin.json`, but only for the `claude` target.
- The `copilot` target's compiled `plugin.json` OMITS `mcpServers` entirely — it isn't part of Copilot's plugin manifest schema.
`dependencies.mcp` in `apm.yml` is for a different purpose — declaring a remote MCP-server package as an APM dependency — not local `.mcp.json` passthrough.
### `includes: auto` and the packed bundle
`includes: auto` does NOT sweep generic root-level passthrough files (README.md, docs/, sources.md, config files, etc.) into the `apm pack` distribution bundle (`build/<name>-<version>`) — only `.apm/` primitives, the compiled `plugin.json`, and the content-aware `.mcp.json` handling above make it into that bundle. This does not affect Claude Code's own plugin loading, which reads a plugin's working directory directly via its `source:` path in `marketplace.json`, not the `apm pack` bundle — but it matters for anyone relying on the packed bundle for distribution via `apm install`.
Run `apm audit` after compile/pack, not before — audit scans deployed/compiled output, not the source `apm.yml` manifest; see `references/audit.md`.
## Publish
```bash
apm publish --package acme/my-skill --dry-run -v
apm publish --package acme/my-skill
```
Publishes a producer package (root containing `apm.yml`, `.apm/`, and optionally a `registries:` block) to a registry. Always dry-run with `-v` first — publishing is not trivially reversible once a version tag is claimed on a registry.
## Run
```bash
apm run <script> [--param key=value]
```
Executes a named script defined under `scripts:` in `apm.yml`, with `--param` substituting values into the script's parameters.
## Gotchas
- Plain `apm compile` HARD-FAILS with exit 1 and `No instruction files found in .apm/ directory` on any package that has skills/agents but no `.apm/instructions/` content — the common case for a skill-only or skill+agent package. Use `apm compile --clean` as the zero-write sanity-check substitute: it sets `allow_empty=True` internally and exits 0 cleanly with zero writes; add `--dry-run` for a pure preview. `compile` ≠ `pack` — see above.
- `claude plugin validate --strict` auto-scans every `.md` file directly under a compiled plugin's `agents/` directory and treats it as an agent definition requiring frontmatter, independent of any manifest declaration (an empirically observed Claude Code platform behavior, not an `apm` mechanic). A non-agent `.md` file (notes, provenance record, README) that ends up under `.apm/agents/` in the source tree will still fail validation once compiled to the `claude` target. Keep such files outside `.apm/agents/` (e.g. at the package root), regardless of what `apm.yml` declares.

View File

@@ -0,0 +1,63 @@
---
topic: configure
source_keys:
- context7-microsoft-apm
---
## Scaffolding a new package
```bash
apm plugin init --yes --target claude,copilot
```
Run from inside the target package directory, with no positional name argument (see Gotchas). Creates `apm.yml` + `plugin.json` in the current directory — it does NOT scaffold a `.apm/` skeleton. Primitive subdirectories (`.apm/skills/`, `.apm/agents/`, `.apm/hooks/`) must be created manually as content is migrated into them. Run this once per package (e.g. once per `plugins/<name>/` directory in a monorepo-hybrid layout), not once for the whole repo.
## `apm.yml` — required fields
Only `name` and `version` (SemVer) are required:
```yaml
name: my-pkg
version: 1.0.0
```
## `apm.yml` — top-level keys
- `name`, `version` — required (see above)
- `description`, `author`, `license`, `homepage`, `repository`, `keywords` — standard package metadata
- `type` — `instructions | skill | hybrid | prompts`; constrains `.apm/` contents
- `targets` — which harnesses this package compiles to (plural list form preferred; legacy singular `target: copilot,claude` CSV form still accepted)
- `includes` — `auto` publishes the authoritative local layout as-is, or list explicit repo paths. Note: `auto` does not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into the `apm pack` distribution bundle — see `references/compile.md`
- `dependencies`/`devDependencies` — `apm`/`mcp`/`lsp` entries; `devDependencies` share the same shape but are excluded from the shipped artifact
- `scripts` — named commands runnable via `apm run <name>`
- `compilation` — target/strategy/exclude/placement controls for `apm compile`/`apm pack`
- `policy` — e.g. `fetch_failure_default`
- `registries` — named registry endpoints for shorthand dependency resolution
- `marketplace` — owner + packages list; see `references/marketplace.md` for the full marketplace workflow
See `docs/research/docs/microsoft-apm/configuration.md` for the complete annotated schema.
## Dependency reference forms
`dependencies.apm` entries accept: a pinned tag (`owner/repo#tag`), a plain repo (uses default branch), a single primitive path within a repo, a raw git URL, a `git:`/`path:`/`ref:` object for finer control, or a local relative path (`./packages/my-shared-skills`).
## MCP server secrets
`${VAR}` indirection is required for MCP server secrets in `apm.yml` — see SKILL.md Gotchas.
## Registries (config-level, not `apm.yml`)
Any git repo is a valid package source by default — no registry required. To declare named registries for shorthand dependency resolution:
```bash
apm experimental enable registries # required first — see SKILL.md Gotchas
apm config set registry.corp-main.url https://artifactory.corp.example.com/apm
apm config set registry.corp-main.token eyJ...
apm config set registry.corp-main.default true
```
`apm config get`/`apm config unset` manage individual keys the same way.
## Gotchas
- `apm plugin init <name>` run with a positional name argument, from inside a directory already named `<name>`, creates a wrongly-nested `<name>/<name>/` subdirectory — it treats the positional arg as "create a new project directory named X," not "confirm the current directory is X." Fix: omit the positional argument entirely when already cd'd into the target package directory — run `apm plugin init --yes --target claude,copilot` instead.

View File

@@ -0,0 +1,20 @@
---
topic: install
source_keys:
- context7-microsoft-apm
---
## `apm install`
```bash
apm install # resolve/install everything in apm.yml against apm.lock.yaml
apm install acme/internal-tools#^1.0.0 # install and add this dependency
apm install --update # re-resolve; accept new upstream content even if it doesn't match the lockfile hash
apm install --target agent-skills # emit a vendor-neutral output dir instead of a harness-specific one
```
With no arguments, resolves and installs everything declared under `dependencies:`/`devDependencies:` in `apm.yml` against `apm.lock.yaml`. One or more `PACKAGE_REF` arguments (any of the forms in `references/configure.md`'s "Dependency reference forms" — pinned tag, plain repo, single primitive, raw git URL, `git:`/`path:`/`ref:` object, or local relative path) install that dependency and add it to `apm.yml`.
`--update` is the escape hatch for a lockfile hash mismatch against upstream — normal `apm install` treats that as drift and won't silently accept it; see `references/audit.md` for the CI-side check (`apm install --frozen`) that fails instead of re-resolving.
`--target agent-skills` generates the vendor-neutral output directory instead of a Claude/Copilot-specific one — for IDE-agnostic tool support.

View File

@@ -0,0 +1,81 @@
---
topic: marketplace
source_keys:
- context7-microsoft-apm
- context7-websites-code-claude
- claude-code-plugins-docs
---
## Building a marketplace from a producer repo
```bash
apm marketplace init # 1. add the marketplace: block to apm.yml
$EDITOR apm.yml # 2. describe each package
apm marketplace check # 3. validate refs resolve
apm pack # 4. build marketplace artifacts
```
5. Commit and release — commit the manifest changes, tag the release, and push, following your
project's established git tooling/conventions (a dedicated commit/release skill or workflow,
commitlint/Conventional Commits rules, required tags) rather than ad hoc raw `git` invocations,
if such tooling exists.
`apm pack` emits `.claude-plugin/marketplace.json` as one of its compile targets — an APM-based marketplace stays consumable by Claude Code's existing marketplace mechanism without a separately hand-maintained file.
## Registering a package into a marketplace you're building
`apm marketplace package add`'s `SOURCE` argument only accepts `owner/repo`, `host.tld/owner/repo`, or full URL forms (per `apm marketplace package add --help`) — it does NOT accept a local relative path. Running it against a local path (e.g. `apm marketplace package add ./packages/plugin-a`) throws a git-remote-reachability error: it tries to treat the path as a remote source and fails to reach it.
To register a local package — the monorepo-hybrid case, pointing at each existing `plugins/<name>/` directory once that directory has its own `apm.yml` — hand-edit `apm.yml`'s `marketplace.packages[]` list directly:
```yaml
marketplace:
packages:
- name: plugin-a
description: ...
source: ./packages/plugin-a
version: 1.0.0
```
This local-path `source:` form IS valid — `apm marketplace check` and `apm pack` both accept it — even though the `package add` CLI subcommand cannot create it for you. Use `apm marketplace package add` only for packages hosted at a remote git ref; for local packages, edit the YAML directly.
## Registering a marketplace as a consumer
`apm marketplace add` accepts many source shapes:
```bash
apm marketplace add my-org/awesome-agents # GitHub shorthand
apm marketplace add gitlab.com/my-org/awesome-agents --host gitlab.com # GitLab
apm marketplace add https://gitea.example.com/org/repo.git#v1.0.0 --name custom # self-hosted git, pinned
apm marketplace add https://catalog.example.com/marketplace.json --name catalog # hosted marketplace.json
apm marketplace add git@gitea.example.com:org/repo.git --name custom # SSH
apm marketplace add /srv/marketplaces/agent-forge.git --name agent-forge # local bare repo/working dir
apm marketplace add ./vendor/marketplace.json --name vendor # local marketplace.json file
apm marketplace add file:///srv/marketplaces/agent-forge.git --name agent-forge # file:// form
```
The local-filesystem and `file://` forms need no hosted registry or network access — the fit for an internal/homelab setup.
## Per-package versioning
```yaml
marketplace:
versioning: { strategy: per_package }
packages:
- { name: plugin-a, source: ./packages/plugin-a, version: 2.0.0 }
- { name: plugin-b, source: ./packages/plugin-b, version: 0.1.0 }
```
Without this block, the default versioning strategy ties every listed package to the marketplace/root version.
## Which mechanism to use
- **Local packages, no distribution needed yet** — local-path dependencies in `apm.yml` (`./packages/my-shared-skills`); no marketplace or registry involved.
- **Internal catalog, still git-based, no server** — `apm marketplace add` against a local path, bare repo, `file://` URI, or a plain git host.
- **Package-level hosting at scale / access control** — registries (Artifactory-style REST endpoint); more infrastructure, only worth it once package count or access-control needs outgrow git-based discovery.
## Gotchas
- Only `claude` and `codex` marketplace output profiles exist in apm 0.28.0 — confirmed via source (`apm_cli/marketplace/output_profiles.py`: `MARKETPLACE_OUTPUTS = {claude: ..., codex: ...}`, nothing else). There is no native Copilot marketplace output; `apm pack` will not generate one. A repo needing a Copilot-consumable marketplace manifest must maintain it separately by other means.
- A package/plugin `name` compiling to the `claude` target must avoid Claude Code's reserved name prefixes/values — `anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins` — otherwise the compiled `.claude-plugin/marketplace.json` is rejected by Claude Code's validator. This is a Claude Code platform constraint, independent of how the manifest gets authored.
- Removing a package from `marketplace.packages[]` in `apm.yml` and re-packing changes the public/consumed catalog. Confirm with the user in conversation before removing the entry and running `apm pack` — this is a normal `apm.yml` edit (not a distinct apm-orchestrate operation with its own gate), so the confirmation is conversational, the same as any other consequential manifest edit.

View File

@@ -0,0 +1,33 @@
# Sources
## context7-microsoft-apm
- **URL:** context7:/microsoft/apm
- **Description:** Microsoft APM (Agent Package Manager) — open-source dependency manager for AI agent configuration (skills, prompts, instructions, agents, hooks, MCP/LSP deps), applying a declare/lock/install/audit workflow.
- **Research doc:** plugins/kyberforge/docs/research/docs/microsoft-apm/sources.md
- **Contributing files:** SKILL.md, references/configure.md, references/install.md, references/marketplace.md, references/compile.md, references/audit.md
- **Status:** `extracted`
## context7-websites-code-claude
- **URL:** context7:/websites/code_claude
- **Description:** Official Claude Code documentation site indexed by Context7 — marketplace manifest schema (reserved name prefixes), `claude plugin validate` command behavior. Claude Code platform facts that hold true independent of how the compiled manifest is authored (APM or hand-authored).
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** references/marketplace.md, references/audit.md
- **Status:** `extracted`
## claude-code-plugins-docs
- **URL:** https://code.claude.com/docs/en/plugins
- **Description:** Official Claude Code plugin authoring guide — marketplace submission constraints (reserved name prefixes), `claude plugin validate` behavior and its scope relative to other CI checks.
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** references/marketplace.md, references/audit.md
- **Status:** `extracted`
## claude-code-subagents-docs
- **URL:** https://code.claude.com/docs/en/sub-agents
- **Description:** Official Claude Code subagent reference — definition format, all frontmatter fields, scope priority, built-in agents, CLI flags, environment variables, known limitations
- **Research doc:** plugins/kyberforge/docs/research/docs/claude-code-plugins/sources.md
- **Contributing files:** (none)
- **Status:** `referenced`