feat: consolidate marketplace skills into kyberforge plugin

Moves create-plugin, marketplace-architect, write-skill, and write-eval
from canonical .agents/skills/ into plugins/kyberforge/skills/, along
with all bundled sub-files, evals, and the plugin-marketplace-architecture
research doc. Bundles templates/plugin/ into create-plugin/assets/plugin-template/
so the skill is self-contained after install-time caching. Removes
templates/plugin/ and docs/research/plugin-marketplace-architecture.md
from the repo root as they are now exclusively in the plugin.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:02:47 +00:00
parent 2287ddccbf
commit 280e98cb71
35 changed files with 7 additions and 7 deletions

View File

@@ -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/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical — lean on these |
| Agents | `agents/<name>.md` | `agents/<name>.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 <name>@<marketplace-name>` | from a registered marketplace by plugin name; **`@marketplace` suffix not confirmed** — `update`/`uninstall` take a bare `<name>`, 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.
### `@<marketplace-name>` 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/<x>` then `/plugin install <x>@<marketplace>`
- `copilot plugin install ./plugins/<x>` 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/<name>/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 `<name>@<marketplace>` form. Run `copilot plugin install --help` and confirm before
putting the command in any README or the architect's generated docs.

View File

@@ -0,0 +1,13 @@
```yaml
version: "1.0"
updated: 2026-06-20
when: Invoked when the user wants to create a new plugin in the marketplace. Scaffolds the
directory structure from templates/plugin/, substitutes PLUGIN_NAME/PLUGIN_DESCRIPTION/
AUTHOR_NAME/AUTHOR_EMAIL/AUTHOR_URL placeholders, writes to plugins/<name>/, registers
the plugin in .claude-plugin/marketplace.json, runs claude plugin validate ., and hands
off to /marketplace-architect.
references:
- docs/research/plugin-marketplace-architecture.md
```

View File

@@ -0,0 +1,89 @@
---
name: create-plugin
description: >
Use when the user wants to create a new plugin in the marketplace — scaffold the directory
structure, generate plugin.json manifests for Claude Code and Copilot CLI, and register
the plugin in marketplace.json. Triggers: "create a new plugin", "add a plugin called X",
"scaffold a plugin", "new plugin for Y". Do NOT use when auditing or refactoring existing
plugins (use /marketplace-architect), authoring skill content inside a plugin (use
/write-skill), adopting an external plugin into the marketplace (use /marketplace-architect),
or validating existing manifests without creating anything (use /marketplace-architect).
metadata:
category: marketplace
---
<requirements>
## Required inputs
- **Plugin name** — kebab-case slug; ask if not stated. Validate: lowercase letters, digits, hyphens only; not already present in `plugins/` or `.claude-plugin/marketplace.json`; not a reserved name (`anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins`).
- **Plugin description** — one sentence; ask if not stated.
- **Author name** — ask if not stated.
- **Author email** — ask if not stated; used in the Copilot root `plugin.json`.
- **Author URL** — ask if not stated; used in Claude's `.claude-plugin/plugin.json`.
## Constraints
- Load the plugin template from `assets/plugin-template/` bundled inside this skill (`${CLAUDE_PLUGIN_ROOT}/skills/create-plugin/assets/plugin-template/`). Stop if the path is missing — do not generate files from memory.
- Plugin name must be kebab-case and not a reserved name (`anthropic-*`, `claude-*`, `agent-skills`, `official-claude-plugins`) — halt and ask for a replacement before Gate A if violated.
- Do not write any file until Gate A (plan approval) and Gate B (file contents approval) are both explicitly confirmed.
- Replace all five placeholder markers — `PLUGIN_NAME`, `PLUGIN_DESCRIPTION`, `AUTHOR_NAME`, `AUTHOR_EMAIL`, `AUTHOR_URL` — in every copied file before Gate B review. No marker may appear in written output.
- Do not generate skill content — `skills/` is scaffolded as an empty directory with README only. Direct the user to `/write-skill` to add skills.
- Do not set `version` in both `plugin.json` and the marketplace entry — `plugin.json` wins silently and blocks updates for existing users.
- Update `.github/plugin/marketplace.json` only if it already exists — do not create it.
- Do not auto-invoke `/marketplace-architect` — hand off by name at the end; let the user trigger it.
</requirements>
<steps>
## Process
1. **Collect inputs.** Ask for plugin name, description, author name, author email, and author URL — one question at a time. Validate the plugin name: kebab-case format, not a reserved name, not already present in `plugins/` or `.claude-plugin/marketplace.json`. If any validation fails, stop and ask for a replacement before continuing.
2. **Check template.** Load the bundled template from `assets/plugin-template/` inside this skill (`${CLAUDE_PLUGIN_ROOT}/skills/create-plugin/assets/plugin-template/`). If the path is missing, stop and report it — do not proceed or generate files from memory.
3. **Gate A — plan review.** Present: the list of files that will be written (derived from `assets/plugin-template/` with `PLUGIN_NAME` substituted into filenames), the new `plugins/<name>/` directory path, the marketplace entry that will be added to `.claude-plugin/marketplace.json`, and whether `.github/plugin/marketplace.json` will also be updated. Wait for explicit approval — do not proceed on "looks good" or silence.
4. **Copy and substitute.** Copy `assets/plugin-template/` to `plugins/<name>/`. In every copied file, replace all occurrences of `PLUGIN_NAME`, `PLUGIN_DESCRIPTION`, `AUTHOR_NAME`, `AUTHOR_EMAIL`, and `AUTHOR_URL` with the collected values. Rename any file or directory whose name contains `PLUGIN_NAME`.
5. **Gate B — file contents review.** Show every file with its full substituted content. Wait for explicit approval — do not write until confirmed.
6. **Write files.** Write all substituted files to `plugins/<name>/`. Append the new plugin entry to `.claude-plugin/marketplace.json`. If `.github/plugin/marketplace.json` exists, append the same entry there.
7. **Validate.** Run `claude plugin validate .` from `plugins/<name>/`. Report all output inline — do not suppress warnings. If the command is unavailable, note it and suggest the user run it manually after local install.
8. **Hand off.** Print: "Plugin `<name>` created and registered in `marketplace.json`. Fill in skill and agent content, then run `/marketplace-architect` to audit the full marketplace."
## Output format
- `plugins/<name>/` — directory tree copied from `assets/plugin-template/` with all placeholders substituted
- `.claude-plugin/marketplace.json` — updated with the new plugin entry
- `.github/plugin/marketplace.json` — updated with the same entry, only if it already existed
- Inline validation report from `claude plugin validate .`
</steps>
<checks>
## Failure handling
- `assets/plugin-template/` not found at `${CLAUDE_PLUGIN_ROOT}/skills/create-plugin/assets/plugin-template/` — stop, report the path, do not generate from memory.
- Plugin name already exists in `plugins/` or `marketplace.json` — stop, report the conflict, ask for a different name.
- Reserved name detected — stop, report the name and the reserved list, ask for a replacement before continuing.
- `claude plugin validate .` unavailable — report that automated validation was skipped; suggest running it manually with `claude --plugin-dir ./plugins/<name>`.
## Self-check
- [ ] Plugin name validated: kebab-case, not reserved, not already present in `plugins/` or `marketplace.json`
- [ ] `assets/plugin-template/` verified to exist at `${CLAUDE_PLUGIN_ROOT}/skills/create-plugin/assets/plugin-template/` before any file generation
- [ ] Gate A presented with file list and marketplace entry — explicit approval received
- [ ] All five markers substituted in all files — none appear in written output
- [ ] Gate B presented with full substituted file contents — explicit approval received
- [ ] Files written only after Gate B approval
- [ ] `.github/plugin/marketplace.json` updated only if it already existed — not created
- [ ] `claude plugin validate .` run and results reported; or unavailability noted
- [ ] Hand-off message printed directing user to `/marketplace-architect`
- [ ] No skill content generated — `skills/` is empty with README only
</checks>

