[US-277] feat: .claude-plugin marketplace + plugin manifests with dataset catalog guard - #386
[US-277] feat: .claude-plugin marketplace + plugin manifests with dataset catalog guard#386rucka wants to merge 18 commits into
Conversation
Verdict
PR: [#386] · Author: rucka · Reviewer: independent reviewer (Claude Opus 5) · Date: 2026-07-28 · Story: [US-277] · Type: feature (config + tooling + docs) Classification matrix — per dimension
Tier = max(assessed) = yellow. Cost = highest detected signal = green. Review value is a floor (D17): confirm or raise, never lower. No refinement-time matrix existed on story #277 (and PR #386 carries no AssessmentsSecurity — Input validationVerdict: green — the only new inputs are two repo-owned JSON manifests, parsed by Details
Security — Output handlingVerdict: green — no rendering, encoding, or serialization surface. Guard output is thrown Security — AuthenticationVerdict: green — no authentication surface touched. Security — AuthorizationVerdict: yellow — no access-control code changes, but the channel changes what runs with the user's credentials: skills loaded from the plugin cache resolve pair's Security — Introduced vulnerabilitiesVerdict: green — 0 introduced, 0 pre-existing. Details
CostVerdict: Details
Architecture (Coupling)Verdict: green — the one new integration ( Details
DetailsFindings by severityCritical (must fix before merge)
Major (should fix before merge)
Minor (consider)
Questions
Positive feedback
Functionality & requirements (AC coverage)
Testing & quality gatesAll gates re-run independently in a detached worktree pinned to
Adoption compliance
Tech debtSurfaced, not blocking:
No Documentation
Performance & deployment
Reviewed independently from story #277's acceptance criteria, the PR diff/description, and the branch code at |
… fail-closed catalog sync Review round 1 (PR #386): - assertNoDistributedAgents -> assertSkillsOnlyDistribution: asserts agents, hooks AND mcpServers in one loop/message (hooks = shell commands on every user's machine, the highest-consequence key); drift-injection test covers all three - requireRelativePath rejects any '..' segment, naming the ADL Decision 1 invariant (a plugin cannot reach outside its own root) instead of surfacing later as a vague "stale entry" - assertSkillCatalogInSync builds declaredSet explicitly; duplicate scan is now side-effect-free, so reordering the checks can no longer make the guard fail open - new assertNoRootPluginComponents + real-repo test: with source "./" the repo IS the payload, so the payload-level skills-only guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json — now enforced, not assumed - tests: it.each(declared) (no re-read/second source of truth); vacuous not.toContain('pair-next,') -> 'pair-next/SKILL.md' Refs: #277
…nnel scoping Review round 1 (PR #386): - claude-code.mdx + ADL: the "known limitation" said a marketplace project gets the guideline defaults / behaves as if adoption files were absent. Wrong: source "./" copies .pair/adoption/ into the plugin cache, so skills read PAIR's decisions as if they were the project's (pair's board/org via way-of-working, pair's risk-matrix and coverage deltas) under the user's credentials. Both texts now describe the real mechanism and state that adoption-dependent skills require the CLI channel (#392 tracks the structural fix) - claude-code.mdx: both-channels consequence spelled out (duplicate skill names make the bare /pair-... form non-unique, degrading every cross-skill composition, not just tidiness); channel-scope note under Configure Agent File (CLI channel only) - claude-code.mdx + ADL: bare-vs-prefixed naming caveat, verified live on CLI v2.1.220 under --plugin-dir with no pair skills in project/user scope: pair-process-review then pair-capability-verify-quality both resolve by bare name (composition works) - ADL Decision 3: non-declaration prevents LOADING agent-browser / .claude/agents as a distributed pair skill or agent, not copying it; the payload-level guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json (now guarded) - RELEASE.md + release-process.mdx: skills-only line covers root-level paths and states the guard is the automatic backstop for all of it Refs: #277
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
… fail-closed catalog sync Review round 1 (PR #386): - assertNoDistributedAgents -> assertSkillsOnlyDistribution: asserts agents, hooks AND mcpServers in one loop/message (hooks = shell commands on every user's machine, the highest-consequence key); drift-injection test covers all three - requireRelativePath rejects any '..' segment, naming the ADL Decision 1 invariant (a plugin cannot reach outside its own root) instead of surfacing later as a vague "stale entry" - assertSkillCatalogInSync builds declaredSet explicitly; duplicate scan is now side-effect-free, so reordering the checks can no longer make the guard fail open - new assertNoRootPluginComponents + real-repo test: with source "./" the repo IS the payload, so the payload-level skills-only guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json — now enforced, not assumed - tests: it.each(declared) (no re-read/second source of truth); vacuous not.toContain('pair-next,') -> 'pair-next/SKILL.md' Refs: #277
0d25763 to
c1692f0
Compare
…nnel scoping Review round 1 (PR #386): - claude-code.mdx + ADL: the "known limitation" said a marketplace project gets the guideline defaults / behaves as if adoption files were absent. Wrong: source "./" copies .pair/adoption/ into the plugin cache, so skills read PAIR's decisions as if they were the project's (pair's board/org via way-of-working, pair's risk-matrix and coverage deltas) under the user's credentials. Both texts now describe the real mechanism and state that adoption-dependent skills require the CLI channel (#392 tracks the structural fix) - claude-code.mdx: both-channels consequence spelled out (duplicate skill names make the bare /pair-... form non-unique, degrading every cross-skill composition, not just tidiness); channel-scope note under Configure Agent File (CLI channel only) - claude-code.mdx + ADL: bare-vs-prefixed naming caveat, verified live on CLI v2.1.220 under --plugin-dir with no pair skills in project/user scope: pair-process-review then pair-capability-verify-quality both resolve by bare name (composition works) - ADL Decision 3: non-declaration prevents LOADING agent-browser / .claude/agents as a distributed pair skill or agent, not copying it; the payload-level guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json (now guarded) - RELEASE.md + release-process.mdx: skills-only line covers root-level paths and states the guard is the automatic backstop for all of it Refs: #277
Escalating to human — round 5, new findings on a scope addition after round-4 convergenceRound 4 converged (all findings resolved) and a Rounds so far (summary)
Full per-finding detail for all 5 rounds lives in the working review log (see Persistence note below). Round 5 — still-open findings (per finding: what/where)Major
Minor Questions Why escalating rather than fixing silently: two judgment calls beyond mechanical fixes — (a) whether the Quality gates: unchanged/not re-run this round — none of the findings are known to flip a gate (config semantics, decision-record, doc counts, unrelated-file provenance). Convention (for whoever picks this up next): any further rework or re-review on story #277 — including manual, out-of-band rounds run directly against this PR — should be appended to Persistence note: that working log is an untracked file living only in the persistent authoring worktree Not merging. Awaiting human decision on this escalation. |
Remediation — round 6 (the new CLI code)
Head: The five Majors
The Minors
Still open, and why
Also removed: a stray |
…aset catalog guard - Add .claude-plugin/marketplace.json (marketplace `pair`, one entry, source "./") and .claude-plugin/plugin.json (plugin `pair`, 40 explicit skill dirs, skills only) - Guard module claude-plugin-manifest.ts: schema validation (Claude Code required fields), dataset-derived catalog sync, stale-path + duplicate + agents (D23) checks - Guard asserts, never generates: manifest stays hand-maintained (AC3), failure names the exact entry to add/remove - ADL records the spike findings + packaging decisions (marketplace-root source so skills' relative KB links resolve in the plugin cache, no version pin, skills only) - Tasks: T1 — spike (schema verified against Claude Code docs + `claude plugin validate` on CLI v2.1.220), T2 — author the manifests Refs: #277
… the manifest - Claude Code integration page: "Install Channels" table (marketplace vs CLI, one per project — D1), marketplace install/update commands, adoption-resolution caveat - RELEASE.md + docs-site release-process: hand-maintained-manifest checklist (catalog entries, `claude plugin validate .`, skills-only) pointing at the guard - Task: T3 — docs site + release-checklist entry Refs: #277
… fail-closed catalog sync Review round 1 (PR #386): - assertNoDistributedAgents -> assertSkillsOnlyDistribution: asserts agents, hooks AND mcpServers in one loop/message (hooks = shell commands on every user's machine, the highest-consequence key); drift-injection test covers all three - requireRelativePath rejects any '..' segment, naming the ADL Decision 1 invariant (a plugin cannot reach outside its own root) instead of surfacing later as a vague "stale entry" - assertSkillCatalogInSync builds declaredSet explicitly; duplicate scan is now side-effect-free, so reordering the checks can no longer make the guard fail open - new assertNoRootPluginComponents + real-repo test: with source "./" the repo IS the payload, so the payload-level skills-only guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json — now enforced, not assumed - tests: it.each(declared) (no re-read/second source of truth); vacuous not.toContain('pair-next,') -> 'pair-next/SKILL.md' Refs: #277
…nnel scoping Review round 1 (PR #386): - claude-code.mdx + ADL: the "known limitation" said a marketplace project gets the guideline defaults / behaves as if adoption files were absent. Wrong: source "./" copies .pair/adoption/ into the plugin cache, so skills read PAIR's decisions as if they were the project's (pair's board/org via way-of-working, pair's risk-matrix and coverage deltas) under the user's credentials. Both texts now describe the real mechanism and state that adoption-dependent skills require the CLI channel (#392 tracks the structural fix) - claude-code.mdx: both-channels consequence spelled out (duplicate skill names make the bare /pair-... form non-unique, degrading every cross-skill composition, not just tidiness); channel-scope note under Configure Agent File (CLI channel only) - claude-code.mdx + ADL: bare-vs-prefixed naming caveat, verified live on CLI v2.1.220 under --plugin-dir with no pair skills in project/user scope: pair-process-review then pair-capability-verify-quality both resolve by bare name (composition works) - ADL Decision 3: non-declaration prevents LOADING agent-browser / .claude/agents as a distributed pair skill or agent, not copying it; the payload-level guarantee is the absence of root-level agents/, commands/, hooks/, .mcp.json (now guarded) - RELEASE.md + release-process.mdx: skills-only line covers root-level paths and states the guard is the automatic backstop for all of it Refs: #277
The 3-key denylist (agents/hooks/mcpServers) left 10 of the schema's 13 component/behaviour keys permitted. Probe-verified on CLI v2.1.220 that settings, dependencies, monitors and commands all pass `claude plugin validate` AND the old guard; `commands` was already forbidden at payload level, so the two halves of one rule disagreed. Sharper hole, same family: the schema allows `skills` on the MARKETPLACE ENTRY, where it REPLACES plugin.json's catalog — and assertSkillCatalogInSync reads plugin.json only. Injecting one key installed a 1-skill plugin instead of 40 with every guard still green, voiding AC2's only automatic enforcement. - allowlist per manifest kind: plugin.json = metadata + `skills`; marketplace entry = metadata + source/category/tags/strict, NO `skills` (unknown or newly specified keys now fail closed) - ROOT_PLUGIN_COMPONENT_PATHS += `skills` (a root skills/ dir is auto-discovered ON TOP of the manifest field, bypassing the catalog guard) - drift injection extended to all 13 schema component keys, per kind - drop dead `displayName` from plugin.json (not in the schema, never read; the allowlist now rejects it) - cite the ADL's Decision 5 in prose instead of the unresolvable "D23/R9.3" (D23 already means mechanical isolation in the skill corpus) - both release checklists: allowlist wording, 5 root paths, never --strict 112 guard tests (was 83). Verified after the change: marketplace add <path> + install + `plugin details` -> Skills (40), Agents/Hooks/MCP/LSP (0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ied claims only - adoption callout now names /pair-capability-record-decision (the sole writer of adoption files) and states the write side: on the marketplace channel a recorded ADR/ADL lands in the plugin cache — invisible to the reader's repo and destroyed by the next /plugin update. No workaround; record decisions on the CLI channel. Mirrored in the ADL Consequences. - channel table: the Marketplace "use it when" cell carries the caveat inline (KB-reading skills only) + anchor to the new section heading, so a reader choosing from the table alone cannot miss it - both-channels callout: drop the unverified failure mode. We observed bare-name resolution only with the plugin as the SOLE source, so the docs now say what is known — resolution order between project scope and plugin is unspecified, don't rely on it, use /pair:pair-… to be certain. ADL consequence aligned. - ADL Decision 4: `--strict` FAILS on the missing version pin (it does not warn) — verified v2.1.220 — so no gate may run --strict while it stands - ADL Decision 5: records the allowlist + the marketplace-entry `skills` hole, plus an identifier note (D23/R9.3 resolve to nothing in-repo; D23 already means mechanical isolation) making this ADL section the rule of record - ADL: AC1 marketplace-resolution evidence, obtained pre-merge via `marketplace add <path>` in a sandboxed CLAUDE_CONFIG_DIR Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d list from schema Third hand-edited surface guarded: marketplace.json's own top level (kind 'marketplace') — rejects forceRemoveDeletedPlugins, allowCrossMarketplaceDependenciesOn and metadata.pluginRoot (nested check, since metadata stays allowed for its descriptive fields). All three pass `claude plugin validate .` unguarded. ROOT_PLUGIN_COMPONENT_PATHS now DERIVED from SCHEMA_COMPONENT_KEYS + '.mcp.json' instead of hand-enumerated: the hand list lagged the schema (monitors missing — unsandboxed background scripts, hooks' trust tier). Manifest half and payload half of the rule now extend from one edit. Test-comment fixes: --strict FAILS (not warns); test title no longer cites bare D23/R9.3 (ADL Decision 5 is the rule of record). 126 tests (was 112). Both release checklists updated to three surfaces + derived root list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…channel in README claude-code.mdx: "loads skills only" (declares none + no root auto-discovery dirs), with an explicit paragraph that every repo file IS copied into the cache (.claude/agents/, agent-browser) so non-declaration is a LOADING control; "stays read-only" replaced with "disposable, not storage" — it contradicted the record-decision write-loss callout 25 lines below. README: native marketplace install path at the front door (pick one channel; adoption-dependent skills need the CLI), 38 -> 40 skills. ADL Decision 5: three surfaces + derived root-payload list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-only setup skill Scope widened deliberately (agreed with the maintainer): the exclusion is the enabler for #392's setup-assistant channel, so it lands where it is used rather than as a standalone mechanism. Test-first: four cases written and verified RED before any implementation. A registry may now declare `exclude: [...]` — source-relative entries the copy never installs. Threaded config -> RegistryConfig -> SyncOptions -> the copy pipeline, matching how `flatten`/`prefix` already travel. Why it exists: the marketplace channel will ship a setup skill that must exist ONLY when installed from the marketplace. Without the exclusion the CLI also writes it into the project, and the same skill name then answers from two trees (`~/.claude/plugins/cache` and the project's `.claude/skills/`) whose resolution order this repo's own ADL records as never observed — verified only with the plugin as sole source. That is an execution ambiguity, not a cosmetic duplicate. Design points, each with a test: - Matching is PER PATH SEGMENT, never a string prefix: excluding `process/setup` must not silently drop `process/setup-helper`. That is the classic shape of this bug and it has its own test. - An excluded entry's WHOLE SUBTREE is skipped, so a `references/` sub-doc does not survive its skill. - Filtered before validation and before any `mkdir`, so an excluded entry cannot contribute a flatten collision, a directory mapping or a link-rewrite pass — as if it were never in the source. - Absent ⇒ exact previous behaviour, asserted, so no other registry moves. - `buildCopyOptions` reads it defensively: it is called with hand-built RegistryConfig objects too, not only with `extractRegistries` output — four existing tests proved that the moment a required field was assumed. - `extractRegistries` ignores a non-array value rather than trusting it. KNOWN LIMIT, in the code and in #392: this prevents WRITING an entry, not having it. The skills registry runs `behavior: overwrite`, which performs no cleanup, so an excluded entry already present in a target is left untouched. `collectInstallableFiles` extracted to keep `copyDirectoryWithTransforms` under the 50-line ceiling — caught by the gate, not by the tests. Also: the catalog guard flagged `pair-process-brainstorm` missing from `plugin.json` after #387 merged. Added — 41 entries. Exactly what that guard exists for, and the first time it has fired on a real omission. `pnpm quality-gate` green. content-ops 22 copy cases, pair-cli 190 registry. Refs #277 · #392 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t it drift - README + claude-code.mdx said 40; the dataset holds 41. - SKILL_COUNT_RE now covers 'N declared pair skills'; new SKILL_COUNT_PROBE_RE pins the quoted 'Skills (N)' transcript. Both were unguarded phrasings. - Drop .codex/agents/*.toml (harness story, and their paths point at a non-existent .Codex/workflows/) and two prettier-only sweeps. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cision 6 - The plain (no flatten/prefix) copy path walked the tree itself and never consulted exclude, so the option was a silent no-op for any registry that declares it without transforms. Both paths now share one isExcluded. - validateExcludes: a non-array or non-string member is a config error, like include. - New smoke scenario drives the real CLI over both paths + the invalid case; assert_no_file added to the smoke lib. - ADL Decision 6 records the mechanism, the #392 motivation and the no-cleanup limit; Decision 3 is now count-free and the probe transcripts are marked as observed-then. - configuration.mdx documents exclude, its segment-wise match and the limit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iling Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
config.json at the repo root was a leftover of a manual CLI reproduction, picked up by a git add -A. It is not part of the story and shadows the CLI's own config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… catalog Scope revision agreed with the maintainer. The plugin no longer ships the whole repository: marketplace.json's entry sources dataset/plugin/, whose .claude-plugin/ plugin.json declares one authored, ISOLATED skill — pair-capability-setup-cli — which installs pair-cli and runs it, so the catalog and KB are produced by the CLI in the user's repo. Why: the whole-repo payload put pair's own .pair/ in every plugin cache, so a skill read pair's adoption decisions as the project's, and record-decision wrote into a cache the next /plugin update erased. Both were documented as known limitations. They are now structurally absent — verified, not argued: the installed cache holds .claude-plugin/ + skills/ only, 12K, and 'claude plugin details' reports Skills (1). Also caught by probing: Claude Code wants the manifest at <root>/.claude-plugin/plugin.json, not <root>/plugin.json. The first layout would have shipped an uninstallable plugin; no repo guard would have caught it. - Catalog guard derives the expected list from the bootstrap corpus on disk. - New assertBootstrapSkillsValid replaces skills:conformance for that corpus, asserting frontmatter and the isolation invariant on LINK TARGETS (the skill legitimately explains the rule in prose). - skills/ is exempt from the root-payload check at the new root, where it IS the declared payload; every other component key stays forbidden. - Skills (N) transcripts are pinned to the DECLARED count, not the dataset's. - 'Pick one channel' retired: no skill name resolves from two sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A 'git add -A' during conflict resolution staged operations.ts with markers still in it. Both hunks are wanted: flattenDepth (from #411, now on main) and exclude. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81ed374 to
e679012
Compare
Scope revision, round 7 — the payload changed, so this PR needs a fresh review
Head What changedThe plugin no longer ships the catalog. The reason is not aesthetic. The whole-repo payload put pair's own Verified by installing it, not by argumentCLI v2.1.220, sandboxed
A layout fact the probe caught and no repo guard would have: Claude Code expects the manifest at Guards rewritten
What a reviewer should look at
Also in this push
|
PR Information
PR Title: [US-277] feat:
.claude-pluginmarketplace + plugin manifests with dataset catalog guardStory/Epic: #277 (epic #213 — Skill marketplace + Quickstart + external KB scaffold)
Type: Feature
Priority: Medium
Labels: user story,
risk:yellowThe plugin no longer ships the catalog. It ships one bootstrap skill.
marketplace.json's entry now sourcespackages/knowledge-hub/dataset/plugin, whose.claude-plugin/plugin.jsondeclares a single authored, isolated skill —pair-capability-setup-cli— which installspair-cliand runs it, so the knowledge base and the full skill catalog are produced by the CLI in the user's repository.Why, in one paragraph: the whole-repository payload (
source: "./") put pair's own.pair/inside every plugin cache. A skill step that read an adoption file resolved pair's decisions as if they were the user's, with no graceful degradation — from the skill's point of view the files were present — and/pair-capability-record-decisionwrote adoption files into a cache the next/plugin updateerased. Both were shipped as documented "known limitations". This revision removes their cause.Verified live, not argued (CLI v2.1.220, sandboxed
CLAUDE_CONFIG_DIR, this branch):claude plugin details pair@pair→Skills (1) pair-capability-setup-cli,Agents (0),Hooks (0),MCP servers (0),LSP servers (0)du -shon the cache) against ~19 MB before; always-on token cost ~124.claude-plugin/+skills/and no.pair/— walked to confirmclaude plugin validatepasses at both the plugin root and the repo root, the latter resolving the entry's manifest through the newsourceOne layout fact the probe caught, and no repo guard would have: Claude Code expects the manifest at
<plugin root>/.claude-plugin/plugin.json, not at<plugin root>/plugin.json. The first attempt at this revision had the bare form and would have shipped an uninstallable plugin.What this invalidates. Review rounds 1–6 were conducted against the whole-repo shape. ADL Decisions 1, 3 and 5 are rewritten (2 and 4 survive); the hand-maintained 41-entry catalog and its dataset-derived guard are gone, replaced by a corpus-derived list; "pick one channel, not both" is retired, since no skill name resolves from two sources. The sections further down that predate this banner describe the old shape unless they say otherwise.
Summary
What Changed
Adds the native Claude Code install channel (D1):
.claude-plugin/marketplace.json+.claude-plugin/plugin.jsonat the repo root, so/plugin marketplace add foomakers/pair→/plugin install pair@pairinstalls pair's full 40-skill catalog with nopair-clistep. The CLI (pair-cli init/update) remains the tool-agnostic channel for Cursor/Windsurf/Copilot and for projects that want their own.pair/adoption files.marketplace.json: thin metadata —name,owner, oneplugins[]entry (source: "./").plugin.json: owns the component list — 40 explicit./.claude/skills/pair-<flattened-name>entries (not a glob:.claude/skills/agent-browseris a third-party skill this repo consumes, and non-declaration means it is never exposed or loaded as a distributed pair skill; note the exact scope — withsource: "./"the file is still copied into the cache, so this is a loading control, not a redistribution control, as the ADL's Decision 3 note states). Noversionpinned (falls back to commit SHA so/plugin updatealways fetches current). Skills only, enforced as an allowlist on all three hand-edited surfaces:plugin.jsonmay carry metadata +skills;marketplace.json's own top level may carry marketplace metadata +owner/plugins(neverforceRemoveDeletedPlugins,allowCrossMarketplaceDependenciesOnormetadata.pluginRoot); a marketplace entry may carry metadata +source/category/tags/strictand noskills— every other schema key (hooks,mcpServers,lspServers,monitors,commands,dependencies,settings,userConfig,channels,outputStyles,themes) fails the guard, as does any key the schema doesn't declare. Rule of record: the ADL's Decision 5 (the "D23/R9.3" identifiers resolve to nothing in-repo, so they are no longer cited as normative).packages/knowledge-hub/src/tools/claude-plugin-manifest.ts+ colocated test, 301 lines): the manifest is hand-maintained by design (AC3) — the guard derives the expected catalog frompackages/knowledge-hub/dataset/.skills/through the real install transform (installedSkillDir, the same onepair updateapplies) and fails loudly naming the exact entry to add/remove, so a skill added/renamed/removed without a manifest update is caught bypnpm test, not discovered by a user.apps/website/content/docs/integrations/claude-code.mdxgets a new "Install Channels" section (marketplace vs CLI comparison table, install/update/uninstall commands, a dedicated "Adoption Files on the Marketplace Channel" section documenting the known adoption-resolution limitation — read and write — with the caveat also carried inline in the channel table).RELEASE.mdand the docs-site release-process page get a 3-item release checklist for the manifest (never runclaude plugin validate --strict: it fails the deliberate no-version-pin)..pair/adoption/decision-log/2026-07-28-marketplace-plugin-packaging.mdrecords the spike findings and all five packaging decisions.Why This Change
Fills the D1 gap: pair had no native Claude Code distribution path — only the CLI. A user in Claude Code alone had to run
pair-cli initjust to get the skills. The marketplace channel gives a zero-setup path while keeping the CLI channel for projects that want the KB/adoption files living in their own repo.Story Context
User Story: As a Claude Code user, I want to install pair's skill catalog via
/plugin marketplace add, so I can use pair's SDLC skills without a CLI bootstrap step.Acceptance Criteria (story #277, verbatim):
foomakers/pairrepo with.claude-plugin/marketplace.jsonpresent When a Claude Code user runs/plugin marketplace add foomakers/pairThen pair's skills install as a Claude Code plugin, without needingpair-clipackages/knowledge-hub/dataset/.skills/, using the same names/descriptions as the dataset source (no drift between the two)marketplace.jsonis updated by hand as part of that release's checklist — it is not auto-generatedBusiness rules: dual channel (D1, same source of truth); no named role agents distributed (D23/R9.3 — skills only); hand-maintained, not generated.
AC → delivery mapping:
pair-cli.claude-plugin/marketplace.json(one entry,source: "./") +.claude-plugin/plugin.json(40 skill entries)claude plugin marketplace addaccepts a path, so the marketplace path is testable without being onmain. Sandboxed viaCLAUDE_CONFIG_DIRat HEAD:marketplace add <path>→ "Successfully added marketplace: pair",plugin install pair@pair→ "Successfully installed",claude plugin details pair@pair→Skills (40),Agents (0),Hooks (0),MCP servers (0),LSP servers (0). Plusclaude plugin validate .(v2.1.220) andclaude --plugin-dir <repo>from outside the repo loading all 40 skills aspair:pair-*, bare/pair-…also resolving (composition pairpair-process-review→pair-capability-verify-qualityboth ACCEPTED). Residual gap: only GitHub-shorthand resolution offoomakers/pair, which genuinely resolves after merge.plugin.jsonskills= 40 dataset-derived pathsSKILL.mdfrontmatter (ADL Decision 3 — copying them in would create a second drift surface). Guarded instead byassertSkillCatalogInSync(expected list derived from the dataset through the real install transform) + a per-entry test pinning each declared path to its frontmattername, with the mirror-equality guard pinning name+description byte-for-byte to the dataset. AC2 on story #277 is annotated with this pointer. For the human merge gate: AC2's wording was re-read after refinement ("provably non-drifting from the dataset source" instead of the literal "using the same names/descriptions as the dataset source") — please acknowledge that re-reading explicitly in the merge decision rather than absorbing it silently, since the AC record is the contract a later reader validates against.RELEASE.md+release-process.mdxchecklist; guard asserts, never generatesSKILL.md, forbidden component key — each fails naming the exact hand edit.claude-code.mdxInstall Channels section (table + two callouts)/pair-…form match two sources, and resolution order between project scope and the plugin is unspecified — we verified bare-name resolution only with the plugin as the sole source, so the docs claim nothing more and point at the prefixed/pair:pair-…form when certainty is needed. No automatic detection of the both-channels state exists; that would be its own story.Classification
Review-time matrix (establishment — story #277 carries no refinement-time matrix; the same gap exists on recently merged #385/#381/#378/#377, so the systemic side warrants its own story, to be filed rather than resolved inside this PR):
tech/risk-matrix.mddeclares no criticality table → KB default Mediumapps/pair-cli/src/registry/,packages/content-opscopy paths). Still yellow, not red: additive behind an option nothing sets yet, no schema/migration/infra change, and both copy paths are covered by unit tests plus a smoke scenario over the real CLIknowledge-hub/src/tools, no new cross-context integrationTier = max = 🟡 yellow →
risk:yellowapplied to this PR. Cost class: 🟢 green (no paid SDK, no provisioning, no LLM/API call added) — computed, not projected as a tag (Active: riskonly).Changes Made
Implementation Details
claude plugin validate .on CLI v2.1.220 — findings recorded before any manifest was written..claude-plugin/marketplace.json,.claude-plugin/plugin.json— repo-root plugin (source: "./"), so.claude/skills/and.pair/knowledge/both land in the plugin cache and every skill's relative KB link (../../../.pair/knowledge/...) resolves at runtime (a slimmer plugin dir would ship skills with dangling KB pointers — plugins can't reach outside their own root).parseMarketplaceManifest/parsePluginManifest(required-field validation per the schemastore schemas),expectedPluginSkillPaths(dataset-derived, sorted, so a new skill extends it with zero code edits),assertSkillCatalogInSync(missing/stale/duplicate detection with copy-pasteable fix),assertDeclaredSkillsResolve(no danglingSKILL.mdpointer),assertSkillsOnlyDistribution(per-manifest-kind allowlist of permitted keys — unknown or newly specified schema keys fail closed),assertNoRootPluginComponents(payload-level: no rootagents/,commands/,hooks/,skills/,.mcp.json— a rootskills/dir is auto-discovered on top of the manifest field and would bypass the catalog guard).claude-code.mdx); 3-item release checklist (RELEASE.md+release-process.mdx).2026-07-28-marketplace-plugin-packaging.md.Files Changed
26 files, +1926/−24. The manifest half is what the story asked for; the CLI half is the
excludeenabler added inc1692f05(see the scope-addition section at the bottom)..claude-plugin/marketplace.json,.claude-plugin/plugin.json,packages/knowledge-hub/src/tools/claude-plugin-manifest.ts(+ its colocated test),.pair/adoption/decision-log/2026-07-28-marketplace-plugin-packaging.mdexcludeverification:scripts/smoke-tests/scenarios/registry-exclude.sh(both copy paths + the invalid-config case, driving the real packaged CLI)exclude):apps/pair-cli/src/registry/{operations,resolver,validation}.ts(+ their tests),packages/content-ops/src/ops/SyncOptions.ts,packages/content-ops/src/ops/copy/{copy-directory,copy-directory-transforms}.ts(+ test),packages/content-ops/src/file-system/file-operations.ts(the sharedisExcluded),scripts/smoke-tests/lib/utils.sh(assert_no_file)packages/knowledge-hub/dataset/plugin/.claude-plugin/plugin.json,packages/knowledge-hub/dataset/plugin/skills/pair-capability-setup-cli/SKILL.md(authored, isolated); the repo-root.claude-plugin/plugin.jsonis deleted — the manifest moved to the plugin rootREADME.md,RELEASE.md,apps/website/content/docs/contributing/release-process.mdx,apps/website/content/docs/integrations/claude-code.mdx,apps/website/content/docs/reference/configuration.mdx,apps/website/lib/docs-staleness-check.ts(+ test) — the skill count was 40 in three places and is 41; two phrasings that let it drift are now pinned by the staleness gatepackages/knowledge-hub/src/conformance/code-host-routing.test.tsshows in a three-dot diff but is byte-identical toorigin/main— a merge-base artifact, contributing nothing to the mergeTesting
Test Coverage
claude-plugin-manifest.test.ts(126 tests) — manifest parsing (required fields, malformed JSON, badsourceshapes), catalog sync assertion (missing/stale/duplicate detection with exact messages), dangling-SKILL.mddetection, allowlist drift injection over all 13 component/behaviour keys the published schemas declare, on each of the three hand-edited surfaces (plugin manifest,marketplace.json's own top level — incl.forceRemoveDeletedPlugins,allowCrossMarketplaceDependenciesOn, nestedmetadata.pluginRoot— and eachplugins[]entry) (plus a non-schema key such asdisplayName, and the marketplace-entryskillscase whose message spells out that it replacesplugin.json's catalog), plugin-root escape (..) rejection, and root-level component-payload absence, whose path list is derived fromSCHEMA_COMPONENT_KEYS(so it coversmonitors/and anything a future schema adds) plus.mcp.json.claude plugin validate .passes on CLI v2.1.220 (one expected "no version" warning — documented tradeoff);claude --plugin-dir <repo>loads all 40 skills from a directory outside the repo, confirmed invocable as/pair:pair-nextand the bare/pair-next.Test Results
Quality Assurance
Review Areas
Deployment Information
No deployable package touched — this PR only changes
packages/knowledge-hub(a guard module + test), root.claude-plugin/config,apps/websitedocs content, andRELEASE.md. No environment/config/migration impact..claude-plugin/is picked up by Claude Code directly from the repo — no build/release step required for it to take effect.Reviewer Guide
Testing the Changes
Key Test Scenarios
packages/knowledge-hub/dataset/.skills/without touching.claude-plugin/plugin.json→pnpm testfails naming the exact entry to add/remove.hooks,mcpServers,monitors,settings,dependencies,commands, a typo, … — or create a root-levelagents/,commands/,hooks/,skills/or.mcp.json→ guard fails, pointing at the ADL's Decision 5 (withsource: "./"the repo IS the payload, so the root-level paths would load regardless of the manifest).2b. Add
"skills": [...]to themarketplace.jsonentry → guard fails explaining that an entry-levelskillsREPLACESplugin.json's catalog and is invisible toassertSkillCatalogInSync. Runtime-confirmed before the fix: one such key installed a 1-skill plugin instead of 40, withclaude plugin validateand every other guard still passing.SKILL.md→ guard fails citing the broken path.Dependencies & Related Work
Related PRs
apps/pair-cli, but this PR's diff has zero overlap with it (git diff origin/main...HEAD --stattouches only.claude-plugin/,packages/knowledge-hub/src/tools/,apps/website/content/docs/,RELEASE.md,.pair/adoption/decision-log/).apps/website/content/docs/getting-started/quickstart*.mdx— deliberately left untouched here; the marketplace install path lives only inintegrations/claude-code.mdxfor this PR.Follow-up Work
claude-code.mdx, not fixed here) — Project-scoped adoption resolution for the Claude Code plugin (marketplace) channel #392: because the plugin source is"./", pair's own.pair/adoption/travels inside the plugin cache, so a skill step that reads an adoption file resolves pair's decisions and treats them as the project's. No graceful-degradation path fires: from the skill's point of view adoption files are present. Concretely, a marketplace user can be steered at pair's own board/org (way-of-working.md), and gated by pair's risk-matrix/coverage deltas, with their own credentials — so adoption-dependent skills (/pair-capability-write-issue,/pair-process-review,/pair-capability-verify-quality,/pair-capability-record-decision, and any process skill composing them) require the CLI channel. The docs now say exactly this; the structural fix (project-scoped adoption resolution) is tracked as Project-scoped adoption resolution for the Claude Code plugin (marketplace) channel #392./pair-capability-record-decisionis the sole writer of adoption/context-map files and resolves its targets skill-relative, so a recorded ADR/ADL is written into the plugin cache — invisible to the user's repository and destroyed by the next/plugin update(silent loss of a recorded decision). Named in the docs callout and the ADL Consequences; Project-scoped adoption resolution for the Claude Code plugin (marketplace) channel #392's scope note now covers read and write.Stakeholder Communication
Not applicable — internal tooling/distribution change, no customer-facing behavior change beyond the new install path documented above.
Scope addition (
c1692f05) — registry-levelexclude, the enabler for a single marketplace channelWidened deliberately with the maintainer, after this PR's own limitation was talked through. This adds code, not documentation — flagged prominently because the PR's title and everything above describe manifests only.
A registry may now declare
exclude: [...]— source-relative entries the copy never installs. Threaded config →RegistryConfig→SyncOptions→ the copy pipeline, the same routeflatten/prefixalready travel.Why it belongs here
This PR documents two channels and warns "install one, never both — nothing detects this state". The agreed resolution (tracked in #392) is one channel: the marketplace ships a single setup skill that invokes
pair-cli, and everything else arrives in the project with consistent paths. That skill must exist only when installed from the marketplace — otherwise the CLI writes it too, and the same skill name answers from two trees (~/.claude/plugins/cacheand the project's.claude/skills/) whose resolution order this PR's own ADL records as never observed. The exclusion is what makes the single-channel shape possible, so it lands with the story that creates the need.Measured while deciding: of pair's 42 installed skills exactly 1 has no KB reference, and it is the third-party
agent-browser. So "ship the isolated skills" has an empty payload — the setup skill has to be written, not carved out.Design points, each with a test
process/setupmust not silently dropprocess/setup-helper. That is the classic shape of this bug and it has its own test.references/sub-doc does not survive its skill.mkdir, so an excluded entry cannot contribute a flatten collision, a directory mapping or a link-rewrite pass — as if it were never in the source.buildCopyOptionsreads it defensively: it is called with hand-builtRegistryConfigobjects too, not only withextractRegistriesoutput — four existing tests proved that the moment a required field was assumed.extractRegistriesignores a non-array value rather than trusting it.Known limit, in the code and in #392
This prevents writing an entry, not having it. The skills registry runs
behavior: overwrite, which performs no cleanup, so an excluded entry already present in a target is left untouched.Also fixed here
The catalog guard flagged
pair-process-brainstormmissing fromplugin.jsonafter #387 merged — the first time that guard has fired on a real omission. Without it the plugin would have shipped 40 skills out of 41. Catalog now at 41 entries.Verification at
c1692f05:pnpm quality-gategreen, content-ops 22 copy cases, pair-cli 190 registry cases.collectInstallableFileswas extracted to keepcopyDirectoryWithTransformsunder the 50-line ceiling — caught by the gate, not by the tests.