diff --git a/.github/workflows/evidence-freshness.yml b/.github/workflows/evidence-freshness.yml new file mode 100644 index 0000000..497ec79 --- /dev/null +++ b/.github/workflows/evidence-freshness.yml @@ -0,0 +1,29 @@ +name: Evidence freshness + +# Fails when a target citation or vendored schema is past the re-verification +# cadence in CONFORMANCE.md. It runs on a schedule rather than on pull +# requests, because its result depends on the date, not on the change. +on: + schedule: + - cron: "0 16 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + freshness: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Set up toolchain + uses: jdx/mise-action@v4 + + - name: Install dependencies + run: npm ci + + - name: Check evidence dates + env: + PLUGINPACK_CHECK_FRESHNESS: "1" + run: npx vitest run tests/evidence-freshness.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 694537f..36c7b48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,7 +34,7 @@ Data flows: **config → discover source → collect/render files → emit per t diff, prune, and validate all derive from it. - `src/cli.ts` — commander CLI: `init`, `build`, `validate`, `diff`, `prune`, - `clean`, `docs`. + `clean`, `install-info`, `docs`. - `src/schema.ts` — **zod schemas are the source of truth for config types**; authoring types use `z.input`, and parsed config uses `z.infer`. The build materializes these aliases as structural declarations without a Zod type diff --git a/CONFORMANCE.md b/CONFORMANCE.md index 8e037da..fc8d01f 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -24,8 +24,9 @@ against a temp fixture via [`bintastic`](https://github.com/scalvert/bintastic). - **claude** — runs `claude plugin validate --strict` against the emitted marketplace and plugin directories when the `claude` CLI is on `PATH` (skipped - otherwise, e.g. CI). This is Anthropic's own validator — the same check their - submission pipeline runs — so it is a genuinely external oracle that tracks + otherwise). CI installs the CLI before running the suite. This is Anthropic's + own validator, the same check their submission pipeline runs, so it is an + external oracle that tracks upstream automatically. A structural golden assertion also pins the exact emitted shape. - **cursor** — validates the emitted `marketplace.json` and `plugin.json` against @@ -71,14 +72,20 @@ against a temp fixture via [`bintastic`](https://github.com/scalvert/bintastic). (re-verified 2026-10-02): a repo-scoped `.agents/plugins/marketplace.json` (`{ name, interface, plugins }`, no `owner` field). The default package is a root `plugin.json` with OpenAI settings under `extensions.com.openai`. - `format: "legacy"` instead emits `.codex-plugin/plugin.json`, where only - `name` is required and `skills`/`hooks`/`mcpServers` are optional pointers; - `validate` detects either layout per plugin. Every marketplace entry must carry + `format: "legacy"` instead emits `.codex-plugin/plugin.json`. + In [Codex rust-v0.146.0's legacy parser](https://github.com/openai/codex/blob/rust-v0.146.0/codex-rs/core-plugins/src/manifest.rs#L34-L287), + every recognized field has a serde default; an omitted or blank `name` falls + back to the plugin directory basename. `version`, `description`, `skills`, + `hooks`, and `mcpServers` are optional. Pluginpack keeps a stricter rule: + an explicit `name` must match the marketplace entry. `validate` detects + either layout per plugin. Every marketplace entry must carry `policy.installation`, `policy.authentication`, and `category`; pluginpack has - no way to infer these, so the base entry stays guess-free and `validateOutput` - errors clearly if an author never supplies them via the per-plugin `entry` - passthrough. An entry's `source` is a bare string only for local plugins (the - only shape pluginpack itself ever emits); a `url`/`git-subdir`/`npm` source + no way to infer these, so the base entry stays guess-free. Build warns and + `validateOutput` errors if an author never supplies them via the per-plugin + `entry` passthrough (or `clients.codex.entry` for a package client). Build + also warns about skill frontmatter that package validation rejects. + An entry's `source` is a bare string only for local plugins (the only shape + pluginpack itself ever emits); a `url`/`git-subdir`/`npm` source added via `entry` is a structured object with an inner `source` discriminator (e.g. `{ source: "git-subdir", url, path, ref }`), validated by shape rather than requiring a local directory to exist. No published JSON Schema exists. @@ -214,19 +221,34 @@ marketplace. Each target's snippet and citation live on its own `PluginTargetDefinition.installSnippet` in `src/targets/.ts`, verified directly against product docs: -| Target | Snippet | Source | Verified | -| ------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ---------- | -| `claude` | `/plugin marketplace add ` then `/plugin install @` (slash-only) | | 2026-07-25 | -| `codex` | `codex plugin marketplace add ` | | 2026-07-25 | -| `copilot` | `copilot plugin marketplace add ` then `copilot plugin install @` | | 2026-07-25 | -| `antigravity` | `git clone plugin-source && cd plugin-source && agy plugin install ` | | 2026-07-25 | -| `cursor` | No CLI equivalent exists. Prints the repo URL, pasted into Dashboard → Plugins → Team Marketplaces → "Import from Repo." | | 2026-07-25 | - -`claude`'s two-step sequence is not symmetric with `codex`/`copilot`'s shell -commands: `/plugin marketplace add` is slash-only inside an active Claude Code -session, with no shell equivalent — but once a marketplace is already added, -`claude plugin install @` does work as a standalone shell -command, surfaced as a secondary `note`. +| Target | Snippet | Source | Verified | +| ------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------- | +| `claude` | `claude plugin marketplace add ` then `claude plugin install @` | | 2026-10-21 | +| `codex` | `codex plugin marketplace add ` | | 2026-10-21 | +| `copilot` | `copilot plugin marketplace add ` then `copilot plugin install @` | | 2026-10-21 | +| `antigravity` | `agy plugin install https://github.com//` | | 2026-10-21 | +| `cursor` | Prints the repo URL, pasted into Dashboard → Plugins & MCPs → Team Marketplaces → "Import from Repo." | | 2026-10-21 | + +The 2026-10-21 refresh corrected both install snippets. Claude's shell +commands were checked against the current command reference and Claude Code +2.1.284's `claude plugin --help` and `claude plugin marketplace --help`. +The old `plugins-reference#cli-commands-reference` URL now redirects to the +manifest reference, so the citation points to the current command reference. +The interactive `/plugin marketplace add ` and +`/plugin install @` forms remain documented. The latter +opens the plugin details for review and scope selection rather than installing +immediately; the snippet's note explains this. + +Antigravity's current CLI docs show the exact command +`agy plugin install https://github.com//` and the local form +`agy plugin install `. `install-info` uses the configured +repository URL. Its note retains clone-then-local-install with the generated +`pluginPath`, because the docs do not specify a GitHub URL subdirectory selector. + +The legacy Codex citation was also corrected on 2026-10-21 against the tagged +OpenAI source described above. The native parser does not require an explicit +`name`; pluginpack's marketplace-matching rule does. Only these three corrected +citations advance their `verifiedAt` dates in this follow-up. `agent-plugins` is the one target with `userConfigurable: false`. The specification defines no marketplace or install command, and installation is @@ -235,6 +257,29 @@ repository URL: , verified 2026-10-02). +## Evidence refresh cadence + +Re-verify every citation and vendored schema at least every 90 days. +The `Evidence freshness` workflow runs `tests/evidence-freshness.test.ts` every +Monday and on demand. It checks every literal `verifiedAt` in `src/` and the +`Verified-at` date in each vendored schema directory's `SOURCE.md`. It fails +when a date is more than 90 days old, is in the future, or is invalid. The date +checks do not run in `npm test`, because their result depends on the day rather +than the change. Run them locally with +`env PLUGINPACK_CHECK_FRESHNESS=1 npx vitest run tests/evidence-freshness.test.ts`. +The tests do not fetch the network. + +To refresh a citation, open its `documentationUrl`, check the claim against the +current product docs, and run the affected target's conformance tests. Update +`verifiedAt` only after that check. Correct the implementation or record a +format conflict here if the upstream behavior changed. + +To refresh a vendored schema, follow the fetch commands in its `SOURCE.md`, +diff the fetched bytes against the pinned copy, and run +`npx vitest run tests/conformance.test.ts` and the freshness command above. +Record the checked commit and update `Verified-at: YYYY-MM-DD` in `SOURCE.md`, +even when the bytes are unchanged. Do not advance dates just to pass CI. + ## Refreshing vendored schemas The Cursor and Agent Plugins schemas are pinned copies. To update them, re-fetch diff --git a/README.md b/README.md index 18ea525..70e44da 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ One source of truth for agent plugins across AI app ecosystems. `pluginpack` is a build tool for compiling portable skills, commands, agents, rules, hooks, assets, and metadata into the native plugin layouts expected by each AI app. It copies files, writes target manifests, and validates generated output; it is not a package manager or publisher.