View File

@@ -0,0 +1,8 @@
{
"name": "PLUGIN_NAME",
"displayName": "PLUGIN_NAME",
"description": "PLUGIN_DESCRIPTION",
"author": { "name": "AUTHOR_NAME", "url": "AUTHOR_URL" },
"license": "MIT",
"keywords": []
}

View File

@@ -0,0 +1,3 @@
{
"mcpServers": {}
}

View File

@@ -0,0 +1,42 @@
# PLUGIN_NAME
PLUGIN_DESCRIPTION
## Install
**Claude Code:**
```bash
claude plugin marketplace add <owner>/<repo>
claude plugin install PLUGIN_NAME@<marketplace-name>
```
**GitHub Copilot CLI:**
```bash
copilot plugin marketplace add <owner>/<repo>
copilot plugin install PLUGIN_NAME
```
**Local (development):**
```bash
# Claude Code
claude --plugin-dir ./plugins/PLUGIN_NAME
# GitHub Copilot CLI
copilot plugin install ./plugins/PLUGIN_NAME
```
## Contents
| Component | Path | Description |
|---|---|---|
| Skills | `skills/` | Slash commands available after install |
| Agents | `agents/` | Role-based agents (`.md` for Claude, `.agent.md` for Copilot) |
| Hooks | `hooks/hooks.json` (Claude) / `hooks.json` (Copilot) | Event-triggered automation |
| MCP servers | `.mcp.json` | Model Context Protocol server definitions |
## Author
AUTHOR_NAME

View File

@@ -0,0 +1,13 @@
---
name: PLUGIN_NAME
description: PLUGIN_DESCRIPTION
tools:
- read_file
- list_directory
---
Replace this with your agent's system prompt and instructions.
This file is loaded by **GitHub Copilot CLI**. The `tools:` frontmatter field declares
which tools the agent can use — add or remove tools as needed.
For Claude Code, see `PLUGIN_NAME.md`.

View File

@@ -0,0 +1,8 @@
---
name: PLUGIN_NAME
description: PLUGIN_DESCRIPTION
---
Replace this with your agent's system prompt and instructions.
This file is loaded by **Claude Code**. For GitHub Copilot CLI, see `PLUGIN_NAME.agent.md`.

View File

@@ -0,0 +1,13 @@
# agents/
Agent definitions for this plugin. Each agent needs two files — one per tool:
| File | Tool | Notes |
|---|---|---|
| `<name>.md` | Claude Code | Frontmatter: `name`, `description`. No `tools:` field. |
| `<name>.agent.md` | GitHub Copilot CLI | Frontmatter: `name`, `description`, `tools:` (array of permitted tool names). |
The two files share the same system prompt body. Keep them in sync.
Rename `PLUGIN_NAME.md` and `PLUGIN_NAME.agent.md` to your agent's name (kebab-case).
Add additional agent pairs as needed — one pair per agent.

View File

@@ -0,0 +1,11 @@
# bin/
**Claude Code only.** Executables placed here are added to PATH when the plugin is installed.
Use this for CLI tools, helper scripts, or MCP server entry points bundled with the plugin.
Reference files in this directory from `.mcp.json` or hooks using `${CLAUDE_PLUGIN_ROOT}/bin/<file>`.
The `${CLAUDE_PLUGIN_ROOT}` variable resolves to the plugin's install cache path at runtime —
do not use relative paths from the repo root, as they will break after install.
GitHub Copilot CLI does not support `bin/`. If you add executables here, they are Claude Code only.

View File

@@ -0,0 +1,7 @@
# docs/
Plugin documentation. Place usage guides, reference material, and examples here.
This directory is not read automatically by either Claude Code or GitHub Copilot CLI.
Reference specific files from your skill bodies or agent prompts when needed
(e.g. `See references/usage.md for examples`).

View File

@@ -0,0 +1,3 @@
{
"hooks": []
}

View File

@@ -0,0 +1,27 @@
# hooks/
**Claude Code only.** Hook definitions that run in response to Claude Code events.
The `hooks.json` in this directory is read by Claude Code. Structure:
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "echo 'tool used'" }
]
}
]
}
}
```
Supported events: `PreToolUse`, `PostToolUse`, `Notification`, `Stop`.
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs
from a cache path after install, not its original repo location.
For GitHub Copilot CLI hooks, see `hooks.json` at the plugin root.

View File

@@ -0,0 +1,3 @@
{
"hooks": {}
}

View File

@@ -0,0 +1,11 @@
{
"name": "PLUGIN_NAME",
"description": "PLUGIN_DESCRIPTION",
"author": { "name": "AUTHOR_NAME", "email": "AUTHOR_EMAIL" },
"license": "MIT",
"keywords": [],
"agents": "agents/",
"skills": ["skills/"],
"hooks": "hooks.json",
"mcpServers": ".mcp.json"
}

View File

@@ -0,0 +1,18 @@
# skills/
Skills for this plugin. Each skill lives in its own subdirectory:
```
skills/
<skill-name>/
SKILL.md # required — frontmatter + skill body
META.md # required — provenance and audit fields
references/ # optional — on-demand reference docs
scripts/ # optional — executable helper scripts
assets/ # optional — templates, data files, lookup tables
```
**Shared** — both Claude Code and GitHub Copilot CLI read `skills/<name>/SKILL.md`.
To add a skill, run `/write-skill` in a Claude Code session. Do not write SKILL.md by hand
without following the authoring standard — trigger descriptions and self-checks are required.

View File

@@ -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
```

