diff --git a/.agents/evals/marketplace/marketplace-architect/eval.yaml b/.agents/evals/marketplace/marketplace-architect/eval.yaml new file mode 100644 index 0000000..7fbbe80 --- /dev/null +++ b/.agents/evals/marketplace/marketplace-architect/eval.yaml @@ -0,0 +1,62 @@ +skill_name: marketplace-architect + +trigger_tests: + - id: explicit-trigger-refactor + name: "Explicit trigger — refactor repo into marketplace" + query: "turn this repo into a plugin marketplace" + should_trigger: true + + - id: implicit-trigger-team-sharing + name: "Implicit trigger — team distribution without saying marketplace" + query: "how do I share my skills with my team so they can install them" + should_trigger: true + + - id: negative-trigger-new-skill + name: "Negative trigger — new skill authoring belongs to write-skill" + query: "write me a new skill for code review" + should_trigger: false + +output_tests: + - id: deterministic-cross-compat-loaded + name: "Deterministic — cross-compat reference loaded before tool-specific recommendations" + type: deterministic + prompt: "audit this repo and recommend how to convert it into a plugin marketplace for both Claude Code and Copilot CLI" + expected_output: > + The skill reads references/cross-compat.md early in its response and demonstrates + awareness of the Claude Code / Copilot CLI format differences — specifically that + manifest paths differ (.claude-plugin/ vs .github/plugin/), agent files differ + (.md vs .agent.md), and hooks placement differs — before recommending any layout. + assertions: + - "Output references or acknowledges the divergence between Claude Code and Copilot CLI manifest paths before proposing a directory layout" + - "Output does not recommend a single shared plugin.json location without noting the two-location requirement (.claude-plugin/ and plugin root)" + - "Output does not write or propose writing any files before presenting a plan" + + - id: deterministic-gate-a-blocks-writes + name: "Deterministic — Gate A plan presented and approval requested before any writes" + type: deterministic + prompt: "help me migrate my skills and agents into a plugin marketplace layout" + expected_output: > + The skill produces a migration plan (checklist of old path → new path, one row per + file) and explicitly asks the user for approval before proceeding to generate any + manifest files. No plugin.json or marketplace.json is written or shown as written output. + assertions: + - "Output contains a migration plan or checklist with at least one file-move entry in the form 'old path → new path'" + - "Output explicitly asks the user to approve or confirm the plan before proceeding" + - "Output does not contain a complete plugin.json or marketplace.json file unless the user has already said 'yes' or equivalent in the prompt" + + - id: llm-rubric-adopt-plugin + name: "LLM-rubric — adopt external plugin covers all required checks without premature writes" + type: llm-rubric + prompt: "I want to add this plugin to my marketplace: https://github.com/example/my-tool-plugin" + expected_output: > + The skill fetches and inspects the plugin source, classifies what assets it contains + (skills, agents, hooks, MCP servers), checks whether any existing plugin in the + marketplace has a naming conflict, evaluates cross-tool compatibility against the + divergence table, and summarises what would be added to marketplace.json — all without + writing any files and without proceeding past Gate A without explicit user approval. + assertions: + - "Response identifies what asset types the external plugin contains (skills, agents, hooks, and/or MCP servers)" + - "Response checks or asks about naming conflicts with existing plugins in the marketplace" + - "Response notes at least one Claude Code vs Copilot CLI compatibility consideration for the adopted plugin" + - "Response summarises the proposed change to marketplace.json without writing the file" + - "Response asks for user approval (Gate A) before any file is created or modified" diff --git a/.agents/skills/marketplace-architect/META.md b/.agents/skills/marketplace-architect/META.md new file mode 100644 index 0000000..daf0d8d --- /dev/null +++ b/.agents/skills/marketplace-architect/META.md @@ -0,0 +1,21 @@ +```yaml +version: "1.0" +updated: 2026-06-20 + +when: > + Invoked when the user wants to create, manage, or maintain a plugin marketplace for + Claude Code and/or GitHub Copilot CLI. Covers four operations: (a) audit and refactor + a repository of skills/agents/hooks into a plugin marketplace layout, (b) adopt external + plugins/skills/agents from outside sources, (c) update and maintain an existing + marketplace.json and plugin manifests, (d) validate existing plugin manifests for naming, + structure, and cross-tool compatibility. Also triggered implicitly when the user asks + about distributing skills to a team, organizing loose skills into installable units, or + setting up cross-tool distribution — even without saying "marketplace". + +references: + - https://code.claude.com/docs/en/plugins + - https://code.claude.com/docs/en/plugin-marketplaces + - https://code.claude.com/docs/en/plugins-reference + - https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating + - https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace +``` diff --git a/.agents/skills/marketplace-architect/SKILL.md b/.agents/skills/marketplace-architect/SKILL.md new file mode 100644 index 0000000..3449155 --- /dev/null +++ b/.agents/skills/marketplace-architect/SKILL.md @@ -0,0 +1,101 @@ +--- +name: marketplace-architect +description: > + Manages and maintains a plugin marketplace for Claude Code and GitHub Copilot CLI. + Use this whenever the user wants to: create or update a marketplace (marketplace.json, + plugin.json manifests), adopt plugins/skills/agents/hooks from external sources, evaluate + cross-tool compatibility between Claude Code and Copilot CLI, plan plugin groupings and + boundaries, refactor a repository into marketplace format, validate plugin naming, detect + duplicate capabilities, or generate per-plugin install docs — even if they don't use the + word "marketplace". Do NOT use when the user wants to author a new skill from scratch + (use write-skill), debug an existing skill (use diagnose), or run a direct plugin CLI + command (copilot plugin install, claude plugin list). +metadata: + category: marketplace +--- + + + +## Required inputs + +- **Target operation** — what the user wants to do; inferred from request. If ambiguous, ask: audit/refactor, adopt an external plugin, update/maintain an existing marketplace, or validate manifests. +- **Repository path** — path to the repo to act on; defaults to current working directory if not stated. +- **Marketplace name** — kebab-case identifier (e.g. `my-ai-marketplace`); required only when generating a new `marketplace.json`. Infer from repo name if obvious, ask if not. +- **Plugin source** — URL, GitHub slug, or local path; required only when adopting an external plugin. + +## Constraints + +- Load `references/cross-compat.md` before any tool-specific decision — Claude Code and Copilot CLI diverge in ways that cause silent breakage at install time. +- Never write files until the user has approved the plan at Gate A and the specific file contents at Gate B — two separate explicit approvals required. +- If credential-shaped content is detected in any manifest field, halt and redirect to environment variable references (e.g. `$MY_TOKEN`) — do not generate the manifest. +- Produce cross-tool deltas and per-plugin READMEs only when explicitly requested — do not generate them automatically. +- Scripts in `scripts/` are loaded on demand by the step that needs them — never preloaded. +- Flag any `../` cross-references in the audited repo before recommending plugin boundaries — plugins cannot reference files outside their own directory after install-time caching. +- Plugin names must be kebab-case; validate against the reserved name list in `references/claude-code.md` before generating any manifest. +- Do not set `version` in both `plugin.json` and the marketplace entry — `plugin.json` wins silently and causes update failures. + + + + + +## Process + +1. **Identify the operation.** Determine intent from the user's request — one of: (a) audit/refactor a repo into marketplace format, (b) adopt an external plugin/skill/agent, (c) maintain or update an existing marketplace, (d) validate existing manifests. Ask if the operation cannot be inferred. + +2. **Load the compatibility reference.** Read `references/cross-compat.md` before any tool-specific decision. Claude Code and Copilot CLI diverge in manifest paths, agent file naming, and hooks layout — every recommendation depends on this table. + +3. **Execute the operation phase.** + + **(a) Audit/refactor:** Run `scripts/inventory.sh` against the repo to classify every asset (skill / command / agent / hook / prompt / MCP). Flag any `../` cross-references — these break under install-time caching. Recommend plugin groupings by user outcome (~10–20 plugins); warn if proposed count exceeds 20 or falls below 3. Diff skill descriptions for duplicate capabilities before finalising boundaries. Produce a concrete migration checklist: old path → new path, one row per file. + + **(b) Adopt external plugin:** Fetch and inspect the plugin source. Classify included assets. Check for naming conflicts with existing plugins in the marketplace. Evaluate cross-tool compatibility using `references/cross-compat.md`. Summarise what will be added to `marketplace.json`. + + **(c) Maintain/update:** Read current `marketplace.json` and all `plugin.json` files. Identify stale versions, reserved name violations, kebab-case violations, and `version` duplication between plugin.json and marketplace entry. Report findings as a prioritised fix list. + + **(d) Validate:** Run `scripts/validate.sh` (wraps `claude plugin validate` plus custom JSON and naming checks). Report each violation with a recommended fix. Do not proceed to file writes until all errors are resolved. + +4. **Gate A — plan review.** Present the full plan or fix list to the user. Wait for explicit approval before proceeding. Do not interpret silence or "looks good" as approval — require a direct "yes" or equivalent. + +5. **Generate outputs.** After Gate A approval: for audit/refactor and adopt operations, run `scripts/gen_manifests.sh` to produce `plugin.json` (at both `.claude-plugin/plugin.json` and plugin root until the Copilot fallback is verified) and `marketplace.json` (at `.claude-plugin/marketplace.json`; optionally mirror to `.github/plugin/marketplace.json`). Read `references/claude-code.md` for Claude-specific path rules and `references/copilot-cli.md` for Copilot-specific requirements. + +6. **Gate B — file write approval.** Show the user every file that will be written with its full contents. Wait for explicit approval per file or as a batch. Write nothing until approved. + +7. **Validate post-write.** After writes complete, run `scripts/validate.sh` again. Report any remaining issues. Suggest local install test commands: `claude --plugin-dir ./plugins/` and `copilot plugin install ./plugins/`. + +8. **Optional deliverables.** Only when the user explicitly asks: emit cross-tool delta notes (what each plugin needs for Copilot vs Claude Code) and per-plugin README with install commands for both tools. + +## Output format + +Files generated depend on operation: +- **Audit/refactor and adopt:** `plugin.json` (two locations per plugin until verified), `marketplace.json` (`.claude-plugin/`, optionally `.github/plugin/`), migration checklist as a markdown table +- **Maintain/update:** updated `marketplace.json` and affected `plugin.json` files +- **Validate:** report only — no file writes unless explicitly requested after review +- **Optional:** per-plugin `README.md` with both `claude` and `copilot` install commands + + + + + +## Failure handling + +- `scripts/inventory.sh` not found or fails — perform manual asset classification using Read and Bash find; note the fallback in output. +- `scripts/gen_manifests.sh` not found or fails — generate manifest JSON inline; flag that the output was not script-produced. +- `scripts/validate.sh` not found or `claude plugin validate` unavailable — run manual JSON schema and naming checks using `references/claude-code.md`; flag that automated validation was skipped. +- Plugin source unreachable (bad URL, private repo, missing path) — stop the adopt operation, report the error, ask the user to verify the source before retrying. +- Reserved name detected in proposed plugin or marketplace name — halt, report the name and the reserved list from `references/claude-code.md`, ask for a replacement before proceeding. +- Credential-shaped content detected in any manifest field — halt, do not generate the manifest, redirect to environment variable references. + +## Self-check + +- [ ] `references/cross-compat.md` loaded before any tool-specific recommendation was made +- [ ] Operation identified before any scanning or file reading began +- [ ] Gate A presented and explicit approval received before any manifest was generated +- [ ] Gate B presented with full file contents and explicit approval received before any file was written +- [ ] No credential-shaped content in any generated manifest field +- [ ] All plugin names validated as kebab-case and checked against reserved name list +- [ ] `version` field not set in both `plugin.json` and marketplace entry for the same plugin +- [ ] Scripts loaded on demand by step — not preloaded at skill invocation +- [ ] Cross-tool deltas and READMEs produced only if explicitly requested +- [ ] Post-write validation run and findings reported + + diff --git a/.agents/skills/marketplace-architect/references/claude-code.md b/.agents/skills/marketplace-architect/references/claude-code.md new file mode 100644 index 0000000..716b8b4 --- /dev/null +++ b/.agents/skills/marketplace-architect/references/claude-code.md @@ -0,0 +1,169 @@ +# Claude Code Plugin Reference + +Verified against code.claude.com/docs as of June 2026. + +--- + +## Directory structure + +```text +plugin-root/ +├── .claude-plugin/ +│ └── plugin.json # ONLY plugin.json goes here; all other dirs at plugin root +├── skills/ # skill directories: /SKILL.md +├── commands/ # legacy flat .md files; promote to skills/ for new plugins +├── agents/ # agent definitions: .md +├── hooks/ +│ └── hooks.json +├── .mcp.json +├── .lsp.json +├── monitors/ +│ └── monitors.json +├── bin/ # executables added to PATH while plugin is enabled +└── settings.json # default settings applied when plugin is enabled +``` + +A plugin that ships exactly one skill may place `SKILL.md` directly at the plugin root. +Use `skills/` for plugins that may grow beyond one skill. + +--- + +## plugin.json schema + +```json +{ + "name": "my-plugin", // kebab-case, no spaces — also the skill namespace prefix + "displayName": "My Plugin", // human-readable; shown in UI (v2.1.143+) + "description": "What it does", + "version": "1.0.0", // OPTIONAL — omit to use git SHA per commit + "author": { "name": "Name", "url": "https://..." }, + "homepage": "https://...", + "repository": "https://github.com/...", + "license": "MIT", + "keywords": [], + "defaultEnabled": true, // set false to install disabled (v2.1.154+) + "dependencies": [ + { "name": "other-plugin", "version": "~2.1.0" } + ] +} +``` + +Only `name` is required. Add fields only when needed. + +--- + +## marketplace.json schema + +```json +{ + "name": "my-ai-marketplace", + "owner": { "name": "Your Name", "email": "you@example.com" }, + "description": "Description", + "version": "1.0.0", + "plugins": [ + { + "name": "startup-cto", + "source": "./plugins/startup-cto", + "description": "...", + "strict": true + } + ] +} +``` + +`description` and `version` are also accepted under a `metadata` key for backward compatibility. + +--- + +## Plugin source types + +| Type | Format | Notes | +|---|---|---| +| Relative path | `"./plugins/my-plugin"` | Must start with `./`. Resolved from marketplace root. Only works with git-hosted marketplaces, not URL-based. | +| GitHub | `{ "source": "github", "repo": "owner/repo", "ref": "main", "sha": "abc123" }` | sha pins exact commit; ref is branch/tag | +| URL / git | `{ "source": "url", "url": "https://...", "ref": "main" }` | also accepts `owner/repo` shorthand and SSH URLs | +| git-subdir | `{ "source": "git-subdir", "url": "...", "path": "packages/my-plugin" }` | sparse clone of a monorepo path | +| npm | `{ "source": "npm", "package": "@scope/pkg", "version": "^2.0.0", "registry": "https://..." }` | installed via npm install | + +When both `ref` and `sha` are set, `sha` is the effective pin. + +--- + +## Strict mode + +Controls whether `plugin.json` is the authority for component definitions. + +- **`strict: true`** (default) — plugin has its own `plugin.json`; marketplace entry can add extra skills/hooks on top. +- **`strict: false`** — marketplace entry is the entire definition; plugin needs no `plugin.json`. The entry declares `skills`, `agents`, `hooks`, `mcpServers` path arrays. + +Do not use `strict: false` plus a component-declaring `plugin.json` — this is a conflict and fails to load. + +--- + +## Reserved marketplace names + +These names are blocked for third-party use: +`claude-code-marketplace`, `claude-code-plugins`, `claude-plugins-official`, +`claude-plugins-community`, `claude-community`, `anthropic-marketplace`, +`anthropic-plugins`, `agent-skills`, `anthropic-agent-skills`, +`knowledge-work-plugins`, `life-sciences`, `claude-for-legal`, +`claude-for-financial-services`, `financial-services-plugins` + +Names that impersonate official marketplaces are also blocked (e.g. `official-claude-plugins`, +`anthropic-tools-v2`). + +Plugin names must be kebab-case (lowercase, digits, hyphens). Claude.ai marketplace sync +rejects anything else even if the local CLI tolerates it. + +--- + +## Version management + +- If `version` is set in `plugin.json`, users receive updates only when you bump it. +- If `version` is omitted, git commit SHA is used — every commit is a new version. +- If `version` is set in both `plugin.json` and the marketplace entry, `plugin.json` wins silently. +- **Recommendation:** omit `version` unless you need explicit release gates. + +--- + +## Environment variables + +- **`${CLAUDE_PLUGIN_ROOT}`** — absolute path to the plugin's installation directory. Use in hook commands and MCP/LSP configs for all in-plugin file references. This path changes on update. +- **`${CLAUDE_PLUGIN_DATA}`** — persistent directory for plugin state that survives updates. Use for `node_modules`, generated code, caches. + +--- + +## Validation and CLI commands + +```bash +# Validate plugin structure and manifest +claude plugin validate ./my-plugin +claude plugin validate ./my-plugin --strict # treat warnings as errors + +# Install/manage +claude plugin install @ +claude plugin update @ +claude plugin uninstall +claude plugin list +claude plugin enable +claude plugin disable + +# Marketplace +claude plugin marketplace add owner/repo +claude plugin marketplace update + +# Development +claude --plugin-dir ./my-plugin # load without installing +claude --plugin-dir ./my-plugin.zip # load from zip (v2.1.128+) +claude plugin init my-tool # scaffold a skills-dir plugin +``` + +--- + +## Key gotchas + +1. **Plugins are copied to cache on install.** Cannot reference `../shared-utils` — those files are not copied. Duplicate shared files into each plugin or use symlinks. +2. **`commands/` ≠ `skills/`.** Flat `foo.md` is a legacy command; `foo/SKILL.md` is a skill. Promote flat commands to skill directories during migration. +3. **Only `plugin.json` in `.claude-plugin/`.** Skills, agents, hooks, and other directories must be at the plugin root, not inside `.claude-plugin/`. +4. **Plugin names are skill namespace prefixes.** `name: my-plugin` means skills invoke as `/my-plugin:skill-name`. +5. **`defaultEnabled: false` requires v2.1.154+.** Earlier versions ignore it and enable on install. diff --git a/.agents/skills/marketplace-architect/references/copilot-cli.md b/.agents/skills/marketplace-architect/references/copilot-cli.md new file mode 100644 index 0000000..10e2bc4 --- /dev/null +++ b/.agents/skills/marketplace-architect/references/copilot-cli.md @@ -0,0 +1,143 @@ +# GitHub Copilot CLI Plugin Reference + +Verified against docs.github.com as of June 2026. + +--- + +## Directory structure + +```text +plugin-root/ +├── plugin.json # at plugin root (NOT in .claude-plugin/) +├── skills/ # skill directories: /SKILL.md (same as Claude Code) +├── agents/ # agent files: .agent.md (differs from Claude Code) +├── hooks.json # at plugin root (differs from Claude Code: hooks/hooks.json) +└── .mcp.json # at plugin root (same as Claude Code) +``` + +--- + +## plugin.json schema (Copilot) + +```json +{ + "name": "my-plugin", + "description": "What it does", + "version": "1.0.0", + "author": { "name": "Name", "email": "you@example.com" }, + "license": "MIT", + "keywords": [], + "agents": "agents/", + "skills": ["skills/"], + "hooks": "hooks.json", + "mcpServers": ".mcp.json" +} +``` + +Key difference from Claude Code: Copilot expects component path declarations inside +`plugin.json` (`"skills": "skills/"`, `"agents": "agents/"`, etc.). Claude Code instead +defaults to standard dirs and takes path overrides only via the marketplace entry. +This means the same `plugin.json` may need these fields for Copilot but not for Claude. + +--- + +## marketplace.json schema (Copilot) + +```json +{ + "name": "my-ai-marketplace", + "owner": { "name": "Your Name", "email": "you@example.com" }, + "metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" }, + "plugins": [ + { + "name": "startup-cto", + "source": "./plugins/startup-cto", + "description": "...", + "version": "1.0.0" + } + ] +} +``` + +Copilot's primary marketplace manifest path is `.github/plugin/marketplace.json`. +It also reads `.claude-plugin/marketplace.json` as a fallback. +Relative `source` paths: `./x` and `x` are both valid (Claude requires `./`). + +--- + +## Agent file format + +Copilot agents use `.agent.md` extension with frontmatter: + +```markdown +--- +name: my-agent +description: What this agent does +tools: + - read_file + - run_command +--- + +Agent instructions here. +``` + +Claude Code agents use `.md` extension without the `.agent.md` suffix. +If shipping agents for both tools, create both files: +- `agents/my-agent.md` — Claude Code +- `agents/my-agent.agent.md` — Copilot CLI + +--- + +## CLI commands + +```bash +# Install plugin locally (development) +copilot plugin install ./my-plugin + +# List installed plugins +copilot plugin list + +# In interactive mode +/plugin list +/skills list +/agent + +# Reload after changes +/reload-plugins + +# Uninstall (uses bare plugin name, not @marketplace form) +copilot plugin uninstall + +# Marketplace +copilot plugin marketplace add owner/repo +``` + +> ⚠️ **UNVERIFIED: Copilot marketplace install command.** +> The `update`/`uninstall` commands take a bare ``. Whether install from a marketplace +> uses `@` (Claude Code's form) or a bare `` is not confirmed in docs. +> Run `copilot plugin install --help` before documenting the install command anywhere. + +--- + +## Validation + +Copilot has no documented `plugin validate` command. For Copilot-side validation, run manual checks: +- Valid JSON in `plugin.json` and `marketplace.json` +- Required fields: `name`, `description` +- Unique plugin names across marketplace +- Kebab-case plugin names +- All `source` paths resolve to existing directories +- `.agent.md` files have valid YAML frontmatter with `name`, `description`, `tools` + +--- + +## Key differences from Claude Code (summary) + +| What | Claude Code | Copilot CLI | +|---|---|---| +| Plugin manifest location | `.claude-plugin/plugin.json` | `plugin.json` at plugin root | +| Agent files | `agents/.md` | `agents/.agent.md` | +| Hooks file | `hooks/hooks.json` | `hooks.json` at plugin root | +| Component paths | Declared in marketplace entry | Declared in `plugin.json` | +| Validate command | `claude plugin validate` | None — manual checks only | +| Relative source `./` | Required | Optional (`x` also valid) | diff --git a/.agents/skills/marketplace-architect/references/cross-compat.md b/.agents/skills/marketplace-architect/references/cross-compat.md new file mode 100644 index 0000000..82ae592 --- /dev/null +++ b/.agents/skills/marketplace-architect/references/cross-compat.md @@ -0,0 +1,79 @@ +# Cross-Tool Compatibility Reference + +Claude Code and GitHub Copilot CLI share the plugin concept but diverge in specific, breaking +ways. **Skills are the portable core. Manifests and agents are where they split.** + +Make Claude Code the source of truth — it is the stricter, more fully specified format. +Treat "loads in Copilot CLI" as a tested checklist item per plugin, not an assumption. + +--- + +## Divergence table + +| Concern | Claude Code | GitHub Copilot CLI | Portable choice | +|---|---|---|---| +| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); also reads `.claude-plugin/` | Put it in `.claude-plugin/` — both read it. Optionally mirror to `.github/plugin/`. | +| Plugin manifest path | `.claude-plugin/plugin.json` (required; only `plugin.json` goes in this dir) | `plugin.json` at **plugin root** | Ship in both locations until verified — see ⚠️ below | +| Skills | `skills//SKILL.md` | `skills//SKILL.md` | ✅ Identical | +| Agents | `agents/.md` | `agents/.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed | +| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe | +| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same | +| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both | +| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Run Claude validator + manual JSON checks for Copilot | +| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape | +| Install plugin | `claude plugin install @` | install by plugin name; `@marketplace` suffix unconfirmed | ⚠️ Verify Copilot install string before documenting | +| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific | +| Component paths in plugin.json | Claude defaults to standard dirs; path overrides via marketplace entry only | `"skills": "skills/"`, `"agents": "agents/"`, etc. in plugin.json | Generate per-tool manifests rather than one shared file | + +> ⚠️ **UNVERIFIED — test before committing to a layout.** +> Copilot docs confirm it reads the **marketplace** manifest from `.claude-plugin/`. They do NOT +> confirm the same fallback for a plugin's `plugin.json`. Copilot docs show `plugin.json` at plugin +> root; Claude requires it in `.claude-plugin/`. Until verified: ship `plugin.json` in BOTH +> `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical content), then +> drop whichever proves redundant. + +--- + +## `@` resolution + +`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's +top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name. +Keep them aligned to avoid confusion, but they are separate fields. + +--- + +## Canonical cross-compatible repo layout + +```text +repo-root/ +├── .claude-plugin/ +│ └── marketplace.json # both tools read here +├── .github/plugin/ +│ └── marketplace.json # OPTIONAL: Copilot canonical path (mirror) +├── plugins/ +│ └── startup-cto/ +│ ├── plugin.json # Copilot root manifest ┐ ship both until +│ ├── .claude-plugin/ # │ the note above is +│ │ └── plugin.json # Claude manifest ┘ verified +│ ├── skills/ +│ │ ├── fundraising/SKILL.md +│ │ └── hiring/SKILL.md +│ ├── agents/ +│ │ ├── startup-cto.md # Claude +│ │ └── startup-cto.agent.md # Copilot (only if shipping native agents) +│ ├── hooks/hooks.json # Claude +│ ├── hooks.json # Copilot (if hooks used) +│ └── README.md +└── README.md +``` + +--- + +## Open questions to resolve before generating layouts + +1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?** + Install a test plugin both ways. The answer decides whether to ship one manifest or two. + +2. **What is Copilot's exact install-from-marketplace command?** + Run `copilot plugin install --help`. The `update`/`uninstall` commands take a bare plugin + name — the `@marketplace` form may not apply. diff --git a/.agents/skills/marketplace-architect/scripts/gen_manifests.sh b/.agents/skills/marketplace-architect/scripts/gen_manifests.sh new file mode 100755 index 0000000..7f911b9 --- /dev/null +++ b/.agents/skills/marketplace-architect/scripts/gen_manifests.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# Generate plugin.json (in both locations) and marketplace.json. +# Dry-run by default; pass --write to apply. +# +# Usage: +# gen_manifests.sh --marketplace-name [options] +# +# Options: +# --marketplace-name kebab-case marketplace identifier (required) +# --author "Name " author string (default: "Unknown ") +# --plugins-dir subdir containing plugin folders (default: plugins) +# --mirror-github also write to .github/plugin/marketplace.json +# --write apply changes (default is dry run) + +set -euo pipefail + +RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official + claude-plugins-community claude-community anthropic-marketplace anthropic-plugins + agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences + claude-for-legal claude-for-financial-services financial-services-plugins" + +RESERVED_PATTERNS="official-claude anthropic-tools claude-official" + +is_kebab_case() { + [[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]] +} + +is_reserved() { + local name="$1" + for n in $RESERVED_NAMES; do + [[ "$name" == "$n" ]] && return 0 + done + for p in $RESERVED_PATTERNS; do + [[ "$name" == "$p"* ]] && return 0 + done + return 1 +} + +validate_name() { + local name="$1" context="$2" + local ok=true + if ! is_kebab_case "$name"; then + echo "ERROR: $context: name '$name' is not kebab-case (lowercase, digits, hyphens only)." >&2 + ok=false + fi + if is_reserved "$name"; then + echo "ERROR: $context: name '$name' is reserved for official Anthropic use." >&2 + ok=false + fi + [[ "$ok" == "true" ]] +} + +write_json() { + local path="$1" content="$2" dry_run="$3" + if [[ "$dry_run" == "true" ]]; then + echo "" + echo "--- $path (dry run) ---" + echo "$content" + else + mkdir -p "$(dirname "$path")" + echo "$content" > "$path" + echo " Written: $path" + fi +} + +# ── parse args ─────────────────────────────────────────────────────────────── + +ROOT="" +MARKETPLACE_NAME="" +AUTHOR="Unknown " +PLUGINS_DIR="plugins" +MIRROR_GITHUB=false +WRITE=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --marketplace-name) MARKETPLACE_NAME="$2"; shift 2 ;; + --author) AUTHOR="$2"; shift 2 ;; + --plugins-dir) PLUGINS_DIR="$2"; shift 2 ;; + --mirror-github) MIRROR_GITHUB=true; shift ;; + --write) WRITE=true; shift ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) ROOT="$1"; shift ;; + esac +done + +if [[ -z "$ROOT" || -z "$MARKETPLACE_NAME" ]]; then + echo "Usage: gen_manifests.sh --marketplace-name [--write]" >&2 + exit 1 +fi + +ROOT="$(cd "$ROOT" && pwd)" +DRY_RUN=$( [[ "$WRITE" == "true" ]] && echo "false" || echo "true" ) + +[[ "$DRY_RUN" == "true" ]] && echo "DRY RUN — pass --write to apply changes" + +# Parse author +AUTHOR_NAME="${AUTHOR%% <*}" +AUTHOR_EMAIL="" +if [[ "$AUTHOR" =~ \<(.+)\> ]]; then + AUTHOR_EMAIL="${BASH_REMATCH[1]}" +fi + +# Validate marketplace name +validate_name "$MARKETPLACE_NAME" "marketplace" || exit 1 + +# Discover plugins +PLUGINS_PATH="$ROOT/$PLUGINS_DIR" +if [[ ! -d "$PLUGINS_PATH" ]]; then + echo "No plugins directory found at $PLUGINS_PATH" >&2 + exit 1 +fi + +mapfile -t PLUGIN_DIRS < <(find "$PLUGINS_PATH" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort) + +if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then + echo "No plugin directories found in $PLUGINS_PATH" >&2 + exit 1 +fi + +echo "Found ${#PLUGIN_DIRS[@]} plugin(s)" + +# Build plugins array for marketplace.json +PLUGINS_JSON="[]" + +for pd in "${PLUGIN_DIRS[@]}"; do + pname="$(basename "$pd")" + validate_name "$pname" "plugin '$pname'" || exit 1 + + # Read existing plugin.json if present + existing_claude="$pd/.claude-plugin/plugin.json" + existing_root="$pd/plugin.json" + existing_desc="Plugin: $pname" + existing_name="$pname" + + for existing in "$existing_claude" "$existing_root"; do + if [[ -f "$existing" ]] && jq -e . "$existing" >/dev/null 2>&1; then + d=$(jq -r '.description // empty' "$existing") + n=$(jq -r '.name // empty' "$existing") + [[ -n "$d" ]] && existing_desc="$d" + [[ -n "$n" ]] && existing_name="$n" + break + fi + done + + # Warn on version + for existing in "$existing_claude" "$existing_root"; do + if [[ -f "$existing" ]] && jq -e '.version' "$existing" >/dev/null 2>&1; then + echo " WARNING: plugin '$pname' sets version in plugin.json. Do not also set it in the marketplace entry — plugin.json wins silently." + break + fi + done + + # Build plugin.json + plugin_json=$(jq -n \ + --arg name "$existing_name" \ + --arg desc "$existing_desc" \ + --arg aname "$AUTHOR_NAME" \ + --arg aemail "$AUTHOR_EMAIL" \ + '{name: $name, description: $desc, author: {name: $aname, email: $aemail}}') + + write_json "$pd/plugin.json" "$plugin_json" "$DRY_RUN" + write_json "$pd/.claude-plugin/plugin.json" "$plugin_json" "$DRY_RUN" + + source="./$PLUGINS_DIR/$pname" + PLUGINS_JSON=$(echo "$PLUGINS_JSON" | jq \ + --arg name "$existing_name" \ + --arg src "$source" \ + --arg desc "$existing_desc" \ + '. + [{name: $name, source: $src, description: $desc}]') +done + +# Build marketplace.json +marketplace_json=$(jq -n \ + --arg name "$MARKETPLACE_NAME" \ + --arg aname "$AUTHOR_NAME" \ + --arg aemail "$AUTHOR_EMAIL" \ + --arg desc "$MARKETPLACE_NAME plugin marketplace" \ + --argjson plugins "$PLUGINS_JSON" \ + '{name: $name, owner: {name: $aname, email: $aemail}, description: $desc, plugins: $plugins}') + +write_json "$ROOT/.claude-plugin/marketplace.json" "$marketplace_json" "$DRY_RUN" + +if [[ "$MIRROR_GITHUB" == "true" ]]; then + write_json "$ROOT/.github/plugin/marketplace.json" "$marketplace_json" "$DRY_RUN" +fi + +if [[ "$DRY_RUN" == "true" ]]; then + echo "" + echo "--- End dry run. Pass --write to apply. ---" +else + echo "" + echo "Done. Run scripts/validate.sh to verify." +fi diff --git a/.agents/skills/marketplace-architect/scripts/inventory.sh b/.agents/skills/marketplace-architect/scripts/inventory.sh new file mode 100755 index 0000000..bcaa69a --- /dev/null +++ b/.agents/skills/marketplace-architect/scripts/inventory.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Scan a repository and classify every asset as skill/command/agent/hook/prompt/MCP. +# Outputs a markdown table of findings plus a list of cross-reference warnings. +# +# Usage: inventory.sh + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "Usage: inventory.sh " >&2 + exit 1 +fi + +ROOT="$(cd "$1" && pwd)" + +if [[ ! -d "$ROOT" ]]; then + echo "Error: $ROOT is not a directory" >&2 + exit 1 +fi + +# ── classify assets ────────────────────────────────────────────────────────── + +declare -a ROWS=() +declare -a CROSS_REFS=() + +while IFS= read -r -d '' path; do + rel="${path#"$ROOT/"}" + name="$(basename "$path")" + dir="$(dirname "$rel")" + parent="$(basename "$dir")" + + # Skip hidden dirs except .claude-plugin and .github + skip=false + IFS='/' read -ra parts <<< "$dir" + for part in "${parts[@]}"; do + if [[ "$part" == .* && "$part" != ".claude-plugin" && "$part" != ".github" ]]; then + skip=true; break + fi + done + $skip && continue + + asset_type="" + + case "$name" in + SKILL.md) asset_type="skill" ;; + hooks.json) asset_type="hook" ;; + .mcp.json) asset_type="mcp" ;; + .lsp.json) asset_type="lsp" ;; + plugin.json) asset_type="manifest-plugin" ;; + marketplace.json) asset_type="manifest-marketplace" ;; + *.agent.md) asset_type="agent-copilot" ;; + *.md) + if [[ "$parent" == "agents" ]]; then + asset_type="agent-claude" + elif [[ "$parent" == "commands" ]]; then + asset_type="command" + elif [[ "$rel" != *"/skills/"* && "$rel" != *"/commands/"* && "$rel" != *"/agents/"* ]]; then + asset_type="prompt" + fi + ;; + esac + + [[ -n "$asset_type" ]] && ROWS+=("$asset_type|$rel") + + # Check for cross-references in text files + case "$name" in *.md|*.json|*.sh) + if grep -q '\.\.\/' "$path" 2>/dev/null; then + while IFS= read -r line; do + lineno="${line%%:*}" + content="${line#*:}" + CROSS_REFS+=("$rel:$lineno: $content") + done < <(grep -n '\.\.\/' "$path" 2>/dev/null | head -20) + fi + ;; + esac + +done < <(find "$ROOT" -type f -print0 | sort -z) + +# ── report ─────────────────────────────────────────────────────────────────── + +echo "# Asset Inventory: $ROOT" +echo "" +echo "## Assets" +echo "" +echo "| Type | Path |" +echo "|---|---|" +for row in "${ROWS[@]+"${ROWS[@]}"}"; do + type="${row%%|*}" + path="${row#*|}" + echo "| \`$type\` | \`$path\` |" +done | sort + +total="${#ROWS[@]}" +echo "" +echo "**Total: $total assets**" +echo "" + +# Summary by type +echo "## Summary by type" +echo "" +for row in "${ROWS[@]+"${ROWS[@]}"}"; do + echo "${row%%|*}" +done | sort | uniq -c | while read -r count type; do + echo "- \`$type\`: $count" +done + +# Cross-reference warnings +echo "" +if [[ ${#CROSS_REFS[@]} -gt 0 ]]; then + echo "## ⚠️ Cross-reference warnings (${#CROSS_REFS[@]} found)" + echo "" + echo "These \`../\` references will break after install-time caching:" + echo "" + for ref in "${CROSS_REFS[@]}"; do + echo "- \`$ref\`" + done +else + echo "## Cross-references" + echo "" + echo "No \`../\` cross-references found. Safe to proceed with plugin boundaries." +fi diff --git a/.agents/skills/marketplace-architect/scripts/validate.sh b/.agents/skills/marketplace-architect/scripts/validate.sh new file mode 100755 index 0000000..b844dff --- /dev/null +++ b/.agents/skills/marketplace-architect/scripts/validate.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# Validate plugin marketplace manifests for Claude Code and GitHub Copilot CLI. +# Wraps `claude plugin validate` (Claude-side) and runs manual checks (Copilot-side). +# +# Usage: +# validate.sh +# validate.sh --plugin plugins/my-plugin + +set -euo pipefail + +RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official + claude-plugins-community claude-community anthropic-marketplace anthropic-plugins + agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences + claude-for-legal claude-for-financial-services financial-services-plugins" + +RESERVED_PATTERNS="official-claude anthropic-tools claude-official" + +ERRORS=0 +WARNINGS=0 + +error() { echo "ERROR: $1"; ((ERRORS++)) || true; } +warn() { echo "WARN: $1"; ((WARNINGS++)) || true; } + +is_kebab_case() { [[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; } + +is_reserved() { + local name="$1" + for n in $RESERVED_NAMES; do [[ "$name" == "$n" ]] && return 0; done + for p in $RESERVED_PATTERNS; do [[ "$name" == "$p"* ]] && return 0; done + return 1 +} + +validate_name() { + local name="$1" context="$2" + [[ -z "$name" ]] && { error "$context: name is missing or empty"; return 0; } + is_kebab_case "$name" || error "$context: name '$name' is not kebab-case" + if is_reserved "$name"; then + error "$context: name '$name' is reserved for official Anthropic use" + fi + return 0 +} + +valid_json() { + local path="$1" + if ! jq -e . "$path" >/dev/null 2>&1; then + error "Invalid JSON in $path" + return 1 + fi + return 0 +} + +validate_marketplace_json() { + local path="$1" + [[ -f "$path" ]] || return 0 + valid_json "$path" || return 0 + + local name + name=$(jq -r '.name // empty' "$path") + [[ -z "$name" ]] && error "$path: 'name' field is required" || validate_name "$name" "$path" + + local plugins_type + plugins_type=$(jq -r 'if .plugins | type == "array" then "ok" else "bad" end' "$path") + if [[ "$plugins_type" != "ok" ]]; then + error "$path: 'plugins' must be an array" + return 0 + fi + + # Check each plugin entry + local seen_names=() + while IFS= read -r pname; do + # Duplicate check + for seen in "${seen_names[@]+"${seen_names[@]}"}"; do + if [[ "$seen" == "$pname" ]]; then error "$path: duplicate plugin name '$pname'"; fi + done + seen_names+=("$pname") + validate_name "$pname" "$path plugin '$pname'" + + # Source path check + local src + src=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$path") + if [[ -n "$src" && "$src" != ./* && "$src" != "github" && "$src" != "npm" && "$src" != "url" && "$src" != "git-subdir" ]]; then + warn "$path plugin '$pname': relative source '$src' should start with './' for Claude Code compatibility" + fi + + # Version duplication warning + local has_ver + has_ver=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .version // empty' "$path") + if [[ -n "$has_ver" ]]; then + warn "$path plugin '$pname': version set in marketplace entry. If also set in plugin.json, plugin.json wins silently." + fi + + done < <(jq -r '.plugins[].name // empty' "$path") + return 0 +} + +validate_plugin_json() { + local path="$1" marketplace_json="${2:-}" + [[ -f "$path" ]] || return 0 + valid_json "$path" || return 0 + + local name + name=$(jq -r '.name // empty' "$path") + if [[ -z "$name" ]]; then + warn "$path: 'name' field missing (plugin dir name will be used)" + else + validate_name "$name" "$path" + + # Version duplication check + if [[ -n "$marketplace_json" && -f "$marketplace_json" ]]; then + local pver mver + pver=$(jq -r '.version // empty' "$path") + mver=$(jq -r --arg n "$name" '.plugins[]? | select(.name==$n) | .version // empty' "$marketplace_json") + if [[ -n "$pver" && -n "$mver" ]]; then + error "$path: version '$pver' set in both plugin.json and marketplace entry — plugin.json wins silently. Remove one." + fi + fi + fi + return 0 +} + +validate_skill_md() { + local path="$1" + local content + content=$(cat "$path") + if [[ "$content" != ---* ]]; then + warn "$path: SKILL.md has no YAML frontmatter" + return + fi + if ! echo "$content" | awk 'NR>1 && /^---/' | grep -q '^---'; then + error "$path: SKILL.md frontmatter not closed" + return + fi + if ! echo "$content" | awk '/^---/{n++; if(n==2) exit} n==1' | grep -q 'description:'; then + warn "$path: SKILL.md frontmatter missing 'description' field" + fi +} + +validate_plugin_dir() { + local pd="$1" marketplace_json="${2:-}" + + local claude_manifest="$pd/.claude-plugin/plugin.json" + local root_manifest="$pd/plugin.json" + + if [[ ! -f "$claude_manifest" && ! -f "$root_manifest" ]]; then + warn "$pd: no plugin.json found (will auto-discover components)" + else + validate_plugin_json "$claude_manifest" "$marketplace_json" + validate_plugin_json "$root_manifest" "$marketplace_json" + + # Sync check + if [[ -f "$claude_manifest" && -f "$root_manifest" ]]; then + if ! diff <(jq -S . "$claude_manifest") <(jq -S . "$root_manifest") >/dev/null 2>&1; then + warn "$pd: .claude-plugin/plugin.json and plugin.json differ — keep them in sync" + fi + fi + fi + + # Components must not be inside .claude-plugin/ + for bad_dir in skills agents hooks commands; do + if [[ -d "$pd/.claude-plugin/$bad_dir" ]]; then + error "$pd/.claude-plugin/$bad_dir: only plugin.json belongs in .claude-plugin/; move $bad_dir/ to plugin root" + fi + done + + # Validate SKILL.md files + while IFS= read -r -d '' skill_md; do + validate_skill_md "$skill_md" + done < <(find "$pd" -name "SKILL.md" -print0 2>/dev/null) + + # Cross-reference check + while IFS= read -r -d '' f; do + if grep -q '\.\.\/' "$f" 2>/dev/null; then + local rel="${f#"$pd/"}" + error "$rel: contains '../' reference — plugins cannot access files outside their directory after caching" + fi + done < <(find "$pd" \( -name "*.md" -o -name "*.json" \) -print0 2>/dev/null) +} + +run_claude_validate() { + local path="$1" + if command -v claude >/dev/null 2>&1; then + if ! claude plugin validate "$path" 2>&1; then + error "claude plugin validate failed for $path" + fi + else + warn "'claude' CLI not found — skipping claude plugin validate" + fi +} + +# ── parse args ──────────────────────────────────────────────────────────────── + +ROOT="" +PLUGIN_ONLY="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --plugin) PLUGIN_ONLY="$2"; shift 2 ;; + -*) echo "Unknown option: $1" >&2; exit 1 ;; + *) ROOT="$1"; shift ;; + esac +done + +if [[ -z "$ROOT" ]]; then + echo "Usage: validate.sh [--plugin ]" >&2 + exit 1 +fi + +ROOT="$(cd "$ROOT" && pwd)" + +# ── validate marketplace.json ───────────────────────────────────────────────── + +MARKETPLACE_JSON="" +for mp in "$ROOT/.claude-plugin/marketplace.json" "$ROOT/.github/plugin/marketplace.json"; do + if [[ -f "$mp" ]]; then + MARKETPLACE_JSON="$mp" + validate_marketplace_json "$mp" + break + fi +done + +if [[ -z "$MARKETPLACE_JSON" ]]; then + warn "No marketplace.json found. Expected at .claude-plugin/marketplace.json" +fi + +# ── validate plugins ────────────────────────────────────────────────────────── + +if [[ -n "$PLUGIN_ONLY" ]]; then + validate_plugin_dir "$(cd "$PLUGIN_ONLY" && pwd)" "$MARKETPLACE_JSON" + run_claude_validate "$(cd "$PLUGIN_ONLY" && pwd)" +else + plugins_path="$ROOT/plugins" + if [[ -d "$plugins_path" ]]; then + while IFS= read -r -d '' pd; do + validate_plugin_dir "$pd" "$MARKETPLACE_JSON" + run_claude_validate "$pd" + done < <(find "$plugins_path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -print0 | sort -z) + else + warn "No plugins/ directory found at $ROOT" + fi +fi + +# ── check source paths resolve ──────────────────────────────────────────────── + +if [[ -n "$MARKETPLACE_JSON" ]]; then + while IFS= read -r src; do + [[ "$src" != ./* ]] && continue + src_path="$ROOT/${src#./}" + if [[ ! -d "$src_path" ]]; then error "Marketplace source path '$src' does not exist at $src_path"; fi + done < <(jq -r '.plugins[]?.source | strings' "$MARKETPLACE_JSON" 2>/dev/null) +fi + +# ── report ──────────────────────────────────────────────────────────────────── + +echo "" +if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then + echo "✓ All checks passed." + exit 0 +elif [[ $ERRORS -eq 0 ]]; then + echo "Passed with $WARNINGS warning(s)." + exit 0 +else + echo "Failed. Fix $ERRORS error(s) before proceeding." + exit 1 +fi diff --git a/.agents/skills/marketplace-architect/tests/test_scripts.sh b/.agents/skills/marketplace-architect/tests/test_scripts.sh new file mode 100755 index 0000000..1045143 --- /dev/null +++ b/.agents/skills/marketplace-architect/tests/test_scripts.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPTS_DIR="$(cd "$(dirname "$0")/../scripts" && pwd)" +PASS=0; FAIL=0 +# Use += to avoid ((var++)) returning 0 exit code when var was 0 under set -e + +# ── helpers ───────────────────────────────────────────────────────────────── + +tmpdir() { mktemp -d; } + +assert_contains() { + local label="$1" expected="$2" actual="$3" + if echo "$actual" | grep -qF "$expected"; then + echo " PASS: $label" + PASS=$((PASS+1)) + else + echo " FAIL: $label" + echo " expected to contain: $expected" + echo " got: $(echo "$actual" | head -5)" + FAIL=$((FAIL+1)) + fi +} + +assert_not_contains() { + local label="$1" unexpected="$2" actual="$3" + if echo "$actual" | grep -qF "$unexpected"; then + echo " FAIL: $label" + echo " expected NOT to contain: $unexpected" + FAIL=$((FAIL+1)) + else + echo " PASS: $label" + PASS=$((PASS+1)) + fi +} + +assert_exit() { + local label="$1" expected="$2" actual="$3" + if [[ "$actual" -eq "$expected" ]]; then + echo " PASS: $label" + PASS=$((PASS+1)) + else + echo " FAIL: $label" + echo " expected exit $expected, got $actual" + FAIL=$((FAIL+1)) + fi +} + +assert_file_exists() { + local label="$1" path="$2" + if [[ -f "$path" ]]; then + echo " PASS: $label" + PASS=$((PASS+1)) + else + echo " FAIL: $label" + echo " file not found: $path" + FAIL=$((FAIL+1)) + fi +} + +assert_file_absent() { + local label="$1" path="$2" + if [[ ! -e "$path" ]]; then + echo " PASS: $label" + PASS=$((PASS+1)) + else + echo " FAIL: $label" + echo " file should not exist: $path" + FAIL=$((FAIL+1)) + fi +} + +# ── inventory.sh tests ─────────────────────────────────────────────────────── + +echo "=== inventory.sh ===" + +# 1. SKILL.md classified as skill +t=$(tmpdir) +mkdir -p "$t/skills/my-skill" +echo "---" > "$t/skills/my-skill/SKILL.md" +out=$("$SCRIPTS_DIR/inventory.sh" "$t") +assert_contains "SKILL.md classified as skill" "skill" "$out" +rm -rf "$t" + +# 2. agents/foo.agent.md classified as agent-copilot +t=$(tmpdir) +mkdir -p "$t/agents" +touch "$t/agents/my-agent.agent.md" +out=$("$SCRIPTS_DIR/inventory.sh" "$t") +assert_contains "agents/*.agent.md classified as agent-copilot" "agent-copilot" "$out" +rm -rf "$t" + +# 3. ../ in file content produces cross-reference warning +t=$(tmpdir) +mkdir -p "$t/skills/my-skill" +printf -- '---\ndescription: test\n---\nSee ../shared/file.md\n' > "$t/skills/my-skill/SKILL.md" +out=$("$SCRIPTS_DIR/inventory.sh" "$t") +assert_contains "../ cross-reference warning emitted" "Cross-reference" "$out" +assert_contains "../ path shown in warning" "../shared/file.md" "$out" +rm -rf "$t" + +# ── gen_manifests.sh tests ─────────────────────────────────────────────────── + +echo "" +echo "=== gen_manifests.sh ===" + +# 4. Without --write, no files are created +t=$(tmpdir) +mkdir -p "$t/plugins/my-plugin/skills/hello" +echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md" +"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace >/dev/null +assert_file_absent "dry run: .claude-plugin/marketplace.json not written" "$t/.claude-plugin/marketplace.json" +assert_file_absent "dry run: plugin.json not written" "$t/plugins/my-plugin/plugin.json" +rm -rf "$t" + +# 5. With --write, creates plugin.json in both locations +t=$(tmpdir) +mkdir -p "$t/plugins/my-plugin/skills/hello" +echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md" +"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace --write >/dev/null +assert_file_exists "--write: plugin root plugin.json created" "$t/plugins/my-plugin/plugin.json" +assert_file_exists "--write: .claude-plugin/plugin.json created" "$t/plugins/my-plugin/.claude-plugin/plugin.json" +rm -rf "$t" + +# 6. Reserved name exits 1 +t=$(tmpdir) +mkdir -p "$t/plugins/my-plugin" +set +e +out=$("$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name claude-plugins-official 2>&1) +code=$? +set -e +assert_exit "reserved name: exit 1" 1 "$code" +assert_contains "reserved name: error message" "reserved" "$out" +rm -rf "$t" + +# ── validate.sh tests ──────────────────────────────────────────────────────── + +echo "" +echo "=== validate.sh ===" + +# 7. Invalid JSON exits 1 +t=$(tmpdir) +mkdir -p "$t/.claude-plugin" +echo "not json" > "$t/.claude-plugin/marketplace.json" +set +e +out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1) +code=$? +set -e +assert_exit "invalid JSON: exit 1" 1 "$code" +assert_contains "invalid JSON: error message" "ERROR" "$out" +rm -rf "$t" + +# 8. Reserved marketplace name exits 1 +t=$(tmpdir) +mkdir -p "$t/.claude-plugin" +printf '{"name":"claude-plugins-official","plugins":[]}\n' > "$t/.claude-plugin/marketplace.json" +set +e +out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1) +code=$? +set -e +assert_exit "reserved marketplace name: exit 1" 1 "$code" +assert_contains "reserved marketplace name: error message" "reserved" "$out" +rm -rf "$t" + +# 9. ../ in plugin file exits 1 +t=$(tmpdir) +mkdir -p "$t/.claude-plugin" "$t/plugins/my-plugin/skills/hello" +printf '{"name":"my-marketplace","plugins":[{"name":"my-plugin","source":"./plugins/my-plugin"}]}\n' \ + > "$t/.claude-plugin/marketplace.json" +printf -- '---\ndescription: test\n---\nSee ../shared.md\n' \ + > "$t/plugins/my-plugin/skills/hello/SKILL.md" +set +e +out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1) +code=$? +set -e +assert_exit "..// in plugin: exit 1" 1 "$code" +assert_contains "..// in plugin: error message" "../" "$out" +rm -rf "$t" + +# 10. No marketplace.json warns but exits 0 +t=$(tmpdir) +set +e +out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1) +code=$? +set -e +assert_exit "no marketplace.json: exit 0" 0 "$code" +assert_contains "no marketplace.json: warning emitted" "WARN" "$out" +rm -rf "$t" + +# ── summary ────────────────────────────────────────────────────────────────── + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[[ $FAIL -eq 0 ]] diff --git a/.agents/skills/write-skill/CATEGORIES.md b/.agents/skills/write-skill/CATEGORIES.md index aa9339b..2efefdd 100644 --- a/.agents/skills/write-skill/CATEGORIES.md +++ b/.agents/skills/write-skill/CATEGORIES.md @@ -12,4 +12,5 @@ | `iac` | write-ansible-role, write-terraform-module, write-k8s-manifest, write-docker-compose, proxmox-vm-spec, iac-security-review, write-molecule-test | | `cross-cutting` | zoom-out, caveman, session-handoff, governance-check, git-guardrails, git-commit-message | | `factory` | write-skill, write-adr, write-workflow, write-eval, validate-skill, upgrade-skill, write-issue-spec | +| `marketplace` | marketplace-architect — plugin and skill distribution tooling for Claude Code / GitHub Copilot CLI | | `roles` | architect, developer, reviewer, security, qa, ops — Chunk 5 | diff --git a/docs/research/plugin-marketplace-architecture.md b/docs/research/plugin-marketplace-architecture.md new file mode 100644 index 0000000..95b130a --- /dev/null +++ b/docs/research/plugin-marketplace-architecture.md @@ -0,0 +1,334 @@ +# Plugin Marketplace Architecture + +Reference for refactoring this repo of skills/agents/hooks/prompts into a single Git-based +plugin marketplace installable by **Claude Code** and **GitHub Copilot CLI**, and for building +the `marketplace-architect` skill that automates the migration. + +> **Provenance & staleness:** Verified against the official Claude Code plugin docs +> (`code.claude.com/docs`) and GitHub Copilot CLI plugin docs (`docs.github.com`) as of +> **June 2026**. Both ecosystems are moving fast; re-verify the divergence table before a +> big migration. One item below (Copilot reading `.claude-plugin/plugin.json` per-plugin) is +> **explicitly unverified** — see the flagged note. Don't treat that part as settled. + +--- + +## 1. Core model (this part is correct and stable) + +- The **Git repository is the marketplace.** No backend, registry API, database, SaaS, or MCP + server is required. A marketplace is just a manifest file that lists plugins and where to find them. +- A **plugin is the deployable unit.** Each plugin bundles one or more of: skills, agents, hooks, + prompts/commands (flat `.md`), MCP servers, and — Claude Code only — output styles, LSP servers, + background monitors, a `bin/` on PATH, and default `settings.json`. "Workflows" from the handoff + aren't a distinct file type; express them as a skill that orchestrates steps, or a command. +- **Organize by user outcome, not file type.** `startup-cto/` and `security-reviewer/`, not + `all-skills/` and `all-agents/`. +- **Aim for ~10–20 opinionated plugins**, not 50 tiny ones. (This is a usability judgment, not a + hard rule from either vendor — but it's sound. Many skills can live inside one plugin.) The + handoff's suggested set, as a starting shape: `startup-cto`, `system-architect`, + `security-reviewer`, `product-manager`, `growth-marketer`, `developer-relations`, + `technical-writer`, `research-analyst`. + +Everything below is where the original handoff was either wrong or incomplete. + +--- + +## 2. The two tools are convergent, NOT identical + +This is the single most important correction. The handoff assumed "write once, run both." +In reality the formats overlap heavily but diverge in specific, breaking ways. **Skills are the +portable core; manifests and agents are where they split.** + +**Recommended stance:** make **Claude Code the source of truth** (it's the stricter, more +fully-specified format) and treat "loads in Copilot CLI" as a **tested checklist item per plugin**, +not an assumption. Encode the Copilot deltas (manifest location, `.agent.md` naming) explicitly in +the architect skill rather than pretending the two are the same. This is more honest than "write +once, run both" and stops surprises at install time. + +### Divergence table (ground truth) + +| Concern | Claude Code | GitHub Copilot CLI | Portable choice | +|---|---|---|---| +| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); **also reads `.claude-plugin/`** | Put it in `.claude-plugin/` — both read it. Optionally also `.github/plugin/`. | +| Plugin manifest path | `.claude-plugin/plugin.json` (required; **only** plugin.json goes in this dir) | `plugin.json` at **plugin root** | ⚠️ See flagged note — may need it in **both** locations | +| Skills | `skills//SKILL.md` | `skills//SKILL.md` | ✅ Identical — lean on these | +| Agents | `agents/.md` | `agents/.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed | +| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe | +| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same | +| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both | +| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Claude validator + custom JSON checks for Copilot | +| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape | +| Install plugin | `claude plugin install @` | from a registered marketplace by plugin name; **`@marketplace` suffix not confirmed** — `update`/`uninstall` take a bare ``, so don't assume Claude's `@marketplace` form | ⚠️ Verify the Copilot install string before documenting it | +| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific | + +> ⚠️ **FLAGGED / UNVERIFIED — test this by hand before committing to a layout.** +> Copilot's docs explicitly confirm it falls back to reading the **marketplace** manifest from +> `.claude-plugin/`. They do **not** confirm the same fallback for an individual plugin's +> `plugin.json`; the Copilot docs only show `plugin.json` at the plugin root. Claude *requires* +> it in `.claude-plugin/`. Until you verify, the pragmatic move is to **ship `plugin.json` in +> both** `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical +> content), then drop whichever proves redundant. The architect skill should generate both and +> note the duplication. + +### `@` resolves to the manifest `name`, not the repo + +`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's +top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name. +Keep them aligned to avoid confusion, but know they're separate things. + +--- + +## 3. Canonical repo layout (cross-compatible) + +```text +repo-root/ +├── .claude-plugin/ +│ └── marketplace.json # both tools read here +├── .github/plugin/ +│ └── marketplace.json # OPTIONAL: Copilot's canonical path (mirror of above) +├── plugins/ +│ └── startup-cto/ +│ ├── plugin.json # Copilot root manifest ┐ ship both until +│ ├── .claude-plugin/ # │ the §2 note is +│ │ └── plugin.json # Claude manifest ┘ verified +│ ├── skills/ +│ │ ├── fundraising/SKILL.md +│ │ └── hiring/SKILL.md +│ ├── agents/ +│ │ ├── startup-cto.md # Claude +│ │ └── startup-cto.agent.md # Copilot (only if you ship native agents) +│ ├── hooks/hooks.json # Claude +│ ├── hooks.json # Copilot (if hooks used) +│ ├── docs/ +│ └── README.md +└── README.md +``` + +If maintaining two manifest copies is annoying, generate the mirrors from one source in CI +(see §7) rather than hand-editing both. + +### Manifest shapes + +`marketplace.json` (root): + +```json +{ + "name": "my-ai-marketplace", + "owner": { "name": "Your Name", "email": "you@example.com" }, + "metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" }, + "plugins": [ + { "name": "startup-cto", "source": "./plugins/startup-cto", "description": "..." }, + { "name": "system-architect", "source": "./plugins/system-architect", "description": "..." } + ] +} +``` + +`plugin.json` (keep minimal; add fields only when needed): + +```json +{ + "name": "startup-cto", + "version": "1.0.0", + "description": "Startup technical leadership toolkit", + "author": { "name": "Your Name" } +} +``` + +Plugin names must be **kebab-case** (lowercase, digits, hyphens). Claude.ai's marketplace sync +rejects anything else even though the local CLI may tolerate it. + +--- + +## 4. Gotchas the original handoff omitted + +These will cause real breakage during refactor. The architect skill must check for them. + +1. **Plugins are copied to a cache on install.** A plugin **cannot** reference files outside its + own directory (e.g. `../shared-utils`) — those files aren't copied. If your current repo shares + helper files across skills/agents, that sharing breaks. Fix by **duplicating** the shared file + into each plugin or using **symlinks**. Audit for cross-references before moving anything. + +2. **Version-pinning footgun.** If `plugin.json` sets `"version"` and you don't bump it on a new + release, existing users get **no update** — the cached copy is kept. Either bump every release, + or **omit `version`** so the git commit SHA is used (every commit = new version). Don't set + `version` in both `plugin.json` and the marketplace entry; the `plugin.json` value wins silently. + +3. **Reserved marketplace names.** Claude blocks a set of names (`anthropic-*`, `claude-*`, + `agent-skills`, and impersonators like `official-claude-plugins`). Validate against these. + +4. **`commands/` ≠ `skills/` in Claude.** A flat `foo.md` is a legacy *command*; a + `foo/SKILL.md` directory is a *skill*. Promote flat command files to skill directories during + migration — don't treat them as interchangeable. + +5. **Use `${CLAUDE_PLUGIN_ROOT}`** in Claude hook/MCP configs to reference in-plugin files, since + the plugin runs from a cache path, not its repo location. + +6. **Strict mode (Claude).** A marketplace plugin entry defaults to `strict: true` — `plugin.json` + is authoritative and the entry can only supplement it. Set `strict: false` to make the + marketplace entry the *entire* definition (it then declares `skills`/`agents`/`hooks`/`mcpServers` + path arrays itself, and the plugin needs no `plugin.json`). Useful when the architect curates a + plugin's exposed components differently from how the files are laid out. Don't mix the two — a + `strict:false` entry plus a component-declaring `plugin.json` is a conflict and fails to load. + +7. **Plugin sources beyond relative paths (Claude).** This repo uses `./plugins/x` relative sources + (simplest for a monorepo). If you later split a plugin into its own repo, the `source` field also + supports `github` (`owner/repo` + `ref`/`sha`), `git-subdir` (sparse clone of a monorepo path), + `url` (any git host), and `npm` (published package). Copilot's docs only *show* relative-path + sources — other source types aren't documented there, so don't rely on them cross-tool. Stay on + relative paths unless you have a reason not to. + +8. **Copilot declares component paths in `plugin.json` (Claude does it differently).** Copilot's + `plugin.json` can carry `"skills": "skills/"`, `"agents": "agents/"`, `"hooks": "hooks.json"`, + `"mcpServers": ".mcp.json"` fields that tell it where components live. Claude instead defaults to + the standard dirs and only takes path overrides via the *marketplace entry* (see strict mode, + #6). So the same `plugin.json` may need these path fields for Copilot but not for Claude — another + reason the architect should generate per-tool manifests rather than one shared file. + +--- + +## 5. The `marketplace-architect` skill spec + +Build this skill **on the corrected spec above** — not on the original handoff, which would bake +in the format errors. It's a Claude Code skill (`skills/marketplace-architect/SKILL.md`) following +standard skill conventions: a tight SKILL.md body (<500 lines) plus bundled `references/` and +`scripts/` loaded progressively. + +### Frontmatter (description is the trigger — make it pushy) + +```yaml +--- +name: marketplace-architect +description: > + Audits a repository of Claude Code / Copilot CLI skills, agents, hooks, and prompts and + refactors it into an installable plugin marketplace. Use this whenever the user wants to + organize loose skills/agents into plugins, define plugin boundaries, generate plugin.json + or marketplace.json manifests, plan a migration to a plugin marketplace, validate plugin + naming or detect duplicate capabilities, or set up cross-tool (Claude Code + GitHub Copilot + CLI) distribution — even if they don't say the word "marketplace". +--- +``` + +### Responsibilities (from the handoff, refined) + +1. **Audit** — walk the repo; classify every asset as skill / command / agent / hook / prompt / MCP. +2. **Detect cross-references** — flag any `../` or shared-file dependencies that break under caching (§4.1). +3. **Recommend plugin boundaries** — group by outcome; warn on 50-tiny-plugins sprawl. +4. **Prevent duplicate capabilities** — diff skill descriptions/agents for overlap before splitting. +5. **Generate manifests** — emit `plugin.json` (both locations per §2 note) and `marketplace.json` + (`.claude-plugin/`, optionally mirror to `.github/plugin/`). +6. **Validate naming** — kebab-case, reserved names, unique plugin names, `@name` ↔ manifest `name`. +7. **Produce a migration plan** — concrete file-move list (old path → new path) as a checklist. +8. **Emit cross-tool deltas** — for each plugin, note what's needed for Copilot (`.agent.md`, + root `plugin.json`) vs Claude. +9. **Generate release notes + install docs** — per-plugin README with both `claude` and `copilot` + install commands. + +### Suggested bundled structure + +```text +skills/marketplace-architect/ +├── SKILL.md +├── references/ +│ ├── claude-code.md # Claude paths, validate, version rules, reserved names +│ ├── copilot-cli.md # Copilot paths, .agent.md, marketplace fallback +│ └── cross-compat.md # the §2 divergence table — the heart of the skill +└── scripts/ + ├── inventory.py # scan repo → classify assets → emit a table + ├── gen_manifests.py # write plugin.json + marketplace.json (both layouts) + └── validate.py # wraps `claude plugin validate .` + JSON/naming checks +``` + +Put the §2 divergence table verbatim into `references/cross-compat.md` — that's the knowledge the +skill exists to apply. Keep SKILL.md to the workflow (audit → boundaries → generate → validate) +and point it at the reference files. + +### Authoring notes + +- Use **imperative** instructions ("Scan the repo", "Emit the manifest"). +- Explain *why* a rule matters (e.g. the cache constraint) rather than bare MUSTs — the model + applies judgment better with rationale. +- After drafting, run 2–3 realistic test prompts (e.g. "turn this repo into a marketplace", + "which of these skills belong together?") and iterate. + +--- + +## 6. Refactor playbook (corrected phases) + +Run these *with* the architect skill once it exists; it automates 1–5. + +1. **Inventory.** Classify every asset (skill/command/agent/hook/prompt/MCP). Record current path. +2. **Detect breakage.** Find cross-references and shared files (§4.1). Decide duplicate vs symlink. +3. **Draw boundaries.** Group by outcome into ~10–20 plugins. De-dupe overlapping capabilities. +4. **Move files.** + - `skills/react.md` (flat command) → `plugins/system-architect/skills/react/SKILL.md` + - `agents/startup-founder.md` → `plugins/startup-cto/agents/startup-founder.md` + (+ `startup-founder.agent.md` if shipping Copilot-native agents) +5. **Generate manifests.** Per-plugin `plugin.json` (both locations); root `marketplace.json`. +6. **Validate.** `claude plugin validate .` — fix every warning. Then test-install in both tools: + - `claude --plugin-dir ./plugins/` then `/plugin install @` + - `copilot plugin install ./plugins/` then `copilot plugin list` / `/skills list` / `/agent` +7. **Document.** Per-plugin README with both install commands; root README listing all plugins. + +--- + +## 7. Future / roadmap (preserved from the handoff) + +Three post-migration features the original handoff called for. Not needed for the first cut, but +recorded here so the intent isn't lost. + +### Marketplace website + +Generate a docs site **directly from `marketplace.json`** — no separate content source. Iterate +over the `plugins` array to produce a browsable catalog (one page per plugin from its +`description`/`README.md`), publishable to something like `marketplace.example.com` via GitHub +Pages. Because the manifest is the single source of truth, the site never drifts from what's +installable. + +### Plugin templates + +Add a `templates/` directory so contributors never start from scratch: + +```text +templates/ + skill-plugin/ # plugin.json + skills//SKILL.md skeleton + agent-plugin/ # plugin.json + agents/ skeleton (both .md and .agent.md) + workflow-plugin/ # plugin.json + a multi-step orchestration skill +``` + +Each ships the dual-manifest layout from §3 so new plugins are cross-tool by default. + +### CI validation (the cross-tool catch) + +A GitHub Action can gate PRs, but remember: **`claude plugin validate` covers the Claude side +only.** There's no Copilot validator, so the Action needs custom JSON checks for the Copilot +layout. Minimum checks: + +- Valid JSON in every `marketplace.json` / `plugin.json`. +- Each plugin's `SKILL.md` files have valid YAML frontmatter. +- Unique plugin names; kebab-case; no reserved names. +- Every `source` path resolves to an existing directory. +- (If mirroring) `.claude-plugin/` and `.github/plugin/` manifests are in sync. +- Version consistency (no conflicting `version` in plugin.json vs marketplace entry). + +Fail the PR on any violation so the contributor flow stays self-service. + +### Success criteria (the target end-state) + +A new contributor should be able to, with **no manual registry edits, no backend, no MCP +dependency**: (1) fork the repo, (2) create a plugin folder, (3) add `plugin.json`, (4) add +skills/agents, (5) open a PR, (6) have the plugin appear in the marketplace automatically once +merged. If a step requires hand-editing a central list, the automation isn't done. + +--- + +## Open questions to resolve first + +Two facts couldn't be confirmed from the published docs. Settle both with a one-skill test plugin +before the architect generates layouts: + +1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?** (Install + the test plugin both ways.) The answer decides whether you ship one manifest or two. Copilot's + docs put `plugin.json` at the plugin root; Claude requires `.claude-plugin/`. +2. **What is Copilot's exact install-from-marketplace command?** The how-to pages don't show the + literal string, and Copilot's `update`/`uninstall` take a bare plugin name — so it may *not* use + Claude's `@` form. Run `copilot plugin install --help` and confirm before + putting the command in any README or the architect's generated docs.