docs: correct the claims this review found false

AGENTS.md told an offline agent to push with SKIP=apm-marketplace-check and
asserted that hook was "the only one whose failure mode is 'no network'".
Running all 12 pre-push hooks under a network namespace shows two fail, for
one shared cause: apm-pack-check-clean resolves the same remote entry. An
exact pin does not remove the ls-remote, so both hooks are named now.

AGENTS.md also said everything in a plugin root except .apm/ is generated.
Plugin roots carry hand-authored README.md, docs/, bin/, sources.md and
.mcp.json, so an agent would hunt for an .apm/ source that does not exist or
refuse the edit. The rule is positional: immunity belongs to the plugin root,
and anything inside a mirrored directory is still rm -rf'd.

ADR-0017 said apm strips a hooks field. The real loop is (agents, skills,
commands, instructions) -- hooks absent, instructions never mentioned -- and
it can never fire, because synthesize_plugin_json_from_apm_yml only emits the
eight identity fields. The decision stands; the mechanism was overstated. Its
mcpServers amendment is rewritten for the pointer payload and now records the
real reason: inlining bypassed apm's credential sanitizer.

ADR-0015's owner.email and version-pin passages are corrected against the apm
source, and ADR-0016 gains the disallowedTools amendment. agent-audit's
allowlist is data, so it gains disallowedTools too -- the ADR and the
validator that enforces it had come apart.

architecture.md described a root CLAUDE.md that imports two files (it imports
one, plus an RTK block) and pointed at an ADR index that does not exist.
Seven skill READMEs listed tests/ files the mirror strips, promising installed
users files their install lacks; those rows are marked source-only, with the
depth-4 template tests explicitly called out as surviving. And
plugins/kyberforge/hooks/README.md, deleted during the conversion and
preserved nowhere, is restored to a path the mirror does not own -- verified
by running a sync against a scratch copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7GvKuJfy2WrdBmUttV4DT
This commit is contained in:
2026-08-14 11:04:56 +00:00
parent 7ba3d9cf1d
commit 430f46b8e8
19 changed files with 313 additions and 97 deletions

View File

@@ -124,12 +124,13 @@ correction) sorted what they document into three buckets:
adaptation — closed, merged in #93) and #90 (the actual repo conversion, which also deleted
`plugin-author`/`marketplace-author` — tracked through to merge; treat #90's own state as the
authority on whether it has landed, not this line).
- **`displayName` is gone from all six compiled `plugin.json` files, and `owner.email` from the
marketplace manifest — accepted, not overlooked.** `apm.yml` has no key that compiles to either,
so the conversion dropped both: every `plugins/<name>/.claude-plugin/plugin.json` now carries
- **`displayName` is gone from all six compiled `plugin.json` files — accepted, not overlooked.**
`apm.yml` has no key that compiles to it: `synthesize_plugin_json_from_apm_yml`
(`apm_cli/deps/plugin_parser.py`) emits only `name`, `version`, `description`, `author`,
`license`, `homepage`, `repository` and `keywords`, and nothing in `plugin_manifest.py` adds
`displayName` afterwards. So every `plugins/<name>/.claude-plugin/plugin.json` now carries
`author`/`description`/`homepage`/`keywords`/`license`/`name`/`repository`/`version` (plus
`mcpServers` for `bin`) and no `displayName`, and `.claude-plugin/marketplace.json`'s `owner`
block is `{name, url}` only. Both fields are optional —
`mcpServers` for `bin`) and no `displayName`. The field is optional —
`plugins/kyberforge/docs/research/docs/claude-code-plugins/api-reference.md:14` lists
`displayName` as `Required: No`, "Human-readable name shown in plugin manager" — which is why
`claude plugin validate --strict` still passes on all six. The visible cost is that the plugin
@@ -138,16 +139,31 @@ correction) sorted what they document into three buckets:
`reinject_*` workaround of the kind ADR-0017's amendment reserves for fields apm strips on a
factually wrong premise, and apm's premise here is simply that the key does not exist in its
schema.
- **`mattpocock-skills` is now version-pinned, and the pin is maintained by hand.** Pre-conversion
the entry was `{"repo": "mattpocock/skills", "source": "github"}` — an unpinned reference that
tracked the upstream default branch, so consumers got whatever was on it at install time. Root
`apm.yml` now declares `version: "^1.2.0"` for it, which `apm pack` resolves and freezes into
`.claude-plugin/marketplace.json` as `ref: v1.2.3` + an explicit `sha`. Consumers get a
reproducible version instead of a moving target, which is the improvement; the cost is that
nothing advances it. apm has no version-bump automation (established under "Versioning" in issue
#90's plan), so picking up a new upstream release means a human editing the `version:` range in
root `apm.yml` and re-running `apm pack`. Left un-bumped, the marketplace pins an ageing release
indefinitely and silently.
- **`owner.email` was dropped by mistake and has been restored (2026-08-14).** An earlier revision
of this ADR listed `owner.email` alongside `displayName` as a field `apm.yml` "has no key that
compiles to." That was wrong. `apm_cli/marketplace/yml_schema.py:186` defines
`_AUTHOR_OBJECT_KEYS = frozenset({"name", "email", "url"})`, and an `email:` under root
`apm.yml`'s `marketplace.owner` block was empirically confirmed to compile straight through into
`.claude-plugin/marketplace.json`'s `owner`. The key is declared in root `apm.yml` again and the
compiled `owner` block is `{name, email, url}`. Only `displayName` is a genuine schema gap; this
one was a documentation error that removed working configuration.
- **`mattpocock-skills` is pinned to an exact version, and the pin is advanced by hand.**
Pre-conversion the entry was `{"repo": "mattpocock/skills", "source": "github"}` — an unpinned
reference that tracked the upstream default branch, so consumers got whatever was on it at
install time. The conversion first replaced that with `version: "^1.2.0"`, which was still not a
pin: a caret range has nothing to freeze it, because there is no lockfile for
`marketplace.packages[]`. `apm pack` re-resolved the range against upstream on **every** run, so
an upstream `v1.2.4` would immediately invalidate the committed `ref`/`sha` and fail
`apm-pack-check-clean` with exit 4 — blocking every push in the repo, triggered by a third party
at an unrelated moment, with no local change to explain it. Root `apm.yml` therefore declares an
exact `version: "1.2.3"`, which `apm pack` freezes into `.claude-plugin/marketplace.json` as
`ref: v1.2.3` + an explicit `sha`. Two consequences, both intended: the committed ref/sha is
genuinely reproducible and cannot move under the repo, and picking up a new upstream release is a
deliberate act — a human edits the `version:` string in root `apm.yml` and re-runs `apm pack`.
apm has no version-bump automation (established under "Versioning" in issue #90's plan), so an
ageing pin is the accepted cost of a push gate that only fires on this repo's own changes.
Note the pin does not make the entry offline-resolvable: an exact version still requires a
`git ls-remote`, which is why two pre-push hooks need the network (see `AGENTS.md`).
- **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