View File

@@ -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
---
<requirements>
## 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.
</requirements>
<steps>
## 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/<name>` and `copilot plugin install ./plugins/<name>`.
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
</steps>
<checks>
## 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
</checks>

View File

@@ -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: <name>/SKILL.md
├── commands/ # legacy flat .md files; promote to skills/ for new plugins
├── agents/ # agent definitions: <name>.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 <name>@<marketplace>
claude plugin update <name>@<marketplace>
claude plugin uninstall <name>
claude plugin list
claude plugin enable <name>
claude plugin disable <name>
# 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.

View File

@@ -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: <name>/SKILL.md (same as Claude Code)
├── agents/ # agent files: <name>.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 <name>
# Marketplace
copilot plugin marketplace add owner/repo
```
> ⚠️ **UNVERIFIED: Copilot marketplace install command.**
> The `update`/`uninstall` commands take a bare `<name>`. Whether install from a marketplace
> uses `<name>@<marketplace>` (Claude Code's form) or a bare `<name>` 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/<name>.md` | `agents/<name>.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) |

View File

@@ -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/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical |
| Agents | `agents/<name>.md` | `agents/<name>.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 <name>@<marketplace-name>` | 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.
---
## `@<marketplace-name>` 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.

View File

@@ -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 <repo-root> --marketplace-name <name> [options]
#
# Options:
# --marketplace-name <name> kebab-case marketplace identifier (required)
# --author "Name <email>" author string (default: "Unknown <unknown@example.com>")
# --plugins-dir <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 <unknown@example.com>"
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 <repo-root> --marketplace-name <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

View File

@@ -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 <repo-path>
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: inventory.sh <repo-path>" >&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" && "$part" != ".agents" ]]; 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

View File

@@ -0,0 +1,332 @@
#!/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 <repo-root>
# validate.sh <repo-root> --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 — shared identity fields must match; component path fields legitimately diverge
if [[ -f "$claude_manifest" && -f "$root_manifest" ]]; then
local field cv rv
for field in name description version license; do
cv=$(jq -r ".$field // empty" "$claude_manifest")
rv=$(jq -r ".$field // empty" "$root_manifest")
if [[ ( -n "$cv" || -n "$rv" ) && "$cv" != "$rv" ]]; then
error "$pd: '$field' differs between .claude-plugin/plugin.json ('$cv') and plugin.json ('$rv')"
fi
done
cv=$(jq -r '.author.name // empty' "$claude_manifest")
rv=$(jq -r '.author.name // empty' "$root_manifest")
if [[ ( -n "$cv" || -n "$rv" ) && "$cv" != "$rv" ]]; then
error "$pd: 'author.name' differs between .claude-plugin/plugin.json ('$cv') and plugin.json ('$rv')"
fi
cv=$(jq -r '.keywords // [] | sort | join(",")' "$claude_manifest")
rv=$(jq -r '.keywords // [] | sort | join(",")' "$root_manifest")
if [[ "$cv" != "$rv" ]]; then
error "$pd: 'keywords' differs between .claude-plugin/plugin.json and plugin.json"
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 <repo-root> [--plugin <path>]" >&2
exit 1
fi
ROOT="$(cd "$ROOT" && pwd)"
# ── validate marketplace.json ─────────────────────────────────────────────────
CLAUDE_MARKETPLACE="$ROOT/.claude-plugin/marketplace.json"
COPILOT_MARKETPLACE="$ROOT/.github/plugin/marketplace.json"
MARKETPLACE_JSON=""
for mp in "$CLAUDE_MARKETPLACE" "$COPILOT_MARKETPLACE"; do
if [[ -f "$mp" ]]; then
[[ -z "$MARKETPLACE_JSON" ]] && MARKETPLACE_JSON="$mp"
validate_marketplace_json "$mp"
fi
done
if [[ -z "$MARKETPLACE_JSON" ]]; then
warn "No marketplace.json found. Expected at .claude-plugin/marketplace.json"
fi
# Marketplace sync check — shared identity fields must match; description/version
# legitimately differ in structure (Claude: top-level; Copilot: under metadata)
if [[ -f "$CLAUDE_MARKETPLACE" && -f "$COPILOT_MARKETPLACE" ]]; then
cm_val=$(jq -r '.name // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r '.name // empty' "$COPILOT_MARKETPLACE")
if [[ "$cm_val" != "$cp_val" ]]; then
error "marketplace: 'name' differs — .claude-plugin ('$cm_val') vs .github/plugin ('$cp_val')"
fi
cm_val=$(jq -r '.owner.name // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r '.owner.name // empty' "$COPILOT_MARKETPLACE")
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
error "marketplace: 'owner.name' differs — '$cm_val' vs '$cp_val'"
fi
# description: Claude top-level, Copilot under metadata — compare values regardless of path
cm_val=$(jq -r '.description // .metadata.description // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r '.metadata.description // .description // empty' "$COPILOT_MARKETPLACE")
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
error "marketplace: description differs between .claude-plugin/marketplace.json and .github/plugin/marketplace.json"
fi
# version: same structural divergence as description
cm_val=$(jq -r '.version // .metadata.version // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r '.metadata.version // .version // empty' "$COPILOT_MARKETPLACE")
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
error "marketplace: version differs — '$cm_val' vs '$cp_val'"
fi
# Plugin catalog must be identical across both files
cm_plugins=$(jq -r '.plugins[].name' "$CLAUDE_MARKETPLACE" 2>/dev/null | sort)
cp_plugins=$(jq -r '.plugins[].name' "$COPILOT_MARKETPLACE" 2>/dev/null | sort)
if [[ "$cm_plugins" != "$cp_plugins" ]]; then
error "marketplace: plugin lists differ between .claude-plugin/marketplace.json and .github/plugin/marketplace.json"
else
while IFS= read -r pname; do
[[ -z "$pname" ]] && continue
cm_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$COPILOT_MARKETPLACE")
if [[ "$cm_val" != "$cp_val" ]]; then
error "marketplace plugin '$pname': source differs — '$cm_val' vs '$cp_val'"
fi
cm_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .description // empty' "$CLAUDE_MARKETPLACE")
cp_val=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .description // empty' "$COPILOT_MARKETPLACE")
if [[ ( -n "$cm_val" || -n "$cp_val" ) && "$cm_val" != "$cp_val" ]]; then
error "marketplace plugin '$pname': description differs between the two marketplace.json files"
fi
done <<< "$cm_plugins"
fi
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

View File

@@ -0,0 +1,141 @@
---
name: write-eval
description: Write or generate an eval.yaml test file for a skill. Use when the user wants to create evals, add test coverage, or says "write evals for this skill", "create eval.yaml for X", or "add tests for this skill". Do NOT use when the user wants to run existing evals, write unit tests for code, or debug test failures.
version: "1.0"
updated: 2026-05-17
when: invoked by explicit trigger ("write evals for this skill", "create eval.yaml for X") or implicit request for skill test coverage
metadata:
category: factory
source:
- repo: agentskills/agentskills
commit: 2d3e01f590f68bee2cb76a3200823e93b2cc9eaa
files:
- docs/skill-creation/evaluating-skills.mdx # evals schema, two-section workspace layout, assertion quality guidelines
updated: 2026-05-17
- repo: darkrishabh/agent-skills-eval
commit: b60eebe3c6edaa917a284e13b9b0e9fa00f1c957
files:
- src/types.ts # AgentSkillsEval interface — string-slug id, name field, prompt/expected_output/assertions structure
- examples/basic-skill/evals/evals.json # concrete schema example
updated: 2026-05-17
- repo: bmad-code-org/BMAD-METHOD
commit: 71136bc6af77cbf507d3768494311d5b6ca95cc5
files:
- evals/bmm-skills/bmad-product-brief/triggers.json # trigger classification dataset, should_trigger boolean pattern
- evals/bmm-skills/bmad-product-brief/evals.json # output test structure, boundary-enforcement negative test pattern
updated: 2026-05-17
- repo: mattpocock/skills
commit: e74f0061bb67222181640effa98c675bdb2fdaa7
files:
- skills/engineering/tdd/SKILL.md # behavioral test philosophy: test observable outputs through public interfaces
updated: 2026-05-17
references:
- https://agentskills.io/skill-creation/evaluating-skills
---
## Role
You are a test architect producing eval.yaml files that verify AI skill trigger behaviour and output quality.
## When to use / When not to use
**Use when:**
- User explicitly requests evals: "write evals for this skill", "create eval.yaml for X", "add tests for this skill"
- A new or refactored skill needs an eval file
- Existing eval coverage needs to be extended with additional test cases
**Do not use when:**
- User wants to run or execute existing evals
- User wants to write unit tests for application code (not a skill eval)
- User asks to debug or analyse failing eval results
- User asks to review or compare eval output
## Required inputs
- Target skill name — explicit or unambiguous from session context
- Target skill's SKILL.md — must be readable at `.agents/skills/<skill-name>/SKILL.md`
- Target skill's `metadata.category` — used to derive the output path
## Constraints
- Output path: `.agents/evals/<category>/<skill-name>/eval.yaml` — nested by category, not flat
- Every eval.yaml must contain all five required test types: ≥1 explicit trigger, ≥1 implicit trigger, ≥1 negative trigger, ≥2 deterministic output, ≥1 LLM-rubric quality
- Assertions must be specific and verifiable — "The output contains a trigger_tests section" not "The output is good"
- Assertions must be provider-agnostic — no tool-call assertions, no assumptions about the underlying model or runtime
- Show the test plan and wait for confirmation before writing any file
- On re-run (eval.yaml already exists): merge — classify proposed cases as NEW / IDENTICAL / CONFLICT; surface conflicts for human resolution before writing; do not silently overwrite
- Body ≤500 lines
## Process
1. **Identify the target skill.** If not explicit in the invocation, infer from session context. If ambiguous, ask before proceeding.
2. **Read the target SKILL.md** at `.agents/skills/<skill-name>/SKILL.md`. Extract:
- `name`, `metadata.category` (for output path)
- `description` (trigger description — source for explicit and implicit trigger test queries)
- When / when not criteria (source for negative trigger test queries)
- Required inputs and output format (source for deterministic output assertions)
3. **Check for an existing eval.yaml** at `.agents/evals/<category>/<skill-name>/eval.yaml`.
- If it exists: read it and record all existing test IDs.
4. **Propose test cases** — one minimum per required type:
**trigger_tests** — classify each query by whether the skill should activate:
- ≥1 explicit trigger: a query using the skill's exact trigger phrase
- ≥1 implicit trigger: a query describing the task without the trigger phrase; derive from the skill's purpose and use cases
- ≥1 negative trigger: a query for an adjacent task the skill must NOT activate on; derive from the skill's when-not criteria; choose a case with surface similarity to the trigger
**output_tests** — test what the skill produces:
- ≥2 deterministic: assert on observable, machine-checkable properties of the output — required sections present, correct file path, schema compliance. Write as specific string conditions a reader could verify without inference.
- ≥1 LLM-rubric: holistic quality assertions — conditions a judge evaluates from the full output. Test qualities that deterministic checks cannot capture: realism of trigger queries, specificity of assertions, boundary case coverage.
For all assertions: write as verifiable conditions, not value judgements. Test boundary cases, not only happy paths. A good assertion survives internal refactoring of the skill.
5. **Classify proposed cases if an existing eval.yaml was found:**
- **NEW** — ID not in existing file; safe to append
- **IDENTICAL** — ID exists, content matches exactly; skip silently
- **CONFLICT** — ID exists, content differs; display existing vs proposed side-by-side
6. **Present the full test plan.** Show each proposed case with its classification label (NEW / IDENTICAL / CONFLICT). For CONFLICT cases, ask the user to choose: keep existing, use proposed, or skip. Wait for confirmation before writing.
7. **Write eval.yaml.** Append NEW cases to the existing file (or write the full structure for a new file). Apply CONFLICT resolutions as chosen. Skip IDENTICAL cases.
## Output format
```yaml
skill_name: <name>
trigger_tests:
- id: <string-slug> # e.g. explicit-trigger-basic
name: <display label> # human-readable, e.g. "Explicit trigger — basic invocation"
query: <exact user input text>
should_trigger: true # true for explicit and implicit; false for negative
output_tests:
- id: <string-slug>
name: <display label>
type: deterministic # or llm-rubric
prompt: <user input to the skill>
expected_output: <prose description of ideal output>
assertions:
- <specific, verifiable condition string>
```
## Failure handling
- **Target SKILL.md not found:** stop, report the path searched, do not guess or generate content from the skill name alone
- **`metadata.category` absent from SKILL.md:** ask for the category before computing the output path
- **All proposed cases conflict with existing file:** report the full conflict summary, wait for explicit direction — do not auto-resolve
- **Proposed test count below minimums:** flag which type is short before presenting the plan; do not proceed with a deficient eval
## Self-check
Verify before writing:
- [ ] All five test types present — ≥1 explicit, ≥1 implicit, ≥1 negative trigger; ≥2 deterministic, ≥1 LLM-rubric output
- [ ] trigger_tests: at least one `should_trigger: true` and at least one `should_trigger: false`
- [ ] All assertions are specific and verifiable — no vague quality claims
- [ ] Output path matches `.agents/evals/<category>/<skill-name>/eval.yaml`
- [ ] Test plan was presented and confirmed before the file was written
- [ ] CONFLICT cases were surfaced to the user and not silently resolved

View File

@@ -0,0 +1,16 @@
# Skill Categories
| Category | Scope |
|---|---|
| `design` | grill-me, grill-with-docs, to-prd, prototype, architecture-review |
| `plan` | to-issues, triage |
| `implement` | tdd, diagnose, implement-feature, refactor, write-docs |
| `test` | write-tests, generate-test-data, review-test-coverage |
| `review` | improve-codebase-architecture, code-review, security-review, pr-description, changelog-entry |
| `deploy` | write-ci-pipeline, write-deployment-config, write-ai-review-workflow, deployment-checklist |
| `operate` | write-runbook, incident-diagnosis, post-mortem, inspect-deployment |
| `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 |

