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
3.4 KiB
topic, source_keys
| topic | source_keys | |
|---|---|---|
| configure |
|
Scaffolding a new package
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:
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 metadatatype—instructions | skill | hybrid | prompts; constrains.apm/contentstargets— which harnesses this package compiles to (plural list form preferred; legacy singulartarget: copilot,claudeCSV form still accepted)includes—autopublishes the authoritative local layout as-is, or list explicit repo paths. Note:autodoes not sweep generic root-level passthrough files (README.md, docs/, sources.md, config files) into theapm packdistribution bundle — seereferences/compile.mddependencies/devDependencies—apm/mcp/lspentries;devDependenciesshare the same shape but are excluded from the shipped artifactscripts— named commands runnable viaapm run <name>compilation— target/strategy/exclude/placement controls forapm compile/apm packpolicy— e.g.fetch_failure_defaultregistries— named registry endpoints for shorthand dependency resolutionmarketplace— owner + packages list; seereferences/marketplace.mdfor 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:
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 — runapm plugin init --yes --target claude,copilotinstead.