- pluginpack packages source components into Claude, Cursor, Antigravity, and Copilot plugin targets + pluginpack packages source components into Claude, Cursor, Antigravity, Copilot, Codex, and Agent Plugins targets

## Quick Start @@ -30,16 +30,45 @@ shared/ release-notes/ SKILL.md agents/ - search-assistant.md - assets/ - icon.png + search-assistant.agent.md pluginpack.config.ts ``` +Create `shared/acme/skills/release-notes/SKILL.md`: + + + +```md snippet=readme/quickstart-skill.md +--- +name: release-notes +description: Draft release notes from a list of changes. +--- + +# Release notes + +Group the changes by feature and fix. Write a short note for each group. +``` + +Create `shared/acme/agents/search-assistant.agent.md`. Copilot requires the +`NAME.agent.md` filename; the other targets accept it too: + + + +```md snippet=readme/quickstart-agent.md +--- +name: search-assistant +description: Find source documents that answer a question. +--- + +Search for relevant documents and cite the sources in your answer. +``` + Map that shared source directly into each native plugin output. Add `overrides` only when a target needs files that differ from or do not exist in the shared source. + + ```ts snippet=readme/snippet-02.ts import { defineConfig } from "@gleanwork/pluginpack"; @@ -57,7 +86,6 @@ export default defineConfig({ plugins: { acme: { source: "shared/acme", - overrides: "overrides/cursor/acme", path: "plugins/cursor/acme", }, }, @@ -84,18 +112,28 @@ export default defineConfig({ codex: { outDir: "plugins/codex", plugins: { - acme: { source: "shared/acme" }, + acme: { + source: "shared/acme", + entry: { + policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, + category: "Productivity", + }, + }, }, }, }, }); ``` -Build and validate the generated outputs: +Build and validate every configured target: -```bash +```bash snippet=readme/quickstart-run.bash npx pluginpack build npx pluginpack validate --target cursor +npx pluginpack validate --target claude +npx pluginpack validate --target antigravity +npx pluginpack validate --target copilot +npx pluginpack validate --target codex ``` Claude, Cursor, Antigravity, Copilot, and Codex users install from the generated @@ -264,9 +302,11 @@ Each target compiles the same source into one app's native plugin layout: | `codex` | [OpenAI Codex / ChatGPT plugins](https://developers.openai.com/codex/plugins/build) | `.agents/plugins/marketplace.json`; an [Agent Plugins](#agent-plugins) package per plugin (`plugin.json` + optional `mcp.json`) | | `agent-plugins` | [Agent Plugins 1.0](https://agent-plugins.org/specification) | a portable package per plugin under `plugins//`; no marketplace unless `clients` is set ([one package for several clients](#one-package-for-several-clients)) | -> **Heads up:** targets that write the same marketplace file need distinct `outDir`s (or separate repos): `claude` and `copilot` both write `.claude-plugin/marketplace.json`; `agent-plugins` with `clients.codex` writes the `codex` target's `.agents/plugins/marketplace.json`; and with `clients.copilot` it writes the `copilot` target's `.github/plugin/marketplace.json`. `build` errors on overlapping output paths. +> **Heads up:** targets that write the same marketplace file need distinct `outDir`s (or separate repos): `claude` and `copilot` both write `.claude-plugin/marketplace.json`; `agent-plugins` with `clients.codex` writes the `codex` target's `.agents/plugins/marketplace.json`; and with `clients.copilot` it writes the `copilot` target's `.github/plugin/marketplace.json`. `build` errors on overlapping output paths. Collision checks normalize Unicode to NFC and ignore case on every filesystem: `assets/Logo.png` and `assets/logo.png` collide even on a case-sensitive volume. Rename one file so the output stays portable. -New targets are added from official docs or real plugin examples — not guessed abstractions. +Oracle strength differs by target. See [the conformance oracles](./CONFORMANCE.md#oracles-the-harness-uses): Claude uses its CLI validator, Cursor uses Glean-authored schemas, and Codex and Agent Plugins use official package schemas. Codex and Copilot marketplace checks and Antigravity layout checks use structural assertions against product docs or published examples. + +New targets are added from official docs or real plugin examples, not guessed abstractions. ## Agent Plugins @@ -346,7 +386,9 @@ targets: { - A client-only content kind is selected by default, and allowed in `include`, only when pluginpack places it for a listed client. Today that is `agents` for `copilot`. pluginpack doesn't place `commands`, `rules`, or - `hooks` for any client yet. + `hooks` for any client yet. With `clients.codex`, build warns when it omits + source hooks. Keep the `codex` target if hooks are required, or explicitly + exclude `hooks` to acknowledge the omission. - Copilot's marketplace requires an owner, from `metadata.owner` or `metadata.author`. - `validate` checks every package, then each client marketplace that's @@ -358,6 +400,16 @@ targets: { valid plugin name, `mcp.json` servers, and skill names that follow the [Agent Skills specification](https://agentskills.io/specification) and match their directory. Conforming clients skip a skill that fails these checks. +Build warns about invalid skill frontmatter and missing Codex entry fields; +`validate` reports them as errors. Supply `policy.installation`, +`policy.authentication`, and `category` in `entry` (or `clients.codex.entry`). + +**Moving between `codex` and `agent-plugins` with `clients.codex`:** run +`pluginpack clean --target codex` while the old target is still configured, +before removing it or building its replacement into the same output directory. +This removes the old managed manifest and marketplace without deleting source +files. In the reverse direction, run `pluginpack clean --target agent-plugins` +before removing that target. The `agent-plugins` target does not ship Codex hooks. **Upgrading from 0.11:** the `codex` target's output layout changes. Run `pluginpack build`, and either publish the new layout or set @@ -410,6 +462,36 @@ pluginpack install-info --target claude The repo comes from `targets..repository`, defaulting to `metadata.repository` (an error if neither is set) — the same fallback `updateCheck.repository` uses. Every target today resolves to a real command except `cursor` and `agent-plugins`. `cursor` has no CLI equivalent: it prints the repo URL and a note to paste it into Cursor's Dashboard under Team Marketplaces. `agent-plugins` has no install snippet, because the specification defines no marketplace or install command. See `CONFORMANCE.md`'s "Install-snippet facts" section for the doc citation behind each target's snippet. +### Claude Code + +The [Claude Code command reference](https://code.claude.com/docs/en/plugins/cli-reference) +shows both operations as shell commands: + +```bash +claude plugin marketplace add +claude plugin install @ +``` + +`install-info` prints that shell sequence. Its note retains the interactive +`/plugin marketplace add ` and `/plugin install @` +forms. In a session, `/plugin install` opens the plugin details so you can +review it and choose an install scope. + +### Antigravity + +The [Antigravity CLI docs](https://antigravity.google/docs/plugins?tab=cli) +show this exact GitHub URL command: + +```bash +agy plugin install https://github.com// +``` + +`install-info` substitutes the configured repository URL. The docs also show +`agy plugin install `, but do not specify a GitHub URL +subdirectory selector. To install a generated plugin from a repository +subdirectory, use the clone-then-local-install command in the snippet's note. +That command uses the generated `pluginPath`, including a custom plugin path. + ## Target Overrides When one app needs different or additional content, put it in that emitted @@ -536,7 +618,7 @@ Legacy 0.10 sources may still declare `additionalFiles` in } ``` -The map is destination (emitted plugin root relative) -> source (source plugin relative). Files are emitted verbatim at the plugin root, are tracked as managed output, and support target overrides: a `targets//` file wins for that host. A destination that collides with a component or static file is an error, and `pluginpack` does not build bundles — produce build output before running `pluginpack build` so the declared source paths exist. +The map is destination (emitted plugin root relative) -> source (source plugin relative). Files are emitted at the plugin root after partial substitution for supported text types (see [Partials](#partials)), are tracked as managed output, and support target overrides: a `targets//` file wins for that host. A destination that collides with a component or static file is an error, and `pluginpack` does not build bundles — produce build output before running `pluginpack build` so the declared source paths exist. ## Partials @@ -652,21 +734,28 @@ Keys stripped by config parsing produce a warning that names the config file and **`targets.`** — `` is one of `cursor`, `claude`, `antigravity`, `copilot`, `codex`, `agent-plugins`. -| Field | Type | Required | Meaning | -| ------------------ | ------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `outDir` | string | yes | Output directory for this target, relative to the config root. May be outside the project; it may not sit inside a source path. | -| `format` | `"agent-plugins"` \| `"legacy"` | no | `codex` only. `"legacy"` keeps the `.codex-plugin/plugin.json` layout; default `"agent-plugins"`. | -| `plugins` | record | yes | Emitted plugins, keyed by emitted plugin name (see **`targets..plugins.`**). | -| `marketplaceDir` | string (safe relative) | no | Override the marketplace dir (defaults: `.cursor-plugin` / `.claude-plugin`). | -| `pluginRoot` | string (safe relative) | no | Override the plugin root dir (`claude`; defaults to `plugins`). | -| `version` | string | no | Override the version for this target (defaults to top-level `version`). | -| `manifest` | object | no | Deep-merged into the generated marketplace manifest. An error on `agent-plugins`; use `clients..manifest`. | -| `clients` | `{ codex?, copilot? }` | no | `agent-plugins` only. Clients each package serves; each gets a marketplace. `clients..manifest` is deep-merged into that client's marketplace. | -| `ignoredDiffPaths` | string[] | no | Output-relative paths `diff` ignores (a dir entry ignores everything below it). | -| `repositoryFiles` | string (safe relative) | no | Directory copied recursively into the generated repository root. | -| `rootFiles` | record (safe relative) | no | Legacy 0.10 output path → source path map; migrate to `repositoryFiles`. | -| `updateCheck` | `{ repository? }` | no | Generate a session-start update-check hook (`claude`/`cursor` only; see **Update Check**). | -| `repository` | string | no | Repo this target's output lives in, for `install-info` (defaults to `metadata.repository`). | +| Field | Type | Required | Meaning | +| ------------------ | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `outDir` | string | yes | Output directory for this target, relative to the config root. May be outside the project; it may not sit inside a source path. | +| `format` | `"agent-plugins"` \| `"legacy"` | no | `codex` only. `"legacy"` keeps the `.codex-plugin/plugin.json` layout; default `"agent-plugins"`. | +| `plugins` | record | yes | Emitted plugins, keyed by emitted plugin name (see **`targets..plugins.`**). | +| `marketplaceDir` | string (safe relative) | no | Compatibility field for `cursor` / `claude`: only the fixed host directory (`.cursor-plugin` / `.claude-plugin`) is accepted. Other targets do not use it. | +| `pluginRoot` | string (safe relative) | no | Override the plugin root dir (`claude`; defaults to `plugins`). | +| `version` | string | no | Override the version for this target (defaults to top-level `version`). | +| `manifest` | object | no | Deep-merged into the generated marketplace manifest. An error on `agent-plugins`; use `clients..manifest`. | +| `clients` | `{ codex?, copilot? }` | no | `agent-plugins` only. Clients each package serves; each gets a marketplace. `clients..manifest` is deep-merged into that client's marketplace. | +| `ignoredDiffPaths` | string[] | no | Output-relative paths `diff` ignores (a dir entry ignores everything below it). | +| `repositoryFiles` | string (safe relative) | no | Directory copied recursively into the generated repository root. | +| `rootFiles` | record (safe relative) | no | Legacy 0.10 output path → source path map; migrate to `repositoryFiles`. | +| `updateCheck` | `{ repository? }` | no | Generate a session-start update-check hook (`claude`/`cursor` only; see **Update Check**). | +| `repository` | string | no | Repo this target's output lives in, for `install-info` (defaults to `metadata.repository`). | + +Claude Code and Cursor require their native manifest paths: `.claude-plugin/` +and `.cursor-plugin/`, respectively. Remove a custom `marketplaceDir`; use +`outDir` to move the generated repository instead. Claude's validator reports +`No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json` +for a relocated manifest. [Cursor's reference](https://cursor.com/docs/reference/plugins) +says to place the marketplace at `.cursor-plugin/marketplace.json`. **`targets..plugins.`** diff --git a/assets/pluginpack-flow.svg b/assets/pluginpack-flow.svg index 1458ffe..cada094 100644 --- a/assets/pluginpack-flow.svg +++ b/assets/pluginpack-flow.svg @@ -9,7 +9,7 @@ pluginpack component packaging flow A source repository with shared components is built by pluginpack into native plugin targets for Claude, - Cursor, Antigravity, and Copilot. + Cursor, Antigravity, Copilot, Codex, and Agent Plugins.