View File

@@ -0,0 +1,27 @@
```yaml
version: "1.0" # increment on meaningful changes to the skill
updated: YYYY-MM-DD # ISO date of last update
# when: describes when this skill is loaded — the full trigger context.
# More detail than the description field; not used for routing.
when: <describe the invocation conditions here>
# source: tracks content you ADOPTED from an upstream repo.
# Adopt = you read someone else's code or docs and incorporated text or logic directly.
# Omit this field entirely if the skill is self-authored — absence means original work.
# Present only when content was actually taken, tracked at commit-level for upgrade reviews.
source:
- repo: org/repo-name # GitHub slug — no URL, slug is stable and searchable
commit: <full SHA> # exact commit reviewed at time of adoption
files:
- path/to/file.md # inline comment: what was taken from this file
- path/to/other.md # inline comment: what was taken from this file
updated: YYYY-MM-DD # date this source entry was last reviewed
# references: tracks content you CITED but did not adopt verbatim.
# Cite = you read it and it informed the skill, but nothing was copied or adapted.
# Examples: a spec you followed, a paper that shaped the approach, external documentation.
# Distinct from source: source = took content; references = informed by content.
references:
- https://example.com/relevant-doc
```

View File

@@ -0,0 +1,16 @@
```yaml
version: "1.5"
updated: 2026-05-26
# when: describes when this skill is loaded — the full trigger context.
# More detail than the description field; not used for routing.
when: invoked by explicit trigger ("write a new skill for X", "create a SKILL.md that does Y") or implicit request to author a skill file or convert an existing placeholder to the canonical authoring standard
# source: omitted — self-authored original; no upstream content adopted
# Absence of source means self-authored. If content is adopted from upstream,
# add a source entry per the META-TEMPLATE.md schema.
references:
- https://agentskills.io/specification.md
- https://agentskills.io/skill-creation/optimizing-descriptions
```

View File

@@ -0,0 +1,93 @@
---
name: <skill-name>
# description: routing-only field — loaded at startup for every skill scan to decide whether
# to activate this skill. Write in imperative phrasing ("Use when X", not "This skill does X").
# Must cover: (1) what the skill does, (2) when to invoke it, (3) negative triggers — what
# adjacent tasks must NOT activate it. No behavioral or role framing; that belongs in the body.
# Max 1024 characters. The `when:` detail that lived here previously now lives in META.md.
# Example: "Use when the user wants to create a new SKILL.md file or convert a placeholder to
# canonical format. Do NOT use when updating an existing well-formed skill — use upgrade-skill."
description: <trigger description>
metadata:
category: <category — see CATEGORIES.md>
# allowed-tools: <add only when the skill has a narrow, well-defined tool surface; omit otherwise>
# model: sonnet | opus | haiku — Claude Code extension; overrides session model for this skill's turn.
# Omit to inherit the active session model. Factory §9 routing: haiku=formatting/classification,
# sonnet=most coding/review, opus=adversarial/complex reasoning.
---
<requirements>
## Required inputs
<!-- List each required input as a bullet: name, what it is, how the agent obtains it.
Negative trigger cases are NOT listed here — the agent proposes them during trigger testing.
Example:
- **Skill name** — kebab-case slug; inferred from user description if not stated explicitly, ask if ambiguous
- **Existing SKILL.md path** — for placeholder conversions only; read before writing -->
- **<Input name>** — <description; how obtained>
## Constraints
<!-- One rule per bullet. State the boundary condition inline. Plain English, no jargon.
Do not include a constraint about body section structure — the template enforces that.
Example:
- Frontmatter has three fields only: `name`, `description`, and `metadata.category` — add `allowed-tools` only when the skill has a narrow, well-defined tool surface
- Body ≤500 lines — content that explains rather than directs belongs in sub-files, not the body
- Sub-files use three spec-defined optional directories: `scripts/` (executable code), `references/` (on-demand docs), `assets/` (templates, data files, lookup tables). File references must be one level deep. Wire each sub-file with an explicit step instruction (e.g. "See references/lookup.md for error codes") — without wiring, the file is never loaded -->
- <constraint>
</requirements>
<steps>
## Process
<!-- Numbered steps with a bold action label. Short, direct sentences — state what to do and
what happens as a result. Call out hard gates explicitly (steps that block all progress
until satisfied). No preamble, no meta-commentary about the steps themselves.
Example:
1. **Scan for overlap.** Check `.agents/skills/` for skills with similar purpose or trigger phrases. If overlap is found, surface it and wait for explicit direction — do not continue.
2. **Grill.** Run a focused grill to reach shared understanding of: skill name, category, purpose, and use cases. One question at a time, with a recommendation for each. -->
1. **<Step name>.** <what to do and what happens as a result>
## Output format
<!-- Describe the files or artifacts produced. Include paths and how they are created
(copy-fill from template, generated, etc.). State the template used for structured file output.
Example:
Two files produced for every skill, plus optional sub-files if the skill requires them:
- `SKILL.md` — copy-filled from `SKILL-TEMPLATE.md` at `.agents/skills/<name>/SKILL.md`
- `META.md` — copy-filled from `META-TEMPLATE.md` at `.agents/skills/<name>/META.md`
- `scripts/`, `references/`, or `assets/` — created only when needed; each file wired with an explicit step instruction -->
<description of output>
</steps>
<checks>
## Failure handling
<!-- One bullet per failure mode. Lean — no overlap with constraints or process.
Format: condition — action.
Example:
- Template file missing — stop, report the path searched, do not write from memory
- `write-eval` fails or is unavailable — flag, do not mark the skill complete -->
- <failure condition> — <what to do>
## Self-check
<!-- Verifiable checklist the agent runs before declaring the skill complete.
Each item must be checkable, not aspirational.
Example:
- [ ] Overlap check completed before any content was written
- [ ] Trigger description tested against all three cases — all passed before body content was written -->
- [ ] <check>
</checks>

View File

@@ -0,0 +1,92 @@
---
name: write-skill
description: Use when the user wants to author a new skill file or convert an existing placeholder to the canonical authoring standard. Triggers: "write a new skill for X", "create a SKILL.md that does Y", "build a skill to handle Z". Do NOT use when fixing or updating an existing well-formed skill (use upgrade-skill), running existing evals (use write-eval), refactoring application code, or writing documentation for non-skill artifacts.
metadata:
category: factory
model: sonnet
---
<requirements>
## Required inputs
- **Skill name** — kebab-case slug; inferred from user description if not stated explicitly, ask if ambiguous
- **Category** — from the category table in `CATEGORIES.md`; ask if unclear
- **Purpose + use cases** — what the skill does and what tasks it handles; source for the trigger description
- **For placeholder conversions:** existing SKILL.md path — read before writing
Negative trigger cases are NOT a required input. The agent proposes them based on the skill's purpose and adjacent skills found during the overlap scan. The user confirms or refines before trigger testing begins.
## Constraints
- Write two files for every skill: `SKILL.md` at `.agents/skills/<name>/SKILL.md` and `META.md` alongside it
- Frontmatter required fields: `name`, `description`, `metadata.category` — add `allowed-tools` only when the skill has a narrow, well-defined tool surface; add `model:` only when the skill's task complexity warrants a specific model tier (see SKILL-TEMPLATE.md for routing guidance)
- Keep the body under 500 lines — content that explains rather than directs belongs in sub-files, not the body
- Sub-files use three spec-defined optional directories: `scripts/` (executable code), `references/` (on-demand docs), `assets/` (templates, data files, lookup tables); additional files (e.g. `META.md`) are valid at the skill root. File references must be one level deep — no nested chains. Wire each sub-file with an explicit instruction in the step that needs it (e.g. `"See references/lookup.md for error codes"`) — without a wiring instruction the file is never loaded
- Use XML tags only when the body has three or more logical sections and exceeds 500 tokens — default to plain prose
- Test the trigger description against all three cases — explicit, implicit, negative — before writing any body content. Hard gate: a failed case means revise and retest, not proceed
- Check for overlapping skills in `.agents/skills/` before writing anything — if overlap is found, surface it and wait for direction
- For placeholder conversions: read the existing SKILL.md first and remove all stale or outdated content
</requirements>
<steps>
## Process
1. **Scan for overlap.** Check for skills with similar purpose or trigger phrases. If overlap is found, surface it and wait for explicit direction — do not continue.
2. **Grill.** Run a focused grill with the /grill-me skill to reach shared understanding of: skill name, category, purpose, and use cases. One question at a time, with a recommendation for each.
3. **Conflict check.** Spawn a sub-agent: read `docs/ai-constitution.md`, `docs/research/ai-coding-factory/ai-coding-factory-principles.md`, and `docs/notes/factory-integration-decisions.md`, then check the agreed skill purpose and design against all three. Where a factory principle is superseded by an integration decision, the decision takes precedence — do not flag it as a conflict. Return a numbered list of genuine unresolved tensions, or confirm none found. An empty list is a valid result. Hard gate: resolve any findings before proceeding.
4. **Write and test the trigger description.** Using the agreed name, category, purpose, and use cases from the grill, draft `description:`. Propose negative trigger cases based on the skill's purpose and adjacent skills — get explicit user confirmation before running tests. Test all three cases and show per-case PASS/FAIL. A failed case means revise and retest — do not proceed.
5. **Walk through each section.** For each section in `SKILL-TEMPLATE.md`: propose content, state where it comes from, present alternatives if they exist. Wait for explicit human confirmation before moving to the next section.
6. **Copy both templates.** Copy `SKILL-TEMPLATE.md` to `.agents/skills/<name>/SKILL.md`. Copy `META-TEMPLATE.md` to `.agents/skills/<name>/META.md`. Do not modify content yet — copy first, fill second.
7. **Fill both files.** Fill in the copied `SKILL.md` with confirmed section content. Fill in the copied `META.md` with version, updated date, when, source (if applicable), and references (if applicable).
8. **Invoke `write-eval`.** Do not mark the skill complete without an eval file.
9. **Run self-check.** Work through every item in the Self-check section below. Do not proceed until all items pass.
10. **Prompt for HITL.** Ask the user to open a fresh session, trigger the skill, and confirm output before committing.
## Output format
Two files produced for every skill, plus optional sub-files if the skill requires them:
- `SKILL.md` — copy-filled from `SKILL-TEMPLATE.md` at `.agents/skills/<name>/SKILL.md`
- `META.md` — copy-filled from `META-TEMPLATE.md` at `.agents/skills/<name>/META.md`
- `scripts/`, `references/`, or `assets/` — created only when needed; each file wired with an explicit step instruction
For placeholder conversions, `SKILL.md` replaces the existing file entirely — no partial edits.
</steps>
<checks>
## Failure handling
- Template file missing — stop, report the path searched, do not write from memory
- Existing SKILL.md not found for a placeholder conversion — stop, report the path searched
- `write-eval` fails or is unavailable — flag, do not mark the skill complete
## Self-check
- [ ] Overlap check completed before any content was written
- [ ] Conflict check sub-agent ran against constitution and factory principles — findings resolved before any writing began
- [ ] Trigger description tested against all three cases — all passed before body content was written
- [ ] Negative trigger cases confirmed by user before testing
- [ ] Each section confirmed explicitly by user before SKILL.md was written
- [ ] SKILL.md copy-filled from `SKILL-TEMPLATE.md` at correct path
- [ ] `META.md` copy-filled from `META-TEMPLATE.md` at correct path
- [ ] Frontmatter contains `name`, `description`, and `metadata.category`; optional `allowed-tools` and `model:` only where justified
- [ ] Body is under 500 lines
- [ ] If sub-files exist: placed in correct directory type (`scripts/`, `references/`, or `assets/`) and wired with an explicit instruction in the relevant step
- [ ] For placeholder conversions: existing files read, all stale content removed, old directory deleted if renamed
- [ ] `write-eval` invoked — eval file exists at correct path, covers trigger cases (explicit, implicit, negative) and at least one output case
</checks>

View File

@@ -0,0 +1,118 @@
skill_name: plugin-create
trigger_tests:
- id: explicit-create-named-plugin
name: "Explicit trigger — create named plugin"
query: "create a new plugin called security-tools"
should_trigger: true
- id: explicit-scaffold-plugin
name: "Explicit trigger — scaffold plugin"
query: "scaffold a plugin called developer-tools"
should_trigger: true
- id: implicit-add-marketplace-plugin
name: "Implicit trigger — add plugin to marketplace"
query: "add a new plugin to the marketplace for developer tools"
should_trigger: true
- id: implicit-package-skills-into-plugin
name: "Implicit trigger — package skills into a plugin"
query: "I want to package up my skills into a distributable plugin"
should_trigger: true
- id: negative-validate-marketplace
name: "Negative trigger — validate marketplace.json (routes to /marketplace-architect)"
query: "validate my marketplace.json"
should_trigger: false
- id: negative-write-skill
name: "Negative trigger — write a skill (routes to /write-skill)"
query: "write a skill for code review"
should_trigger: false
- id: negative-adopt-external-plugin
name: "Negative trigger — adopt external plugin (routes to /marketplace-architect)"
query: "adopt this external plugin into my marketplace"
should_trigger: false
output_tests:
- id: gate-a-shows-plan
name: "Gate A presents file list and marketplace entry before writing"
type: deterministic
prompt: >
Create a new plugin called marketplace-tools, description: 'Tools for managing the
holocron marketplace', author: Jane Doe, email: jane@example.com,
URL: https://github.com/jane
expected_output: >
The skill presents a plan listing all files that will be written under
plugins/marketplace-tools/, shows the new marketplace.json entry, and
explicitly asks for approval before writing any file.
assertions:
- "Output lists files to be written under plugins/marketplace-tools/"
- "Output includes a marketplace.json entry with name 'marketplace-tools'"
- "Output explicitly asks for approval and does not proceed without it"
- "Output does not contain any written file confirmation before approval is given"
- id: reserved-name-rejected
name: "Reserved plugin name is rejected before Gate A"
type: deterministic
prompt: "Create a new plugin called claude-tools"
expected_output: >
The skill halts before presenting Gate A, reports that 'claude-tools' matches
the reserved name pattern 'claude-*', and asks the user to provide a different name.
assertions:
- "Output does not present Gate A or a file list"
- "Output identifies 'claude-tools' as a reserved name"
- "Output asks the user to provide a replacement name before continuing"
- id: no-skill-stub-generated
name: "No skill stub generated — skills/ is empty with README only"
type: deterministic
prompt: >
Create a new plugin called marketplace-tools, description: 'Tools for managing the
holocron marketplace', author: Jane Doe, email: jane@example.com,
URL: https://github.com/jane. Approve Gate A and Gate B.
expected_output: >
The plugin scaffold does not include any SKILL.md file. The skills/ directory
is present with a README only. The skill directs the user to /write-skill
to add skill content.
assertions:
- "Output does not include creation of any SKILL.md file"
- "Output includes a skills/ directory entry with README.md only"
- "Output mentions /write-skill for adding skill content"
- id: handoff-message-present
name: "Hand-off message directs user to /marketplace-architect"
type: deterministic
prompt: >
Create a new plugin called marketplace-tools, description: 'Tools for managing the
holocron marketplace', author: Jane Doe, email: jane@example.com,
URL: https://github.com/jane. Approve Gate A and Gate B.
expected_output: >
After writing files and running validation, the skill prints a hand-off message
that names the created plugin and instructs the user to run /marketplace-architect.
assertions:
- "Output contains the string '/marketplace-architect'"
- "Output includes the plugin name 'marketplace-tools' in the hand-off message"
- "Output does not auto-invoke /marketplace-architect itself"
- id: full-flow-quality
name: "Full flow — correct sequencing, substitution, and hand-off"
type: llm-rubric
prompt: >
Create a new plugin called developer-tools with description 'Developer productivity
toolkit', author: Alex Smith, email: alex@example.com, URL: https://github.com/alex.
Approve Gate A and Gate B.
expected_output: >
The skill executes the full create-plugin flow: collects all five inputs one at a
time, presents Gate A with a file plan, copies and substitutes the template, presents
Gate B with full file contents, writes files after Gate B approval, updates
marketplace.json, runs validation, and prints a hand-off message.
assertions:
- "All five inputs (plugin name, description, author name, email, URL) were collected individually before Gate A was presented"
- "Gate A clearly listed all files to be written and the new marketplace.json entry, and waited for explicit approval"
- "Gate B showed the full substituted content of every file before writing"
- "None of the strings PLUGIN_NAME, PLUGIN_DESCRIPTION, AUTHOR_NAME, AUTHOR_EMAIL, or AUTHOR_URL appear in the written file output"
- "The validation step either ran claude plugin validate . and reported output, or explicitly stated it was unavailable"
- "The final message directed the user to run /marketplace-architect and included the plugin name 'developer-tools'"

View File

@@ -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"

View File

@@ -0,0 +1,85 @@
skill_name: write-eval
trigger_tests:
- id: explicit-trigger-write-evals
name: "Explicit trigger — write evals"
query: "write evals for this skill"
should_trigger: true
- id: explicit-trigger-create-eval-yaml
name: "Explicit trigger — create eval.yaml"
query: "create eval.yaml for the tdd skill"
should_trigger: true
- id: implicit-trigger-test-coverage
name: "Implicit trigger — test coverage request"
query: "I need test coverage for the grill-me skill"
should_trigger: true
- id: negative-trigger-run-evals
name: "Negative — run evals (runner concern, not writer)"
query: "run my evals"
should_trigger: false
- id: negative-trigger-code-unit-tests
name: "Negative — unit tests for application code"
query: "write unit tests for my Python file"
should_trigger: false
- id: negative-trigger-debug-failing-eval
name: "Negative — debug failing eval"
query: "my eval is failing, help me debug it"
should_trigger: false
output_tests:
- id: deterministic-correct-output-path
name: "Deterministic — eval.yaml written to correct path"
type: deterministic
prompt: "write evals for the tdd skill"
expected_output: "eval.yaml written to .agents/evals/implement/tdd/eval.yaml containing skill_name: tdd"
assertions:
- "Output references the path .agents/evals/implement/tdd/eval.yaml"
- "Output file contains 'skill_name: tdd'"
- id: deterministic-all-five-types-present
name: "Deterministic — eval.yaml contains all five required test types"
type: deterministic
prompt: "create eval.yaml for the grill-me skill"
expected_output: "eval.yaml contains trigger_tests and output_tests sections with all five required test types represented"
assertions:
- "Output contains 'trigger_tests:'"
- "Output contains 'output_tests:'"
- "Output contains at least one entry with 'should_trigger: true'"
- "Output contains at least one entry with 'should_trigger: false'"
- "Output contains at least one entry with 'type: deterministic'"
- "Output contains at least one entry with 'type: llm-rubric'"
- id: deterministic-plan-shown-before-write
name: "Deterministic — test plan presented before file is written"
type: deterministic
prompt: "write evals for the diagnose skill"
expected_output: "Skill presents each proposed test case with its id, type, and query before writing any file, then requests confirmation"
assertions:
- "Response presents each proposed test case individually — showing at minimum the query and test type — before any file is written"
- "Response requests confirmation before proceeding to write"
- id: deterministic-merge-conflict-flagged
name: "Deterministic — conflict flagged in plan on re-run with existing eval"
type: deterministic
prompt: "write evals for the tdd skill — eval.yaml already exists at .agents/evals/implement/tdd/eval.yaml with a test case id 'explicit-trigger-basic'"
expected_output: "Skill identifies the existing eval.yaml, classifies the conflicting case as CONFLICT, and does not write until the user resolves it"
assertions:
- "Response indicates eval.yaml already exists at the target path"
- "Response labels the conflicting test case as CONFLICT or equivalent"
- "Response does not write the file before the user resolves the conflict"
- id: llm-rubric-assertion-quality
name: "LLM rubric — assertions are specific and verifiable"
type: llm-rubric
prompt: "write evals for the write-skill skill"
expected_output: "eval.yaml contains high-quality assertions that are specific, observable, and not vague"
assertions:
- "All assertions describe observable, verifiable conditions — not vague quality claims like 'output is good' or 'the response is helpful'"
- "Trigger test queries reflect realistic user phrasings, not just the exact skill description verbatim"
- "Negative trigger tests target adjacent tasks that share surface-level similarity with the skill's trigger"
- "Deterministic assertions are machine-checkable without LLM inference — presence of strings, path patterns, required sections"

View File

@@ -0,0 +1,69 @@
skill_name: write-skill
trigger_tests:
- id: explicit-trigger-new-skill
name: Explicit — new skill phrase
query: "Write a new skill for handling database migrations"
should_trigger: true
- id: implicit-trigger-no-phrase
name: Implicit — no trigger phrase
query: "I want to add a skill that automates our deploy process"
should_trigger: true
- id: implicit-trigger-conversion
name: Implicit — placeholder conversion
query: "The grill-me skill is a Pocock placeholder, can you convert it to our standard?"
should_trigger: true
- id: negative-trigger-upgrade
name: Negative — existing skill fix
query: "The tdd skill is producing wrong output, fix it"
should_trigger: false
- id: negative-trigger-code-refactor
name: Negative — code refactor
query: "Refactor this module to use the new API client"
should_trigger: false
- id: negative-trigger-write-eval
name: Negative — eval request
query: "Write evals for the diagnose skill"
should_trigger: false
output_tests:
- id: output-has-all-sections
name: All 8 body sections present in order
type: deterministic
prompt: "Write a new skill for linting markdown files, category: implement"
expected_output: A complete SKILL.md containing all 8 required body sections in the prescribed order.
assertions:
- "Output contains '## Role'"
- "Output contains '## When to use / When not to use'"
- "Output contains '## Required inputs'"
- "Output contains '## Constraints'"
- "Output contains '## Process'"
- "Output contains '## Output format'"
- "Output contains '## Failure handling'"
- "Output contains '## Self-check'"
- "Sections appear in this order: ## Role, ## When to use / When not to use, ## Required inputs, ## Constraints, ## Process, ## Output format, ## Failure handling, ## Self-check"
- id: output-path-correct
name: Output path and frontmatter fields correct
type: deterministic
prompt: "Write a new skill for sending Slack notifications on deploy events, category: deploy"
expected_output: A SKILL.md with correct output path stated and all required frontmatter fields present.
assertions:
- "Output contains '.agents/skills/' in the stated output path"
- "Output contains 'metadata:' and 'category:' in frontmatter"
- "Output contains 'version:'"
- "Output contains 'when:'"
- id: output-trigger-tested-before-body
name: Trigger description tested before body content written
type: llm-rubric
prompt: "Write a new skill for summarising pull request diffs"
expected_output: The skill presents a trigger description and tests it against at least 3 cases (explicit, implicit, negative) before proposing or writing any body section content.
assertions:
- "The skill proposes a trigger description and explicitly tests it against an explicit query, an implicit query, and a negative query before writing any body section"
- "The skill walks through each body section individually and seeks confirmation before writing the file"

View File

@@ -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 ]]