diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 000000000..defdb4373 --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,12 @@ +{ + "name": "apache-magpie", + "interface": { "displayName": "Apache Magpie" }, + "plugins": [ + { + "name": "magpie", + "source": { "source": "local", "path": "." }, + "category": "maintenance", + "policy": { "installation": "manual", "authentication": "none" } + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 000000000..f49505bbb --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,76 @@ +{ + "name": "apache-magpie", + "owner": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "description": "Apache Magpie agentic skills for maintaining open-source projects. Install the all-in-one `magpie` plugin, or per-family plugins to load only what you use.", + "plugins": [ + { + "name": "magpie", + "source": ".", + "version": "0.2.0.dev0", + "description": "All Apache Magpie skills (all 10 families; ~21.7k always-on tokens)." + }, + { + "name": "magpie-contributor-growth", + "source": "./plugins/magpie-contributor-growth", + "version": "0.2.0.dev0", + "description": "Apache Magpie contributor-growth family (6 skills)." + }, + { + "name": "magpie-issue", + "source": "./plugins/magpie-issue", + "version": "0.2.0.dev0", + "description": "Apache Magpie issue family (8 skills)." + }, + { + "name": "magpie-mentoring", + "source": "./plugins/magpie-mentoring", + "version": "0.2.0.dev0", + "description": "Apache Magpie mentoring family (4 skills)." + }, + { + "name": "magpie-pairing", + "source": "./plugins/magpie-pairing", + "version": "0.2.0.dev0", + "description": "Apache Magpie pairing family (2 skills)." + }, + { + "name": "magpie-pr-management", + "source": "./plugins/magpie-pr-management", + "version": "0.2.0.dev0", + "description": "Apache Magpie pr-management family (8 skills)." + }, + { + "name": "magpie-release-management", + "source": "./plugins/magpie-release-management", + "version": "0.2.0.dev0", + "description": "Apache Magpie release-management family (10 skills)." + }, + { + "name": "magpie-repo-health", + "source": "./plugins/magpie-repo-health", + "version": "0.2.0.dev0", + "description": "Apache Magpie repo-health family (7 skills)." + }, + { + "name": "magpie-security", + "source": "./plugins/magpie-security", + "version": "0.2.0.dev0", + "description": "Apache Magpie security family (12 skills)." + }, + { + "name": "magpie-setup", + "source": "./plugins/magpie-setup", + "version": "0.2.0.dev0", + "description": "Apache Magpie setup family (9 skills)." + }, + { + "name": "magpie-utilities", + "source": "./plugins/magpie-utilities", + "version": "0.2.0.dev0", + "description": "Apache Magpie utilities family (4 skills)." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 000000000..13d44272e --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,33 @@ +{ + "name": "magpie", + "version": "0.2.0.dev0", + "description": "Apache Magpie — a reusable, governance-agnostic framework of agentic skills for maintaining open-source projects: release management, security triage, PR and issue workflows, contributor growth, and repo health.", + "author": { "name": "Apache Magpie", "url": "https://magpie.apache.org/" }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "keywords": [ + "apache", + "agent-skills", + "maintenance", + "release-management", + "security", + "pr-management", + "open-source" + ], + "skills": "./skills", + "hooks": { + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/check-upgrade.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json new file mode 100644 index 000000000..4b5868917 --- /dev/null +++ b/.codex-plugin/plugin.json @@ -0,0 +1,19 @@ +{ + "name": "magpie", + "version": "0.2.0.dev0", + "description": "Apache Magpie — agentic skills for maintaining open-source projects.", + "skills": "./skills", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "${PLUGIN_ROOT}/hooks/check-upgrade.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48075459d..fde95b865 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -233,6 +233,22 @@ repos: files: ^(\.gitattributes|\.(agents|claude|github|kiro)/skills/.*|skills/.*|projects/_template/.*)$ types_or: [symlink, file] pass_filenames: false + # Marketplace-plugin consistency guard. Validates that every plugin is + # properly defined and in sync with the skills' `family:` frontmatter (the + # source of truth): the all-in-one `magpie` manifest + its upgrade-hook + # script, each per-family manifest (name / skills / description), every + # `.claude-plugin/marketplace.json` entry resolving to a matching, uniquely + # named plugin.json, and each family's skills present as single-hop symlinks + # into the shared skills/ tree. Catches a new skill, a changed family, a stale + # symlink, or a malformed manifest. `--fix` regenerates the family plugins. + - repo: local + hooks: + - id: check-family-plugins + name: check-family-plugins (marketplace plugins vs frontmatter) + language: system + entry: python3 tools/dev/check-family-plugins.py + files: ^(skills/.*/SKILL\.md|plugins/.*|\.claude-plugin/(marketplace|plugin)\.json|\.codex-plugin/plugin\.json|\.agents/plugins/marketplace\.json|(plugin|marketplace)\.json|gemini-extension\.json|apm\.yml|pyproject\.toml|hooks/check-upgrade\.sh)$ + pass_filenames: false # Workspace-membership guard. The single source of truth for # which Python projects get pre-commit hooks + the CI pytest # matrix is the `[tool.uv.workspace] members` array in the root diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..af5373d2c --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,38 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Apache Magpie — Gemini CLI extension context](#apache-magpie--gemini-cli-extension-context) + - [Upgrade prompt](#upgrade-prompt) + + + + + +# Apache Magpie — Gemini CLI extension context + +This context ships with the Apache Magpie Gemini CLI extension. Gemini has no +plugin update/lifecycle hook (unlike Claude Code), so this file is how the +extension prompts you to reconcile after an update. + +## Upgrade prompt + +The Magpie extension declares its version in `gemini-extension.json`. At the +start of a session, compare that `version` against the value you last recorded +in `magpie-plugin-version` under the extension's state directory (create the +file if it does not exist): + +- If the version has **changed** since you last recorded it — the marketplace + updated the extension — tell the user, once and non-intrusively: + + > Apache Magpie was updated to ``. If this repo adopts Magpie, run + > `/magpie-setup upgrade` to reconcile the snapshot, agentic overrides, and + > drift. + + Then record the new version. + +- If the version is unchanged, say nothing. + +Never run the upgrade yourself: `/magpie-setup upgrade` is a guided, +user-confirmed action, and Magpie never mutates an adopter repo unattended. diff --git a/README.md b/README.md index 244a2aa3c..b63e7f7dd 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,11 @@ **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Apache Magpie](#apache-magpie) - - [How adoption works](#how-adoption-works) - - [Adopting the framework](#adopting-the-framework) - - [1. Bootstrap (copy-pasteable shell)](#1-bootstrap-copy-pasteable-shell) - - [2. Skill takeover](#2-skill-takeover) - - [Subsequent contributors](#subsequent-contributors) - - [Drift detection](#drift-detection) + - [Install](#install) + - [Usage](#usage) + - [Update / maintain](#update--maintain) - [Skill families](#skill-families) - [External skill sources](#external-skill-sources) - - [Maintenance](#maintenance) - [Acknowledgements](#acknowledgements) - [Cross-references](#cross-references) @@ -27,155 +23,77 @@ [![Magpie](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/apache/magpie/main/assets/badge.json)](https://magpie.apache.org/) -Apache Magpie provides high-quality recipes for agent-assisted software project -maintenance. +**Adopt a Magpie.** Apache Magpie provides high-quality recipes for +agent-assisted software project maintenance. These recipes allow human maintainers working with AIs to efficiently handle -the repetitive parts of running an open-source project: Issue triage, PR +the repetitive parts of running an open-source project: issue triage, PR review, mentoring contributors, managing security reports, and more. -Magpie is distributed per the [ASF release policy](https://www.apache.org/legal/release-policy.html) -(see [release-distribution](https://infra.apache.org/release-distribution.html) -for the canonical mechanism); you adopt it by pinning a release. - -> [!NOTE] -> A **public skill marketplace is planned but not yet implemented.** -> Today you adopt Magpie directly from the released source, not from a -> marketplace. +Magpie is currently in development for ASF projects + Python Core team +friendlies. Testers welcome! > [!IMPORTANT] > The motivation, scope, and design commitments behind this work > live in [`MISSION.md`](MISSION.md) — the founding mission of the > Apache Magpie Top-Level Project, originally filed as its > establishment proposal. Read that for the *why*; this README is -> the *how* once you've decided to adopt. - -## How adoption works - -The framework uses a **snapshot + agentic-override** adoption -model. An adopter project commits a single skill — -[`setup`](skills/setup/SKILL.md) — -into their repo. That skill manages everything else: - -1. **Snapshot.** `setup` downloads the framework into - a **gitignored** `/.apache-magpie/` directory. - The snapshot is a build artefact, not source — refreshed - by `/magpie-setup upgrade`, never committed. -2. **Symlinks.** `setup` symlinks the framework's - skills (security, pr-management, the rest of setup) under - one canonical home — `.agents/skills/` (the path shared by - Codex, Cursor, Gemini CLI, Copilot, …) — and gives every - other agent dir (`.claude/skills/`, `.github/skills/`, …) a - thin per-skill **relay** symlink pointing back at the - canonical entry. This is the same regardless of how the - adopter previously organised those dirs. The symlinks are - **also gitignored** — they ultimately target the gitignored - snapshot, so they would dangle on a fresh clone before - `/magpie-setup` runs. -3. **Overrides.** Adopter-specific modifications to framework - workflows live as agent-readable markdown under - `/.apache-magpie-overrides/.md`, - **committed** in the adopter repo. The framework's skills - consult those files at run-time and apply the overrides - before executing default behaviour. See - [`docs/setup/agentic-overrides.md`](docs/setup/agentic-overrides.md) - for the contract. - -**No git submodules. No marketplace (yet). No vendored copies of -framework skills.** Just one committed skill (the bootstrap), -a gitignored snapshot, and agent-readable override files. - -## Adopting the framework - -Two phases — a **shell bootstrap** that gets `setup` -into your repo, then the **skill takeover** that wires up the -rest interactively. - -### 1. Bootstrap (copy-pasteable shell) - -Pick an install method and follow the verbatim recipe in -[**`docs/setup/install-recipes.md`**](docs/setup/install-recipes.md): - -| Method | When to use | Reproducibility | -|---|---|---| -| `svn-zip` | Production once ASF official releases ship to `dist.apache.org` (signed + checksummed) | Frozen by version | -| `git-tag` | Pin a specific framework version | Frozen by tag | -| `git-branch` (default `main`) | WIP path — track the framework's `main` directly for the latest unreleased changes. | Tracks tip | - -Each recipe is a single shell block that: - -1. Adds `.apache-magpie/`, `.apache-magpie.local.lock`, and - the framework-skill symlinks to `.gitignore`. -2. Downloads + verifies + extracts the framework into - `.apache-magpie/` (gitignored — build artefact, not - source). -3. Copies the - [`setup`](skills/setup/SKILL.md) - skill into the canonical `.agents/skills/magpie-setup/` and - adds a relay symlink to it from each agent dir you use - (`.claude/skills/magpie-setup`, `.github/skills/magpie-setup`). - -After the recipe completes, the framework snapshot is on -disk and the bootstrap skill is in your repo. - -### 2. Skill takeover - -Tell your agent: **"adopt apache/magpie in my repo"** -(or invoke `/magpie-setup` directly). The skill walks -through the rest: - -- writes `.apache-magpie.lock` (committed) — the project's - pin: install method + URL + ref + verification anchor; -- writes `.apache-magpie.local.lock` (gitignored) — what - this machine actually fetched + when; -- asks which skill families (`security`, `pr-management`) to - symlink in; -- creates the gitignored framework-skill symlinks; -- scaffolds `.apache-magpie-overrides/` (committed) for any - local workflow modifications; -- installs a `post-checkout` git hook so worktrees re-create - runtime state automatically; -- updates your project documentation with a brief mention. - -After the skill finishes, you commit the small, focused -diff — the bootstrap skill, the `.gitignore` entries, the -two lock files (committed + gitignore exclusion for the -local one), the overrides scaffold, the doc note — and you're -done. Open a PR. - -### Subsequent contributors - -Future contributors who clone your repo just say "adopt -Magpie in this repo" (or invoke `/magpie-setup`). -The skill reads `.apache-magpie.lock` (already committed) -and re-installs to the same version your project pinned. No -need to redo the manual recipe — the committed lock is the -project's source-of-truth. - -### Drift detection - -Every framework skill compares the gitignored -`.apache-magpie.local.lock` against the committed -`.apache-magpie.lock` at the top of its run. If they have -drifted (project lead bumped the pin, or the local install -is stale on a `main`-tracking adopter), the skill surfaces -the gap and proposes `/magpie-setup upgrade`. `upgrade` -deletes the gitignored snapshot, re-installs per the -committed pin, refreshes the gitignored symlinks, and -reconciles any agentic overrides — see -[`docs/setup/install-recipes.md`](docs/setup/install-recipes.md) -and -[`skills/setup/upgrade.md`](skills/setup/upgrade.md) -for the full flow. +> the *how* once you've decided to install. + +## Install + +You **adopt** Magpie once — the decision to bring the framework into your +project — while **installation** is how you carry that out. There are two +ways to install: + +**From an agent marketplace (easiest).** Install the skills directly into your +agent — Claude Code, Codex, Copilot, Gemini, Cursor, and more — with nothing +committed to your repository. See +[`docs/setup/marketplaces.md`](docs/setup/marketplaces.md). + +**As a committed snapshot (installed into your repo).** For a project that +adopts Magpie into its own source — pin a release and let `/magpie-setup` +install the snapshot, overrides, and drift detection in the adopter repo: + +1. [Download / pin a release](https://magpie.apache.org/downloads/) +2. Set up the symlinks and git-ignores — see + [`docs/setup/install-recipes.md`](docs/setup/install-recipes.md) +3. Ask your agent to complete the install: `/magpie-setup install` + (`/magpie-setup adopt` is an alias) + +## Usage + +Magpie is used by interacting with your AI agents. You'll use plain-language +prompts like + +> review PR #5193 + +or + +> triage the latest security reports + +or skill calls starting with a slash, like + +> /dependency-audit + +## Update / maintain + +- `/magpie-setup upgrade` — refresh the snapshot to a newer + framework version + reconcile any overrides against the new + framework structure. +- `/magpie-setup verify` — read-only health check (snapshot + intact, symlinks live, `.gitignore` correct, etc.). +- `/magpie-setup override ` — open or + scaffold an override file for a framework skill. ## Skill families -Ten skill families ship in the framework, all at `experimental` or +The following skill families ship in the framework, all at `experimental` or `stable`, and each skill declares its family in a `family:` frontmatter -key. At adoption (and on every upgrade), `/magpie-setup` offers the +key. At install (and on every upgrade), `/magpie-setup` offers the **opt-in** families — and the optional **MCP servers** (`ponymail`, `apache-projects`, `gmail-plaintext`) — in a single install choice; -symlinks for the picked families land in the adopter's skill directory. +symlinks for the picked families land in the skill directory. The two **always-on** families (`setup`, `utilities`) are wired unconditionally and never prompted for. @@ -185,7 +103,7 @@ means and which modes are still proposed vs. shipping today. | Family | Type | Modes | Purpose | Detail | |---|---|---|---|---| -| [**setup**](docs/setup/README.md) | always-on | (infra) | Isolated agent setup, framework adoption + maintenance, shared-config sync. The prerequisite — at minimum the `setup` skill itself runs out of this family. | 9 skills, [`docs/setup/`](docs/setup/) | +| [**setup**](docs/setup/README.md) | always-on | (infra) | Isolated agent setup, framework install + maintenance, shared-config sync. The prerequisite — at minimum the `setup` skill itself runs out of this family. | 9 skills, [`docs/setup/`](docs/setup/) | | **utilities** | always-on | (meta) | Framework meta-skills: author skills (`write-skill`), restructure them (`optimize-skill`), reconcile skill state (`skill-reconciler`), and print a live index (`list-skills`). | 4 skills | | [**security**](docs/security/README.md) | opt-in | Triage, Drafting | 16-step security-issue handling lifecycle — from `security@` import through CVE publication, including state sync. Maintainer-only. | 12 skills, [`docs/security/`](docs/security/) | | [**pr-management**](docs/pr-management/README.md) | opt-in | Triage | Maintainer-facing PR-queue management — triage, stats, deep code review, express-lane merge, stale-sweep, reviewer routing, and pre-first-PR checks. | 8 skills, [`docs/pr-management/`](docs/pr-management/README.md) | @@ -198,7 +116,7 @@ means and which modes are still proposed vs. shipping today. ### External skill sources -Beyond the in-tree families, an adopter can pull a skill or whole family +Skill families or individual skills can be pulled from a **trusted external source** — a repo other than `apache/magpie` that ships Magpie-shaped skills (with their evals and tests). Where a skill directory would sit, a `skills//source.md` **redirect** names a @@ -209,19 +127,6 @@ skill. Nothing is fetched unless the adopter commits the pin — see [`PRINCIPLES.md` §13](PRINCIPLES.md#13-snapshot-plus-override-never-vendored-copies), and [`RFC-AI-0006`](docs/rfcs/RFC-AI-0006.md). -## Maintenance - -After the initial adoption, the same skill handles ongoing -maintenance: - -- `/magpie-setup upgrade` — refresh the snapshot to a newer - framework version + reconcile any overrides against the new - framework structure. -- `/magpie-setup verify` — read-only health check (snapshot - intact, symlinks live, `.gitignore` correct, etc.). -- `/magpie-setup override ` — open or - scaffold an override file for a framework skill. - ## Acknowledgements Apache Magpie was first developed and proven inside **Apache Airflow**, and was diff --git a/apm.yml b/apm.yml new file mode 100644 index 000000000..287ed305c --- /dev/null +++ b/apm.yml @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 +# +# microsoft/apm (Agent Package Manager) package manifest. apm compiles this +# single source to multiple agent clients (Claude, Cursor, Codex, Copilot, +# Gemini). Schema is v0.1 and may change — verify against the current +# https://microsoft.github.io/apm/ before publishing. +name: magpie +version: 0.2.0.dev0 +type: skill +description: >- + Apache Magpie — a reusable, governance-agnostic framework of agentic skills + for maintaining open-source projects: release management, security triage, + PR and issue workflows, contributor growth, and repo health. +author: Apache Magpie +license: Apache-2.0 +homepage: https://magpie.apache.org/ +repository: https://github.com/apache/magpie +keywords: + - apache + - agent-skills + - maintenance + - release-management + - security +includes: + - skills/ diff --git a/docs/index.md b/docs/index.md index c4a85e3e1..f47ad5d3f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -78,7 +78,7 @@ Start with [`setup`](setup/README.md) regardless — it is the prerequisite ever You have an open-source project with an issue tracker and/or PR queue, and you want agent assistance with the mechanical parts. -→ Start with the [README](../README.md#adopting-the-framework) (adoption steps) and [install recipes](setup/install-recipes.md). +→ Start with the [README](../README.md#install) (install steps) and [install recipes](setup/install-recipes.md). ### Security team members @@ -128,7 +128,7 @@ from mailing lists, slack etc. | Pull a skill/family from a trusted external source | [skill-sources/README.md](skill-sources/README.md) | | Extend Magpie (project / org / individual) | [extending.md](extending.md) | | See what skills exist today | [modes.md](modes.md) | -| Adopt in my project | [README → Adopting](../README.md#adopting-the-framework) | +| Install in my project | [README → Install](../README.md#install) | | Set up the secure agent sandbox | [setup/](setup/README.md) | | Understand the security workflow | [security/](security/README.md) | | Know what it costs to run | [mode-economics.md](mode-economics.md) | diff --git a/docs/issue-management/README.md b/docs/issue-management/README.md index 7283ea5c6..0a91550da 100644 --- a/docs/issue-management/README.md +++ b/docs/issue-management/README.md @@ -128,7 +128,7 @@ uv run --project tools/pilot-report-validator pilot-report-validate /` layout, ## Cross-references -- [Top-level README — Adopting the framework](../../README.md#adopting-the-framework) — 3-step bootstrap. +- [Top-level README — Install](../../README.md#install) — 3-step bootstrap. - [`projects/_template/README.md`](../../projects/_template/README.md) — adopter scaffold index, including the PR-management config files. - [`tools/spec-loop/specs/pr-management-family.md`](../../tools/spec-loop/specs/pr-management-family.md) — functional spec: acceptance criteria, validation commands, and known gaps. - [`docs/mentoring/README.md`](../mentoring/README.md) — `pr-management-mentor` family overview. diff --git a/docs/security/README.md b/docs/security/README.md index acd2ae8b1..cf1345f43 100644 --- a/docs/security/README.md +++ b/docs/security/README.md @@ -113,7 +113,7 @@ Optional but commonly needed: ## Cross-references -- [Top-level README — Adopting the framework](../../README.md#adopting-the-framework) — 3-step bootstrap. +- [Top-level README — Install](../../README.md#install) — 3-step bootstrap. - [`docs/prerequisites.md`](../prerequisites.md) — what a security triager / remediation developer / release manager needs installed before invoking any skill. diff --git a/docs/setup/README.md b/docs/setup/README.md index 134c312db..5e95f2886 100644 --- a/docs/setup/README.md +++ b/docs/setup/README.md @@ -108,6 +108,6 @@ file under `~/.claude-config/`). ## Cross-references -- [Top-level README — Adopting the framework](../../README.md#adopting-the-framework) — 3-step bootstrap. +- [Top-level README — Install](../../README.md#install) — 3-step bootstrap. - [`docs/prerequisites.md`](../prerequisites.md) — what each framework skill needs (Claude Code, Gmail MCP, GitHub auth, browser, etc.). diff --git a/docs/setup/marketplaces.md b/docs/setup/marketplaces.md new file mode 100644 index 000000000..32635dade --- /dev/null +++ b/docs/setup/marketplaces.md @@ -0,0 +1,499 @@ + + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Installing Apache Magpie from agent marketplaces](#installing-apache-magpie-from-agent-marketplaces) + - [Two manifest families: Agent Plugins 1.0 and client-specific](#two-manifest-families-agent-plugins-10-and-client-specific) + - [Choosing a plugin: all-in-one vs per-family](#choosing-a-plugin-all-in-one-vs-per-family) + - [Skill names differ by install method](#skill-names-differ-by-install-method) + - [Supported agents](#supported-agents) + - [Claude Code](#claude-code) + - [OpenAI Codex CLI](#openai-codex-cli) + - [VS Code and GitHub Copilot](#vs-code-and-github-copilot) + - [Google Gemini CLI](#google-gemini-cli) + - [Cursor](#cursor) + - [microsoft/apm (multiplexer)](#microsoftapm-multiplexer) + - [Kiro (AWS)](#kiro-aws) + - [OpenCode](#opencode) + - [Not supported](#not-supported) + - [Automatic upgrade detection](#automatic-upgrade-detection) + - [Versioning](#versioning) + - [Verification status](#verification-status) + + + +# Installing Apache Magpie from agent marketplaces + +From 0.2.0, Apache Magpie ships manifests so its skills can be installed +through the plugin/extension mechanisms of the major AI coding agents, +**in addition to** the canonical `/magpie-setup` snapshot adoption (see +[`install-recipes.md`](install-recipes.md)). + +> [!IMPORTANT] +> The marketplace path is a **discovery and trial** channel: it drops the +> 70 skills into your agent so you can use them immediately. It does **not** +> set up the full adoption machinery (the committed pin, the gitignored +> snapshot, drift detection, agentic overrides, or the secure-agent setup). +> For a project that adopts Magpie for real, use `/magpie-setup` — the +> marketplace install and full adoption are complementary, not exclusive. + +> [!NOTE] +> The **canonical release** of Apache Magpie remains the signed source +> artefact on `dist.apache.org` per the +> [ASF release policy](https://www.apache.org/legal/release-policy.html). +> Marketplace entries are a convenience layer that reference the released +> `X.Y.Z` git tag; they are derived from — not a substitute for — the ASF +> source release. + +> [!WARNING] +> **Marketplace/plugin support is still young in most agentic CLIs.** +> [Agent Plugins 1.0](#two-manifest-families-agent-plugins-10-and-client-specific) +> standardised the *package format* in August 2026, but it deliberately +> specifies no install mechanism or marketplace format — so install commands, +> catalog schemas, and the client-specific manifests still change between +> releases (see [Verification status](#verification-status)). +> If a marketplace install breaks, or your agent has no marketplace at all, the +> **non-marketplace install is always available, universal, and portable**: +> adopt Magpie with `/magpie-setup` from either the **signed SVN release** +> (`dist.apache.org`) or the **GitHub repo** (git tag or branch) — see +> [`install-recipes.md`](install-recipes.md). That path is **harness-neutral**: +> it wires the skills into *any* agent's directory via the universal +> `.agents/skills/` layout, so it works on **every** agentic CLI — not only the +> ones with a marketplace. Rule of thumb: use a marketplace for a quick trial +> on a supported agent; use `/magpie-setup` for a stable, portable install. + +## Two manifest families: Agent Plugins 1.0 and client-specific + +Magpie ships **both** of the manifest shapes an agent may look for, because as +of 2026-08 no single one is read by every client. + +**[Agent Plugins 1.0](https://agent-plugins.org/specification)** (published +2026-08-06 by a TSC drawn from Amazon, Cursor, Microsoft, OpenAI, and Vercel; +Google has since joined) is the vendor-neutral standard. A conformant plugin is +a directory with a root [`plugin.json`](../../plugin.json) declaring the +canonical `$schema`, plus skills in `skills//SKILL.md` and — optionally — +MCP servers in a root `mcp.json`. Magpie's skill tree already had exactly that +layout, so conformance needed **no file moves**: the root `plugin.json` is the +only addition. + +**Client-specific manifests** stay alongside it, because the clients that +predate the standard still read their own: + +| Manifest | Read by | Why it is still needed | +|---|---|---| +| [`plugin.json`](../../plugin.json) (root) | VS Code, GitHub Copilot (CLI + app + SDK) | The AP1 manifest. VS Code auto-detects the format from the root manifest and treats the `$schema` value as the AP1 marker | +| [`.claude-plugin/plugin.json`](../../.claude-plugin/plugin.json) | Claude Code (also read by VS Code) | Claude Code documents only this path, and AP1's schema is closed — it has no place for the `hooks` block or the `skills` path | +| [`.codex-plugin/plugin.json`](../../.codex-plugin/plugin.json) | OpenAI Codex CLI | Codex documents this as its plugin entry point, with its own `interface` / `apps` / `hooks` fields | +| [`gemini-extension.json`](../../gemini-extension.json) | Google Gemini CLI | Gemini's extension format is unrelated to AP1; Google has announced support for the standard but not a migration for this file | +| [`apm.yml`](../../apm.yml) | `microsoft/apm` | A cross-client compiler, not a client — its own package schema | + +The manifests do not conflict: they sit at different paths, each client reads +the one it documents, and every one of them points at the same single `skills/` +tree. `tools/dev/check-family-plugins.py` enforces that they all carry the same +version and shared metadata, and that the AP1 manifest stays inside its closed +ten-field schema — a Claude-only key such as `skills` or `hooks` copied into it +is a **fatal** manifest error for an AP1 client, not an ignorable one. + +> [!NOTE] +> **AP1 covers skills and MCP servers only.** It deliberately specifies no +> hooks, agents, commands, or marketplace/registry format. So Magpie's +> `SessionStart` upgrade prompt and its marketplace catalogs remain +> client-specific by necessity, not by choice — see +> [Automatic upgrade detection](#automatic-upgrade-detection). + +## Choosing a plugin: all-in-one vs per-family + +The framework ships as **eleven** plugins. You can install **either** the +all-in-one plugin **or** any number of per-family plugins — and you can mix +several families. Pick based on the trade-off between install simplicity and +always-on token cost (each installed skill advertises a short description to +the model on **every** turn — see ["always-on" cost](#versioning) below). + +**All-in-one — `magpie`** + +- ✅ One install; all 70 skills; nothing to decide. Uses the real `skills/` + directory, so **no symlinks** — works on Windows out of the box. +- ⚠️ Adds **~21.7k always-on tokens to every session**, including families you + may never use — that context (and cost) is spent whether or not you invoke a + Magpie skill that turn. +- Best when you genuinely want everything, or you're on Windows without symlink + support. + +**Per-family — `magpie-`** *(recommended)* + +- ✅ Install only the families you use, so the always-on cost is proportional + (`magpie-security` ≈ 3.9k, `magpie-pairing` ≈ 0.6k). Install several to mix + and match. +- ⚠️ You manage a few installs instead of one; adding a family later is a + separate install; relies on git symlinks (see the Windows note below). +- Best for day-to-day use where you want a lean context window. + +Mixing is fine — e.g. install `magpie-release-management` + `magpie-security` +and nothing else. The two are **not** exclusive with the all-in-one either, but +installing both `magpie` *and* a family plugin just double-loads those skills, +so pick one approach. + +| Family plugin | Skills | ~Always-on tokens | +|---|---|---| +| `magpie-security` | 12 | ~3.9k | +| `magpie-release-management` | 10 | ~2.9k | +| `magpie-setup` | 9 | ~2.6k | +| `magpie-pr-management` | 8 | ~2.4k | +| `magpie-issue` | 8 | ~2.4k | +| `magpie-repo-health` | 7 | ~2.1k | +| `magpie-contributor-growth` | 6 | ~1.8k | +| `magpie-utilities` | 4 | ~1.4k | +| `magpie-mentoring` | 4 | ~1.2k | +| `magpie-pairing` | 2 | ~0.6k | +| **`magpie`** (all) | **70** | **~21.7k** | + +Skills are invoked under the installing plugin's namespace — e.g. +`/magpie:release-vote-tally` (all-in-one) or +`/magpie-release-management:release-vote-tally` (family plugin). + +Per-family plugins reference the shared `skills/` tree via symlinks (no copies), +so there is a single source of truth for every skill. + +> [!IMPORTANT] +> **Windows + per-family plugins.** The per-family plugins rely on git symlinks +> (each `plugins/magpie-/skills/` links to the shared +> `skills/`). Git for Windows does **not** materialise real symlinks +> unless `core.symlinks` is enabled *and* the account may create them (Windows +> Developer Mode, or an elevated shell) — otherwise the clone writes each +> symlink as a plain text file and that family's skills won't load. On Windows, +> either enable symlink support +> (`git config --global core.symlinks true` + Developer Mode) **or** install the +> **all-in-one `magpie` plugin**, which uses the real `skills/` directory and +> needs no symlinks. macOS and Linux are unaffected. (Verified on macOS: a +> `/plugin marketplace add` GitHub clone preserves and resolves the symlinks.) + +> [!IMPORTANT] +> **The per-family plugins are not Agent Plugins 1.0 packages.** AP1 requires a +> symlink's final target to resolve *inside* the plugin root, and each family +> plugin's `skills/` deliberately points out of its own root at the +> shared `../../../skills/` tree. Materialising them as real directories +> would mean vendored copies of every skill — which +> [PRINCIPLES §13](../../PRINCIPLES.md) rules out, and which would leave eleven +> divergent copies to keep in sync. So the families stay a **Claude Code** +> feature (Claude Code resolves the symlinks, as verified above), and AP1 +> clients install the **all-in-one `magpie` plugin**, whose `skills/` *is* the +> real tree and needs no symlink at all. If per-family granularity on AP1 +> clients turns out to be worth its cost, the way to get it is to generate +> materialised family directories as a **release artefact** rather than commit +> them — deliberately deferred, not overlooked. + +## Skill names differ by install method + +The **same skill** is invoked by a **different name** depending on how you +installed it. The portable `/magpie-setup` install bakes a `magpie-` prefix into +each skill's name (so framework skills never collide with your own); the +marketplace plugins namespace with `plugin:skill` and keep the bare skill name. + +| Skill (directory) | Portable — `/magpie-setup` snapshot | Marketplace — all-in-one `magpie` | Marketplace — family plugin | +|---|---|---|---| +| `release-vote-tally` | `/magpie-release-vote-tally` | `/magpie:release-vote-tally` | `/magpie-release-management:release-vote-tally` | +| `security-issue-triage` | `/magpie-security-issue-triage` | `/magpie:security-issue-triage` | `/magpie-security:security-issue-triage` | +| `setup` | `/magpie-setup` | `/magpie:setup` | `/magpie-setup:setup` | + +Why the difference: + +- **Portable install** (`/magpie-setup` snapshot) — the `setup` skill symlinks + each framework skill under a `magpie-` entry (e.g. + `skills/release-vote-tally/` → `magpie-release-vote-tally`), and the skill's + own frontmatter `name:` carries the same `magpie-` prefix. It is therefore + invoked as a **single hyphenated token**, `/magpie-`. The prefix *is* the + namespace — it keeps framework skills from clashing with the adopter's own + skills. +- **Marketplace install** — the **plugin name** is the namespace, applied with a + **colon**: `/:`. The `magpie-` frontmatter prefix is ignored + (the plugin already namespaces), so the skill keeps its bare directory name. + With the all-in-one plugin that's `/magpie:`; with a family plugin it's + `/magpie-:`. + +Throughout this repo's own docs and skills, cross-references use the +**portable** form (`/magpie-`), because that is the canonical install. +When you install via a marketplace, translate `/magpie-` to +`/:` (drop the `magpie-` prefix, add the plugin namespace). + +## Supported agents + +Every method below uses the **GitHub repository +[`apache/magpie`](https://github.com/apache/magpie)** as the marketplace — +no third-party or vendor "official" directory is required. Pin to a released +tag (e.g. `0.2.0`) for reproducibility, or track `main` for the latest. + +Quick reference: + +| Agent | One-liner | Manifest in this repo | +|---|---|---| +| **Claude Code** | `/plugin marketplace add apache/magpie` → `/plugin install magpie@apache-magpie` | `.claude-plugin/marketplace.json` + `.claude-plugin/plugin.json` | +| **OpenAI Codex CLI** | `codex plugin marketplace add apache/magpie` → install `magpie` | `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json` | +| **VS Code / GitHub Copilot** | install straight from the repo URL `https://github.com/apache/magpie`, or add it as a plugin marketplace | root `plugin.json` (AP1), `marketplace.json` (repo root) | +| **Google Gemini CLI** | `gemini extensions install https://github.com/apache/magpie` | `gemini-extension.json` | +| **Cursor** | add via the plugin/skill install flow pointing at the repo | root `plugin.json` (AP1) | +| **microsoft/apm** | `apm install apache/magpie` (compiles to Claude/Cursor/Codex/Copilot/Gemini) | `apm.yml` | +| **Kiro** | install per-skill from a GitHub subdirectory, or the AP1 package | root `plugin.json` (AP1), native `skills//SKILL.md` | +| **OpenCode** | clone skills into `.opencode/skills/`, or use a community installer | native `skills//SKILL.md` | + +Detailed steps per agent follow. + +### Claude Code + +1. In a Claude Code session, add the marketplace from GitHub — this clones + the repo and reads `.claude-plugin/marketplace.json`: + + ```text + /plugin marketplace add apache/magpie + ``` + +2. Install the all-in-one plugin, **or** just the families you use: + + ```text + /plugin install magpie@apache-magpie # everything (~21.7k always-on) + /plugin install magpie-security@apache-magpie # one family (~3.9k always-on) + /plugin install magpie-release-management@apache-magpie + ``` + +3. Confirm it is enabled (the `magpie` plugin should appear as installed): + + ```text + /plugin + ``` + +4. Invoke any skill under the plugin namespace, e.g.: + + ```text + /magpie:release-vote-tally + /magpie:security-issue-triage + ``` + +5. **Update** later with `/plugin marketplace update apache-magpie` then + `/plugin update magpie@apache-magpie`. On a version change the bundled + `SessionStart` hook also prompts you to run `/magpie-setup upgrade`. + +To pin a specific version instead of tracking `main`, add the marketplace +from the tag: `/plugin marketplace add apache/magpie@0.2.0`. + +### OpenAI Codex CLI + +1. Add the marketplace (reads `.agents/plugins/marketplace.json`): + + ```bash + codex plugin marketplace add apache/magpie + ``` + +2. Install the plugin: + + ```bash + codex plugin install magpie + ``` + +3. List / verify — inside Codex run `/plugins`, or from the shell + `codex plugin list`. + +Only the **all-in-one** `magpie` plugin is offered here — the per-family +plugins are Claude Code-only, for the reason recorded +[above](#choosing-a-plugin-all-in-one-vs-per-family). The catalog is checked +against that rule by `tools/dev/check-family-plugins.py`, so it cannot drift +into advertising a plugin Codex could not install. + +> Codex's plugin/marketplace verbs are still evolving. If a command name +> differs, check `codex plugin --help`. + +### VS Code and GitHub Copilot + +Agent Plugins 1.0 support is generally available in VS Code, Copilot CLI, the +Copilot app, and the Copilot SDK on all Copilot plans. VS Code auto-detects the +plugin format from the root manifest, and Magpie's root +[`plugin.json`](../../plugin.json) declares the AP1 `$schema`, so it is loaded +as an AP1 package. Two ways in: + +1. **Straight from the repo URL** — no marketplace needed. Point VS Code's + plugin install at: + + ```text + https://github.com/apache/magpie + ``` + + VS Code clones the repo and installs the plugin. + +2. **As a marketplace** — add `apache/magpie` as a plugin marketplace (CLI or + the coding-agent settings) and install `magpie` from it. That path reads the + root [`marketplace.json`](../../marketplace.json). + +Either way the 70 skills become available to the agent under the plugin. As +with Codex, only the **all-in-one** `magpie` plugin is offered — the per-family +plugins are Claude Code-only, for the reason recorded +[above](#choosing-a-plugin-all-in-one-vs-per-family). + +> [!NOTE] +> VS Code **ignores client extension data and directories** in an AP1 package. +> Magpie's `.claude-plugin/` hook block is therefore inert here — the upgrade +> prompt is Claude Code-only (see +> [Automatic upgrade detection](#automatic-upgrade-detection)). Existing +> Copilot plugins that do not target AP1 remain supported, so the root +> `marketplace.json` keeps working regardless. + +### Google Gemini CLI + +1. Install the extension straight from GitHub (reads `gemini-extension.json` + and auto-discovers the skills under `skills/`): + + ```bash + gemini extensions install https://github.com/apache/magpie + ``` + +2. Verify: + + ```bash + gemini extensions list + ``` + +3. Use the skills by asking the agent in natural language or by skill name. + +4. **Update** with `gemini extensions update magpie`. Gemini has no lifecycle + hook, so the shipped [`GEMINI.md`](../../GEMINI.md) reminds you to run + `/magpie-setup upgrade` when the version changes. + +### Cursor + +Cursor is one of the Agent Plugins 1.0 launch clients (and sits on the spec's +TSC), so it reads the root [`plugin.json`](../../plugin.json). Add Magpie +through Cursor's plugin/skill install flow (Customize → Plugins/Skills) +pointing at `github.com/apache/magpie`. + +> Confirm the exact add flow in Cursor's current docs — its self-serve +> marketplace surface is evolving. + +### microsoft/apm (multiplexer) + +`apm` compiles one package to several agents at once (Claude, Cursor, Codex, +Copilot, Gemini). + +1. From your project root: + + ```bash + apm install apache/magpie + ``` + + (reads `apm.yml`, `type: skill`). + +2. `apm` deploys the skills into each supported agent's directory and writes + an `apm.lock.yaml` — commit it to pin the exact resolved commit. + +> `apm` schema is **v0.1** and may change; verify verbs with `apm --help`. + +### Kiro (AWS) + +Kiro installs skills **per-skill from a GitHub subdirectory** (it does not +consume the repo root). For each skill you want, point Kiro's "install from +GitHub" at that skill's subdir on a pinned tag, e.g.: + +```text +https://github.com/apache/magpie/tree/0.2.0/skills/release-vote-tally +``` + +Kiro reads the `skills//SKILL.md` there. + +### OpenCode + +OpenCode reads native Agent Skills from `.opencode/skills/`. Either: + +- clone the skill directories you want into `.opencode/skills/` (project) or + `~/.opencode/skills/` (personal) from `github.com/apache/magpie`, or +- use a community installer (e.g. the `opencode-skills-collection` npm + package) pointed at this repo. + +### Not supported + +- **Windsurf** — has no skills/rules marketplace; project rules are plain + `.windsurfrules` files with no install mechanism. Skills would have to be + converted by hand; there is no distribution channel. +- **Goose (Block)** — its extension registry is Model Context Protocol + (MCP) servers, not `SKILL.md` skills. Distributing Magpie there would + require wrapping skills behind an MCP server (a rebuild, not packaging). + +## Automatic upgrade detection + +When the marketplace updates the plugin to a new version, Magpie prompts you +to run **`/magpie-setup upgrade`** — which reconciles the gitignored snapshot, +the agentic overrides, and drift. This is **detect-and-prompt, not auto-run**: +a plugin hook cannot invoke a slash command, and Magpie never mutates an +adopter repo without the guided skill's confirmation, so the *trigger* is +automatic while the *changes* stay confirmed. + +| Agent | Mechanism | +|---|---| +| **Claude Code** | `SessionStart` hook [`hooks/check-upgrade.sh`](../../hooks/check-upgrade.sh) compares the installed version to a marker in the plugin's persistent data dir and prompts on change. Deterministic. | +| **Codex CLI** | The same [`hooks/check-upgrade.sh`](../../hooks/check-upgrade.sh), wired inline via the plugin's `hooks` block — Codex uses the same event schema and the same `SessionStart` event. Codex sets `PLUGIN_ROOT`/`PLUGIN_DATA` (and the `CLAUDE_*` pair for compatibility), which the script reads. See the caveat below. | +| **VS Code / Copilot, Cursor, Kiro (AP1)** | None. Agent Plugins 1.0 specifies no hook component and VS Code ignores client extension directories, so there is nothing to fire. Re-run `/magpie-setup upgrade` after updating. | +| **Gemini CLI** | No lifecycle hook; the extension context file [`GEMINI.md`](../../GEMINI.md) instructs the agent to compare the extension version to a recorded marker and prompt on change (LLM-driven, advisory). | +| Other agents | Re-run `/magpie-setup upgrade` manually after updating the package. | + +> [!WARNING] +> **Codex plugin-local hooks may not fire yet.** [openai/codex#16430](https://github.com/openai/codex/issues/16430) +> reports that the runtime executes only the global `hooks.json` even though the +> plugin docs describe plugin-local hooks. The manifest is written to the +> documented schema so it starts working when the runtime catches up; until +> then, treat the Codex upgrade prompt as best-effort and re-run +> `/magpie-setup upgrade` manually. + +The hook writes its prompt to **stdout**, which is what a `SessionStart` hook +exiting 0 has added to the session context — stderr on a zero exit reaches only +the debug log. It is read-only apart from writing its own version marker, which +goes to the client-provided persistent data directory (`CLAUDE_PLUGIN_DATA` / +`PLUGIN_DATA`), falling back to `$XDG_STATE_HOME/magpie` — never inside the +plugin checkout, which a plugin update may replace wholesale. It makes no +network calls and touches nothing in the adopter repo. + +## Versioning + +The plugin version tracks the framework version in `pyproject.toml`, which is +the single authority every manifest mirrors verbatim — **including the `.devN` +suffix**. Between releases the manifests therefore read `0.2.0.dev0`, not +`0.2.0`: a bare `0.2.0` would advertise a release that does not exist yet. Only +a tagged release carries a bare version, and only released versions are ever +published to a marketplace, so the PEP 440 suffix never reaches a consumer. + +Nothing is hand-edited. `pyproject.toml` feeds the four ecosystem manifests, +and the all-in-one [`.claude-plugin/plugin.json`](../../.claude-plugin/plugin.json) +in turn feeds the ten per-family manifests and the marketplace entries, which +also inherit `author`, `homepage`, `repository`, and `license`. Bump +`project.version` and run `python3 tools/dev/check-family-plugins.py --fix`; the +same script, run as a prek hook, fails the build on any manifest left behind at +the old version. See +[`release-management-config.md`](../../projects/magpie/release-management-config.md) +(`version_manifest_files`). + +## Verification status + +Every manifest here has been checked against the vendor's **published +documentation**; what varies is whether it has also been exercised against a +**live install**. + +| Manifest | Schema source | Status | +|---|---|---| +| root `plugin.json` | [Agent Plugins 1.0.0 spec](https://github.com/agentplugins/agent-plugins-spec/blob/main/spec/1.0.0.md) + [`plugin.schema.json`](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) | Conforms to the published closed schema; enforced by `check-family-plugins.py`. Not yet live-installed | +| `.claude-plugin/*` | Claude Code plugins reference | Verified live — `claude plugin validate . --strict` passes with 0 warnings; a family plugin installs and loads from a local marketplace replica | +| `.codex-plugin/plugin.json`, `.agents/plugins/marketplace.json` | Codex plugin docs (`Package your plugin`) | Matches the documented entry point, field set, and repo-marketplace path. Not yet live-installed; see the plugin-local hooks caveat above | +| root `marketplace.json` | Copilot / VS Code plugin marketplace docs | Legacy-format catalog, explicitly still supported alongside AP1. Not yet live-installed | +| `gemini-extension.json` | Gemini CLI extensions docs | Follows the published schema. Google has joined the AP1 TSC but has published no migration for this file — keep both | +| `apm.yml` | `microsoft/apm` schema **v0.1** | Pre-1.0 and the most likely to churn; re-check before publish | + +The skills themselves are checked against the +[Agent Skills specification](https://agentskills.io/specification), which AP1 +defers to. Worth stating explicitly, because it looks like a problem and is +not: 41 of the 70 `description` fields contain the framework's +`` syntax (``, ``, …). The spec constrains +`description` on **length only** — 1–1024 characters, non-empty — and places no +restriction on angle brackets; the character-class rules apply to `name`, which +every skill satisfies. So the placeholders are conformant, not a portability +risk to design around. + +Re-check each against the vendor's current documentation before a marketplace +publish. Manifests that fail live validation should be fixed here and +re-released — none of them change how the ASF source release is built or +signed. diff --git a/docs/setup/unadopt.md b/docs/setup/unadopt.md index 6e8ff62dd..0443643c1 100644 --- a/docs/setup/unadopt.md +++ b/docs/setup/unadopt.md @@ -237,7 +237,7 @@ a first-time adopter takes. - [`docs/setup/README.md`](README.md) — the setup skill family overview (verify, upgrade, shared-config sync). -- [Top-level README — Adopting the framework](../../README.md#adopting-the-framework) +- [Top-level README — Install](../../README.md#install) — the original 3-step bootstrap. - [`install-recipes.md`](install-recipes.md) — the counterpart to this page. diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 000000000..5b5e3c620 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,6 @@ +{ + "name": "magpie", + "version": "0.2.0.dev0", + "description": "Apache Magpie — agentic skills for maintaining open-source projects. Skills are auto-discovered from ./skills.", + "contextFileName": "GEMINI.md" +} diff --git a/hooks/check-upgrade.sh b/hooks/check-upgrade.sh new file mode 100755 index 000000000..e6cadf416 --- /dev/null +++ b/hooks/check-upgrade.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Session-start hook for the Apache Magpie plugin. Wired by the Claude Code +# plugin (SessionStart) and by the Codex CLI plugin, which uses the same event +# schema. Agent Plugins 1.0 defines no hook component, so an AP1-only client +# does not run this at all — see docs/setup/marketplaces.md. +# +# Detects when the installed plugin version has changed since the last +# session (i.e. the marketplace updated it) and prompts the user to run +# `/magpie-setup upgrade`, which reconciles the gitignored snapshot, the +# agentic overrides, and drift. +# +# Deliberately DETECT-AND-PROMPT, not auto-run: a plugin hook cannot invoke +# a slash command, and Magpie never mutates an adopter repo without the +# guided skill's confirmation. The hook is read-only apart from writing its +# own version marker; it makes no network calls and touches nothing in the +# adopter repo. +# +# Agents without a session hook (e.g. Gemini CLI) surface the same prompt via +# their extension context file (GEMINI.md) instead. +set -euo pipefail + +# Drain any event JSON delivered on stdin (unused). +cat >/dev/null 2>&1 || true + +# Claude Code exports `CLAUDE_PLUGIN_ROOT` / `CLAUDE_PLUGIN_DATA` to hook +# processes; Codex exports `PLUGIN_ROOT` / `PLUGIN_DATA` (the Agent Plugins 1.0 +# names) *and* the `CLAUDE_*` pair for compatibility. There is no +# `CODEX_PLUGIN_ROOT`. +root="${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-.}}" + +# The final fallback is the XDG state dir — never a path inside `$root`, which +# for a `marketplace add` install (or this repo's own local marketplace) is a +# git working tree, and which a plugin update may replace wholesale. +data="${CLAUDE_PLUGIN_DATA:-${PLUGIN_DATA:-${XDG_STATE_HOME:-$HOME/.local/state}/magpie}}" + +# Read the plugin version from whichever manifest is present. The root +# `plugin.json` is the Agent Plugins 1.0 manifest; the two client-specific +# manifests carry the same version string (enforced by +# `tools/dev/check-family-plugins.py`), so the order only matters for which +# file is read, not for the value. +manifest="" +for m in "$root/.claude-plugin/plugin.json" "$root/.codex-plugin/plugin.json" "$root/plugin.json"; do + [ -f "$m" ] && { manifest="$m"; break; } +done +[ -n "$manifest" ] || exit 0 + +current="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$manifest" | head -n1)" +[ -n "$current" ] || exit 0 + +marker="$data/magpie-plugin-version" +stored="" +[ -f "$marker" ] && stored="$(cat "$marker" 2>/dev/null || true)" + +if [ "$current" != "$stored" ]; then + mkdir -p "$data" 2>/dev/null || true + printf '%s' "$current" >"$marker" 2>/dev/null || true + + # stdout, not stderr: for a `SessionStart` hook exiting 0, Claude Code adds + # stdout to the session context (stderr on a zero exit only reaches the debug + # log). Writing the prompt to stderr would deliver it to nobody — and because + # the marker is written first, the next session would see no change and stay + # silent too. + if [ -n "$stored" ]; then + echo "Apache Magpie plugin updated ($stored -> $current). If this repo adopts Magpie, run \`/magpie-setup upgrade\` to reconcile the snapshot, agentic overrides, and drift." + else + echo "Apache Magpie plugin $current is active. If this repo already adopts Magpie, run \`/magpie-setup upgrade\` to reconcile; otherwise run \`/magpie-setup\` to adopt." + fi +fi + +exit 0 diff --git a/marketplace.json b/marketplace.json new file mode 100644 index 000000000..798cfcb6e --- /dev/null +++ b/marketplace.json @@ -0,0 +1,12 @@ +{ + "name": "apache-magpie", + "owner": { "name": "Apache Magpie", "url": "https://magpie.apache.org/" }, + "description": "Apache Magpie agentic skills for maintaining open-source projects (GitHub Copilot plugin marketplace catalog).", + "plugins": [ + { + "name": "magpie", + "source": ".", + "description": "All Apache Magpie skills (release management, security, PR/issue workflows, contributor growth, repo health)." + } + ] +} diff --git a/plugin.json b/plugin.json new file mode 100644 index 000000000..ea3036de6 --- /dev/null +++ b/plugin.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "magpie", + "version": "0.2.0.dev0", + "description": "Apache Magpie — a reusable, governance-agnostic framework of agentic skills for maintaining open-source projects: release management, security triage, PR and issue workflows, contributor growth, and repo health.", + "author": { "name": "Apache Magpie", "url": "https://magpie.apache.org/" }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "keywords": [ + "apache", + "agent-skills", + "maintenance", + "release-management", + "security", + "pr-management", + "open-source" + ] +} diff --git a/plugins/magpie-contributor-growth/.claude-plugin/plugin.json b/plugins/magpie-contributor-growth/.claude-plugin/plugin.json new file mode 100644 index 000000000..3bcf85b2f --- /dev/null +++ b/plugins/magpie-contributor-growth/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-contributor-growth", + "description": "Apache Magpie \u2014 path-to-committer: activity sweeps, nominations, sentiment, readiness, committer/post-vote onboarding.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-contributor-growth/skills/committer-onboarding b/plugins/magpie-contributor-growth/skills/committer-onboarding new file mode 120000 index 000000000..b83c90500 --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/committer-onboarding @@ -0,0 +1 @@ +../../../skills/committer-onboarding \ No newline at end of file diff --git a/plugins/magpie-contributor-growth/skills/contributor-activity-sweep b/plugins/magpie-contributor-growth/skills/contributor-activity-sweep new file mode 120000 index 000000000..fb487ce3e --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/contributor-activity-sweep @@ -0,0 +1 @@ +../../../skills/contributor-activity-sweep \ No newline at end of file diff --git a/plugins/magpie-contributor-growth/skills/contributor-nomination b/plugins/magpie-contributor-growth/skills/contributor-nomination new file mode 120000 index 000000000..2124598e5 --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/contributor-nomination @@ -0,0 +1 @@ +../../../skills/contributor-nomination \ No newline at end of file diff --git a/plugins/magpie-contributor-growth/skills/contributor-sentiment b/plugins/magpie-contributor-growth/skills/contributor-sentiment new file mode 120000 index 000000000..1831aa68c --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/contributor-sentiment @@ -0,0 +1 @@ +../../../skills/contributor-sentiment \ No newline at end of file diff --git a/plugins/magpie-contributor-growth/skills/contributor-to-committer b/plugins/magpie-contributor-growth/skills/contributor-to-committer new file mode 120000 index 000000000..b8094a483 --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/contributor-to-committer @@ -0,0 +1 @@ +../../../skills/contributor-to-committer \ No newline at end of file diff --git a/plugins/magpie-contributor-growth/skills/onboarding-concierge b/plugins/magpie-contributor-growth/skills/onboarding-concierge new file mode 120000 index 000000000..91029b53f --- /dev/null +++ b/plugins/magpie-contributor-growth/skills/onboarding-concierge @@ -0,0 +1 @@ +../../../skills/onboarding-concierge \ No newline at end of file diff --git a/plugins/magpie-issue/.claude-plugin/plugin.json b/plugins/magpie-issue/.claude-plugin/plugin.json new file mode 100644 index 000000000..6bf44f247 --- /dev/null +++ b/plugins/magpie-issue/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-issue", + "description": "Apache Magpie \u2014 issue lifecycle: triage, reproduction, fix drafting, reassess, stale-sweep, dedup, backlog stats.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-issue/skills/issue-backlog-stats b/plugins/magpie-issue/skills/issue-backlog-stats new file mode 120000 index 000000000..d12cad954 --- /dev/null +++ b/plugins/magpie-issue/skills/issue-backlog-stats @@ -0,0 +1 @@ +../../../skills/issue-backlog-stats \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-deduplicate b/plugins/magpie-issue/skills/issue-deduplicate new file mode 120000 index 000000000..c6367f27b --- /dev/null +++ b/plugins/magpie-issue/skills/issue-deduplicate @@ -0,0 +1 @@ +../../../skills/issue-deduplicate \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-fix-workflow b/plugins/magpie-issue/skills/issue-fix-workflow new file mode 120000 index 000000000..8914695dd --- /dev/null +++ b/plugins/magpie-issue/skills/issue-fix-workflow @@ -0,0 +1 @@ +../../../skills/issue-fix-workflow \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-reassess b/plugins/magpie-issue/skills/issue-reassess new file mode 120000 index 000000000..c5f4026ee --- /dev/null +++ b/plugins/magpie-issue/skills/issue-reassess @@ -0,0 +1 @@ +../../../skills/issue-reassess \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-reassess-stats b/plugins/magpie-issue/skills/issue-reassess-stats new file mode 120000 index 000000000..35fc4ffe0 --- /dev/null +++ b/plugins/magpie-issue/skills/issue-reassess-stats @@ -0,0 +1 @@ +../../../skills/issue-reassess-stats \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-reproducer b/plugins/magpie-issue/skills/issue-reproducer new file mode 120000 index 000000000..1deca5bfd --- /dev/null +++ b/plugins/magpie-issue/skills/issue-reproducer @@ -0,0 +1 @@ +../../../skills/issue-reproducer \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-stale-sweep b/plugins/magpie-issue/skills/issue-stale-sweep new file mode 120000 index 000000000..4bb581f40 --- /dev/null +++ b/plugins/magpie-issue/skills/issue-stale-sweep @@ -0,0 +1 @@ +../../../skills/issue-stale-sweep \ No newline at end of file diff --git a/plugins/magpie-issue/skills/issue-triage b/plugins/magpie-issue/skills/issue-triage new file mode 120000 index 000000000..e4c48d1ba --- /dev/null +++ b/plugins/magpie-issue/skills/issue-triage @@ -0,0 +1 @@ +../../../skills/issue-triage \ No newline at end of file diff --git a/plugins/magpie-mentoring/.claude-plugin/plugin.json b/plugins/magpie-mentoring/.claude-plugin/plugin.json new file mode 100644 index 000000000..242625fd8 --- /dev/null +++ b/plugins/magpie-mentoring/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-mentoring", + "description": "Apache Magpie \u2014 newcomer mentoring: welcome, newcomer-issue explanations, good-first-issue authoring + sweep.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-mentoring/skills/good-first-issue-author b/plugins/magpie-mentoring/skills/good-first-issue-author new file mode 120000 index 000000000..0333f7f04 --- /dev/null +++ b/plugins/magpie-mentoring/skills/good-first-issue-author @@ -0,0 +1 @@ +../../../skills/good-first-issue-author \ No newline at end of file diff --git a/plugins/magpie-mentoring/skills/good-first-issue-sweep b/plugins/magpie-mentoring/skills/good-first-issue-sweep new file mode 120000 index 000000000..f5b5458ca --- /dev/null +++ b/plugins/magpie-mentoring/skills/good-first-issue-sweep @@ -0,0 +1 @@ +../../../skills/good-first-issue-sweep \ No newline at end of file diff --git a/plugins/magpie-mentoring/skills/mentoring-welcome b/plugins/magpie-mentoring/skills/mentoring-welcome new file mode 120000 index 000000000..27df0a2eb --- /dev/null +++ b/plugins/magpie-mentoring/skills/mentoring-welcome @@ -0,0 +1 @@ +../../../skills/mentoring-welcome \ No newline at end of file diff --git a/plugins/magpie-mentoring/skills/newcomer-issue-explainer b/plugins/magpie-mentoring/skills/newcomer-issue-explainer new file mode 120000 index 000000000..c6ecbde9e --- /dev/null +++ b/plugins/magpie-mentoring/skills/newcomer-issue-explainer @@ -0,0 +1 @@ +../../../skills/newcomer-issue-explainer \ No newline at end of file diff --git a/plugins/magpie-pairing/.claude-plugin/plugin.json b/plugins/magpie-pairing/.claude-plugin/plugin.json new file mode 100644 index 000000000..cf1c8118c --- /dev/null +++ b/plugins/magpie-pairing/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-pairing", + "description": "Apache Magpie \u2014 pair a change with a structured self-review or a multi-agent adversarial review.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-pairing/skills/pairing-multi-agent-review b/plugins/magpie-pairing/skills/pairing-multi-agent-review new file mode 120000 index 000000000..6219e2ba4 --- /dev/null +++ b/plugins/magpie-pairing/skills/pairing-multi-agent-review @@ -0,0 +1 @@ +../../../skills/pairing-multi-agent-review \ No newline at end of file diff --git a/plugins/magpie-pairing/skills/pairing-self-review b/plugins/magpie-pairing/skills/pairing-self-review new file mode 120000 index 000000000..84052f6e5 --- /dev/null +++ b/plugins/magpie-pairing/skills/pairing-self-review @@ -0,0 +1 @@ +../../../skills/pairing-self-review \ No newline at end of file diff --git a/plugins/magpie-pr-management/.claude-plugin/plugin.json b/plugins/magpie-pr-management/.claude-plugin/plugin.json new file mode 100644 index 000000000..ebc6f4e61 --- /dev/null +++ b/plugins/magpie-pr-management/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-pr-management", + "description": "Apache Magpie \u2014 PR-queue management: triage, stats, deep code review, quick-merge, stale-sweep, reviewer routing, pre-first-PR checks.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-pr-management/skills/pr-management-code-review b/plugins/magpie-pr-management/skills/pr-management-code-review new file mode 120000 index 000000000..831ee72b1 --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-management-code-review @@ -0,0 +1 @@ +../../../skills/pr-management-code-review \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pr-management-mentor b/plugins/magpie-pr-management/skills/pr-management-mentor new file mode 120000 index 000000000..686e9d943 --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-management-mentor @@ -0,0 +1 @@ +../../../skills/pr-management-mentor \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pr-management-quick-merge b/plugins/magpie-pr-management/skills/pr-management-quick-merge new file mode 120000 index 000000000..5f1ca0c25 --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-management-quick-merge @@ -0,0 +1 @@ +../../../skills/pr-management-quick-merge \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pr-management-stats b/plugins/magpie-pr-management/skills/pr-management-stats new file mode 120000 index 000000000..e7513a192 --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-management-stats @@ -0,0 +1 @@ +../../../skills/pr-management-stats \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pr-management-triage b/plugins/magpie-pr-management/skills/pr-management-triage new file mode 120000 index 000000000..e62d08d8f --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-management-triage @@ -0,0 +1 @@ +../../../skills/pr-management-triage \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pr-stale-sweep b/plugins/magpie-pr-management/skills/pr-stale-sweep new file mode 120000 index 000000000..66f2e99ed --- /dev/null +++ b/plugins/magpie-pr-management/skills/pr-stale-sweep @@ -0,0 +1 @@ +../../../skills/pr-stale-sweep \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/pre-first-pr-check b/plugins/magpie-pr-management/skills/pre-first-pr-check new file mode 120000 index 000000000..f2bc033b2 --- /dev/null +++ b/plugins/magpie-pr-management/skills/pre-first-pr-check @@ -0,0 +1 @@ +../../../skills/pre-first-pr-check \ No newline at end of file diff --git a/plugins/magpie-pr-management/skills/reviewer-routing b/plugins/magpie-pr-management/skills/reviewer-routing new file mode 120000 index 000000000..989f9fc30 --- /dev/null +++ b/plugins/magpie-pr-management/skills/reviewer-routing @@ -0,0 +1 @@ +../../../skills/reviewer-routing \ No newline at end of file diff --git a/plugins/magpie-release-management/.claude-plugin/plugin.json b/plugins/magpie-release-management/.claude-plugin/plugin.json new file mode 100644 index 000000000..0dcee0838 --- /dev/null +++ b/plugins/magpie-release-management/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-release-management", + "description": "Apache Magpie \u2014 ASF release lifecycle: plan, RC cut/sign, vote, tally, promote, announce, archive, audit.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-release-management/skills/release-announce-draft b/plugins/magpie-release-management/skills/release-announce-draft new file mode 120000 index 000000000..983a5efc6 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-announce-draft @@ -0,0 +1 @@ +../../../skills/release-announce-draft \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-archive-sweep b/plugins/magpie-release-management/skills/release-archive-sweep new file mode 120000 index 000000000..650df1824 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-archive-sweep @@ -0,0 +1 @@ +../../../skills/release-archive-sweep \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-audit-report b/plugins/magpie-release-management/skills/release-audit-report new file mode 120000 index 000000000..9b0d7aae8 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-audit-report @@ -0,0 +1 @@ +../../../skills/release-audit-report \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-keys-sync b/plugins/magpie-release-management/skills/release-keys-sync new file mode 120000 index 000000000..5c6f2bea2 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-keys-sync @@ -0,0 +1 @@ +../../../skills/release-keys-sync \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-prepare b/plugins/magpie-release-management/skills/release-prepare new file mode 120000 index 000000000..9fab6454a --- /dev/null +++ b/plugins/magpie-release-management/skills/release-prepare @@ -0,0 +1 @@ +../../../skills/release-prepare \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-promote b/plugins/magpie-release-management/skills/release-promote new file mode 120000 index 000000000..38407b4f4 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-promote @@ -0,0 +1 @@ +../../../skills/release-promote \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-rc-cut b/plugins/magpie-release-management/skills/release-rc-cut new file mode 120000 index 000000000..6734629b7 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-rc-cut @@ -0,0 +1 @@ +../../../skills/release-rc-cut \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-verify-rc b/plugins/magpie-release-management/skills/release-verify-rc new file mode 120000 index 000000000..d4c32799b --- /dev/null +++ b/plugins/magpie-release-management/skills/release-verify-rc @@ -0,0 +1 @@ +../../../skills/release-verify-rc \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-vote-draft b/plugins/magpie-release-management/skills/release-vote-draft new file mode 120000 index 000000000..92c924cfe --- /dev/null +++ b/plugins/magpie-release-management/skills/release-vote-draft @@ -0,0 +1 @@ +../../../skills/release-vote-draft \ No newline at end of file diff --git a/plugins/magpie-release-management/skills/release-vote-tally b/plugins/magpie-release-management/skills/release-vote-tally new file mode 120000 index 000000000..2c469d205 --- /dev/null +++ b/plugins/magpie-release-management/skills/release-vote-tally @@ -0,0 +1 @@ +../../../skills/release-vote-tally \ No newline at end of file diff --git a/plugins/magpie-repo-health/.claude-plugin/plugin.json b/plugins/magpie-repo-health/.claude-plugin/plugin.json new file mode 100644 index 000000000..615819455 --- /dev/null +++ b/plugins/magpie-repo-health/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-repo-health", + "description": "Apache Magpie \u2014 read-only repo-health audits: runner labels, workflow security, dependency/license/NOTICE, flaky tests, audit-finding fixes.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-repo-health/skills/audit-finding-fix b/plugins/magpie-repo-health/skills/audit-finding-fix new file mode 120000 index 000000000..a606cc6ce --- /dev/null +++ b/plugins/magpie-repo-health/skills/audit-finding-fix @@ -0,0 +1 @@ +../../../skills/audit-finding-fix \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/ci-runner-audit b/plugins/magpie-repo-health/skills/ci-runner-audit new file mode 120000 index 000000000..e9c23b4d0 --- /dev/null +++ b/plugins/magpie-repo-health/skills/ci-runner-audit @@ -0,0 +1 @@ +../../../skills/ci-runner-audit \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/dependency-audit b/plugins/magpie-repo-health/skills/dependency-audit new file mode 120000 index 000000000..35b24b551 --- /dev/null +++ b/plugins/magpie-repo-health/skills/dependency-audit @@ -0,0 +1 @@ +../../../skills/dependency-audit \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/dependency-license-audit b/plugins/magpie-repo-health/skills/dependency-license-audit new file mode 120000 index 000000000..1fac0ccd7 --- /dev/null +++ b/plugins/magpie-repo-health/skills/dependency-license-audit @@ -0,0 +1 @@ +../../../skills/dependency-license-audit \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/flaky-test-triage b/plugins/magpie-repo-health/skills/flaky-test-triage new file mode 120000 index 000000000..993b2ad9f --- /dev/null +++ b/plugins/magpie-repo-health/skills/flaky-test-triage @@ -0,0 +1 @@ +../../../skills/flaky-test-triage \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/license-compliance-audit b/plugins/magpie-repo-health/skills/license-compliance-audit new file mode 120000 index 000000000..96861730d --- /dev/null +++ b/plugins/magpie-repo-health/skills/license-compliance-audit @@ -0,0 +1 @@ +../../../skills/license-compliance-audit \ No newline at end of file diff --git a/plugins/magpie-repo-health/skills/workflow-security-audit b/plugins/magpie-repo-health/skills/workflow-security-audit new file mode 120000 index 000000000..eedd5ab9f --- /dev/null +++ b/plugins/magpie-repo-health/skills/workflow-security-audit @@ -0,0 +1 @@ +../../../skills/workflow-security-audit \ No newline at end of file diff --git a/plugins/magpie-security/.claude-plugin/plugin.json b/plugins/magpie-security/.claude-plugin/plugin.json new file mode 100644 index 000000000..10eff7dad --- /dev/null +++ b/plugins/magpie-security/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-security", + "description": "Apache Magpie \u2014 security-issue handling lifecycle \u2014 import through CVE publication, triage, sync, dedup, invalidate. Maintainer-only.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-security/skills/security-cve-allocate b/plugins/magpie-security/skills/security-cve-allocate new file mode 120000 index 000000000..4b8ddcc84 --- /dev/null +++ b/plugins/magpie-security/skills/security-cve-allocate @@ -0,0 +1 @@ +../../../skills/security-cve-allocate \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-deduplicate b/plugins/magpie-security/skills/security-issue-deduplicate new file mode 120000 index 000000000..27467f599 --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-deduplicate @@ -0,0 +1 @@ +../../../skills/security-issue-deduplicate \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-fix b/plugins/magpie-security/skills/security-issue-fix new file mode 120000 index 000000000..ca3a85797 --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-fix @@ -0,0 +1 @@ +../../../skills/security-issue-fix \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-import b/plugins/magpie-security/skills/security-issue-import new file mode 120000 index 000000000..f2e97f241 --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-import @@ -0,0 +1 @@ +../../../skills/security-issue-import \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-import-from-md b/plugins/magpie-security/skills/security-issue-import-from-md new file mode 120000 index 000000000..44c90e3c1 --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-import-from-md @@ -0,0 +1 @@ +../../../skills/security-issue-import-from-md \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-import-from-pr b/plugins/magpie-security/skills/security-issue-import-from-pr new file mode 120000 index 000000000..b640559cb --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-import-from-pr @@ -0,0 +1 @@ +../../../skills/security-issue-import-from-pr \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-import-from-scan b/plugins/magpie-security/skills/security-issue-import-from-scan new file mode 120000 index 000000000..75d352a6b --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-import-from-scan @@ -0,0 +1 @@ +../../../skills/security-issue-import-from-scan \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-import-via-forwarder b/plugins/magpie-security/skills/security-issue-import-via-forwarder new file mode 120000 index 000000000..4b835055d --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-import-via-forwarder @@ -0,0 +1 @@ +../../../skills/security-issue-import-via-forwarder \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-invalidate b/plugins/magpie-security/skills/security-issue-invalidate new file mode 120000 index 000000000..c35602c5b --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-invalidate @@ -0,0 +1 @@ +../../../skills/security-issue-invalidate \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-sync b/plugins/magpie-security/skills/security-issue-sync new file mode 120000 index 000000000..0f524f33f --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-sync @@ -0,0 +1 @@ +../../../skills/security-issue-sync \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-issue-triage b/plugins/magpie-security/skills/security-issue-triage new file mode 120000 index 000000000..bbe501c5e --- /dev/null +++ b/plugins/magpie-security/skills/security-issue-triage @@ -0,0 +1 @@ +../../../skills/security-issue-triage \ No newline at end of file diff --git a/plugins/magpie-security/skills/security-tracker-stats-dashboard b/plugins/magpie-security/skills/security-tracker-stats-dashboard new file mode 120000 index 000000000..912859aa6 --- /dev/null +++ b/plugins/magpie-security/skills/security-tracker-stats-dashboard @@ -0,0 +1 @@ +../../../skills/security-tracker-stats-dashboard \ No newline at end of file diff --git a/plugins/magpie-setup/.claude-plugin/plugin.json b/plugins/magpie-setup/.claude-plugin/plugin.json new file mode 100644 index 000000000..451b21c70 --- /dev/null +++ b/plugins/magpie-setup/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-setup", + "description": "Apache Magpie \u2014 framework install/maintenance: install (adopt), upgrade, verify, override, status, secure-agent setup, shared-config sync.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-setup/skills/setup b/plugins/magpie-setup/skills/setup new file mode 120000 index 000000000..8ae30b1a9 --- /dev/null +++ b/plugins/magpie-setup/skills/setup @@ -0,0 +1 @@ +../../../skills/setup \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-isolated-setup-doctor b/plugins/magpie-setup/skills/setup-isolated-setup-doctor new file mode 120000 index 000000000..729daa07a --- /dev/null +++ b/plugins/magpie-setup/skills/setup-isolated-setup-doctor @@ -0,0 +1 @@ +../../../skills/setup-isolated-setup-doctor \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-isolated-setup-install b/plugins/magpie-setup/skills/setup-isolated-setup-install new file mode 120000 index 000000000..b142191ff --- /dev/null +++ b/plugins/magpie-setup/skills/setup-isolated-setup-install @@ -0,0 +1 @@ +../../../skills/setup-isolated-setup-install \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-isolated-setup-update b/plugins/magpie-setup/skills/setup-isolated-setup-update new file mode 120000 index 000000000..e1dd883e5 --- /dev/null +++ b/plugins/magpie-setup/skills/setup-isolated-setup-update @@ -0,0 +1 @@ +../../../skills/setup-isolated-setup-update \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-isolated-setup-verify b/plugins/magpie-setup/skills/setup-isolated-setup-verify new file mode 120000 index 000000000..31a127698 --- /dev/null +++ b/plugins/magpie-setup/skills/setup-isolated-setup-verify @@ -0,0 +1 @@ +../../../skills/setup-isolated-setup-verify \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-override-upstream b/plugins/magpie-setup/skills/setup-override-upstream new file mode 120000 index 000000000..fb63dc407 --- /dev/null +++ b/plugins/magpie-setup/skills/setup-override-upstream @@ -0,0 +1 @@ +../../../skills/setup-override-upstream \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-shared-config-sync b/plugins/magpie-setup/skills/setup-shared-config-sync new file mode 120000 index 000000000..236211e34 --- /dev/null +++ b/plugins/magpie-setup/skills/setup-shared-config-sync @@ -0,0 +1 @@ +../../../skills/setup-shared-config-sync \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-status b/plugins/magpie-setup/skills/setup-status new file mode 120000 index 000000000..ab87fce5a --- /dev/null +++ b/plugins/magpie-setup/skills/setup-status @@ -0,0 +1 @@ +../../../skills/setup-status \ No newline at end of file diff --git a/plugins/magpie-setup/skills/setup-upstream-fix b/plugins/magpie-setup/skills/setup-upstream-fix new file mode 120000 index 000000000..3fa8f25df --- /dev/null +++ b/plugins/magpie-setup/skills/setup-upstream-fix @@ -0,0 +1 @@ +../../../skills/setup-upstream-fix \ No newline at end of file diff --git a/plugins/magpie-utilities/.claude-plugin/plugin.json b/plugins/magpie-utilities/.claude-plugin/plugin.json new file mode 100644 index 000000000..30572ef9d --- /dev/null +++ b/plugins/magpie-utilities/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "magpie-utilities", + "description": "Apache Magpie \u2014 framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills.", + "version": "0.2.0.dev0", + "author": { + "name": "Apache Magpie", + "url": "https://magpie.apache.org/" + }, + "homepage": "https://magpie.apache.org/", + "repository": "https://github.com/apache/magpie", + "license": "Apache-2.0", + "skills": "./skills" +} diff --git a/plugins/magpie-utilities/skills/list-skills b/plugins/magpie-utilities/skills/list-skills new file mode 120000 index 000000000..97f92b85a --- /dev/null +++ b/plugins/magpie-utilities/skills/list-skills @@ -0,0 +1 @@ +../../../skills/list-skills \ No newline at end of file diff --git a/plugins/magpie-utilities/skills/optimize-skill b/plugins/magpie-utilities/skills/optimize-skill new file mode 120000 index 000000000..f25d4c312 --- /dev/null +++ b/plugins/magpie-utilities/skills/optimize-skill @@ -0,0 +1 @@ +../../../skills/optimize-skill \ No newline at end of file diff --git a/plugins/magpie-utilities/skills/skill-reconciler b/plugins/magpie-utilities/skills/skill-reconciler new file mode 120000 index 000000000..495f0f27e --- /dev/null +++ b/plugins/magpie-utilities/skills/skill-reconciler @@ -0,0 +1 @@ +../../../skills/skill-reconciler \ No newline at end of file diff --git a/plugins/magpie-utilities/skills/write-skill b/plugins/magpie-utilities/skills/write-skill new file mode 120000 index 000000000..2d8e4f960 --- /dev/null +++ b/plugins/magpie-utilities/skills/write-skill @@ -0,0 +1 @@ +../../../skills/write-skill \ No newline at end of file diff --git a/projects/magpie/release-management-config.md b/projects/magpie/release-management-config.md index 7d084310d..8cead13cf 100644 --- a/projects/magpie/release-management-config.md +++ b/projects/magpie/release-management-config.md @@ -71,7 +71,39 @@ mandatory ASF approval + announce mechanisms (`dev-list-vote`, | `git_upstream_remote` | `upstream` | | `release_planning_issue_template` | *(none — uses the `release-prepare` default template)* | | `release_branch_base` | `main` | -| `version_manifest_files` | `pyproject.toml` | +| `version_manifest_files` | `pyproject.toml`, `uv.lock`, `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `plugin.json` (Agent Plugins 1.0), `.codex-plugin/plugin.json`, `gemini-extension.json`, `apm.yml`, and the ten generated `plugins/magpie-/.claude-plugin/plugin.json` manifests | + +`pyproject.toml`'s `project.version` is the **single authority** for the +framework version; every other file above mirrors it verbatim, including a +`.devN` suffix (between releases the manifests read `0.2.0.dev0`, not `0.2.0`). +Dev versions are never published to a marketplace, so the PEP 440 suffix never +reaches a consumer — and keeping one identical string across every manifest is +what lets the Step 2a bump work as a single literal search/replace. + +`uv.lock` is the one entry `--fix` does **not** touch: it carries the version +because it locks this workspace's own package, and it is refreshed by +`uv lock` (which the Step 2a bump runs), not by the manifest propagation below. + +The version flows outward in two hops, neither of them hand-edited: + +1. `pyproject.toml` → the five ecosystem manifests + (`.claude-plugin/plugin.json`, the Agent Plugins 1.0 `plugin.json`, + `.codex-plugin/plugin.json`, `gemini-extension.json`, `apm.yml`); +2. `.claude-plugin/plugin.json` → the ten generated + `plugins/magpie-/.claude-plugin/plugin.json` manifests and the + family entries in `.claude-plugin/marketplace.json`, which also inherit + `author`, `homepage`, `repository`, and `license`. + +So a bump is one edit plus one command: + +```bash +# edit project.version in pyproject.toml, then: +python3 tools/dev/check-family-plugins.py --fix +``` + +`tools/dev/check-family-plugins.py` (a prek hook) fails the build if any +manifest or marketplace entry is left behind at the previous version, so a +missed propagation cannot reach a release. ## Backends diff --git a/tools/dev/check-family-plugins.py b/tools/dev/check-family-plugins.py new file mode 100644 index 000000000..128ec324d --- /dev/null +++ b/tools/dev/check-family-plugins.py @@ -0,0 +1,573 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: Apache-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 +"""Validate the marketplace plugins against the skills' `family:` frontmatter — +the source of truth. + +Checks that every plugin is properly defined: + +- every ecosystem manifest that declares the framework version + (`.claude-plugin/plugin.json`, the Agent Plugins 1.0 `plugin.json`, + `.codex-plugin/plugin.json`, `gemini-extension.json`, `apm.yml`) mirrors + `pyproject.toml`'s `project.version` verbatim — including a `.devN` suffix; +- the all-in-one `magpie` manifest (`.claude-plugin/plugin.json`) names itself + correctly, declares `skills: ./skills`, and wires the `hooks/check-upgrade.sh` + SessionStart hook (which must exist); +- the root `plugin.json` conforms to Agent Plugins 1.0 — the pinned `$schema`, + the name pattern, the closed ten-field set (so a Claude-only component path + never leaks in), and the same shared metadata as the Claude manifest; +- every `.claude-plugin/marketplace.json` entry resolves to a matching, + uniquely-named `plugin.json` and carries the root manifest's version; +- the Codex and Copilot catalogs (`.agents/plugins/marketplace.json`, + `marketplace.json`) list the all-in-one plugin and *only* that — the family + plugins are Claude Code-only, so advertising them there would offer those + clients something they cannot install; +- for every family declared in a skill's `family:` frontmatter there is a + `plugins/magpie-/` plugin whose manifest is well-formed, inherits the + root manifest's shared metadata (version, author, homepage, repository, + license), and whose `skills/` directory contains exactly that family's skills + as single-hop symlinks into the shared `skills/` tree. + +Drift — a new skill, a changed family, a stale symlink, a malformed or +mis-named manifest, a dangling marketplace entry, a family manifest left behind +at the previous release's version — fails the check. + +Run with `--fix` to propagate the version from `pyproject.toml` and regenerate +the family plugins + symlinks + marketplace entries from the frontmatter. A +release bump therefore has one edit point: `pyproject.toml`, then `--fix`. +""" +from __future__ import annotations + +import argparse +import json +import re +import shutil +import sys +import tomllib +from pathlib import Path + +SKILLS = Path("skills") +PLUGINS = Path("plugins") +MARKETPLACE = Path(".claude-plugin/marketplace.json") +ROOT_MANIFEST = Path(".claude-plugin/plugin.json") # the all-in-one `magpie` plugin +HOOK_SCRIPT = Path("hooks/check-upgrade.sh") # referenced by the all-in-one plugin hook +SYMLINK_TARGET = "../../../skills/{skill}" # relative to plugins/magpie-/skills/ + +# The vendor-neutral Agent Plugins 1.0 manifest for the same all-in-one plugin. +# It lives at the repo root (the spec permits no alternative location) and is +# read by VS Code / Copilot, which auto-detect the format from the root manifest +# and treat the `$schema` value as the AP1 marker. Its schema is *closed*: only +# the ten fields below are permitted, so component paths (`skills`, `hooks`) +# must NOT appear — AP1 fixes skills at `skills/` and defines no hook component. +AP1_MANIFEST = Path("plugin.json") +AP1_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" +AP1_FIELDS = frozenset( + { + "$schema", + "name", + "version", + "description", + "author", + "homepage", + "repository", + "license", + "keywords", + "extensions", + } +) +AP1_AUTHOR_FIELDS = frozenset({"name", "email", "url"}) + +# The non-Claude catalogs. These list the all-in-one plugin *only*: the family +# plugins reach their skills through symlinks that deliberately resolve outside +# the family's own root, which Agent Plugins 1.0 forbids (a symlink's final +# target must stay within the plugin root). Materialising them into real +# directories would mean vendored copies of every skill — PRINCIPLES §13. So +# per-family is a Claude Code feature, and this check keeps the other catalogs +# from drifting into advertising something their clients cannot install. +CLIENT_CATALOGS = ( + Path(".agents/plugins/marketplace.json"), # Codex CLI repo marketplace + Path("marketplace.json"), # GitHub Copilot / VS Code +) +# `name`: 1–64 chars, lowercase alphanumeric plus `-`/`.`, no `--`/`..`, +# alphanumeric at both ends. +AP1_NAME_RE = re.compile(r"^(?!.*(?:--|\.\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$") + +# `pyproject.toml` is the single authority for the framework version — it is what +# the post-release `chore: bump version` commit edits. Every ecosystem manifest +# mirrors that string verbatim (including a `.devN` suffix), so `release-prepare` +# can bump them all with one literal search/replace. Dev versions are never +# published to a marketplace, so a PEP 440 suffix never reaches a consumer. +PYPROJECT = Path("pyproject.toml") +ECOSYSTEM_MANIFESTS = ( + ROOT_MANIFEST, + AP1_MANIFEST, + Path(".codex-plugin/plugin.json"), + Path("gemini-extension.json"), + Path("apm.yml"), +) +APM_VERSION_RE = re.compile(r"^(version:[ \t]*)(\S+)[ \t]*$", re.M) +# Substituted rather than re-serialised: these manifests are hand-authored, and a +# json.dumps() round-trip would reformat them (escaping em-dashes, expanding +# inline objects) far beyond the one string being bumped. +JSON_VERSION_RE = re.compile(r'("version"[ \t]*:[ \t]*")[^"]*(")') + +# Metadata the per-family plugins and the marketplace entries inherit verbatim +# from the all-in-one manifest, so a release bump has exactly one edit point. +# `version` + `author` are what `claude plugin validate --strict` warns about +# when absent; the rest carry attribution into a marketplace listing. +INHERITED = ("version", "author", "homepage", "repository", "license") + +# Per-family descriptions used when (re)generating manifests. Keep in sync with +# the family README; `check` does not enforce these (only structure/symlinks). +DESC = { + "security": "security-issue handling lifecycle — import through CVE publication, triage, sync, dedup, invalidate. Maintainer-only.", + "release-management": "ASF release lifecycle: plan, RC cut/sign, vote, tally, promote, announce, archive, audit.", + "setup": "framework install/maintenance: install (adopt), upgrade, verify, override, status, secure-agent setup, shared-config sync.", + "pr-management": "PR-queue management: triage, stats, deep code review, quick-merge, stale-sweep, reviewer routing, pre-first-PR checks.", + "issue": "issue lifecycle: triage, reproduction, fix drafting, reassess, stale-sweep, dedup, backlog stats.", + "repo-health": "read-only repo-health audits: runner labels, workflow security, dependency/license/NOTICE, flaky tests, audit-finding fixes.", + "contributor-growth": "path-to-committer: activity sweeps, nominations, sentiment, readiness, committer/post-vote onboarding.", + "utilities": "framework meta-skills: write-skill, optimize-skill, skill-reconciler, list-skills.", + "mentoring": "newcomer mentoring: welcome, newcomer-issue explanations, good-first-issue authoring + sweep.", + "pairing": "pair a change with a structured self-review or a multi-agent adversarial review.", +} + + +def load_json(path: Path): + try: + return json.loads(path.read_text(encoding="utf-8")), None + except (OSError, ValueError) as exc: + return None, f"{path}: cannot read/parse ({exc})" + + +def validate_manifest( + path: Path, expected_name: str, inherited: dict | None = None +) -> list[str]: + """A plugin.json must exist, be valid JSON, name itself correctly, and + declare its skills + a description. Per-family manifests additionally carry + the root manifest's shared metadata (`inherited`) verbatim.""" + if not path.is_file(): + return [f"{path}: missing plugin manifest"] + data, err = load_json(path) + if err: + return [err] + errors: list[str] = [] + if data.get("name") != expected_name: + errors.append(f"{path}: name is {data.get('name')!r}, expected {expected_name!r}") + if data.get("skills") != "./skills": + errors.append(f"{path}: 'skills' is {data.get('skills')!r}, expected './skills'") + if not str(data.get("description", "")).strip(): + errors.append(f"{path}: missing/empty 'description'") + for key, want in (inherited or {}).items(): + if data.get(key) != want: + errors.append( + f"{path}: {key!r} is {data.get(key)!r}, expected {want!r} " + f"(inherited from {ROOT_MANIFEST})" + ) + return errors + + +def pyproject_version() -> tuple[str | None, list[str]]: + """The framework version — the single authority every manifest mirrors.""" + try: + version = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))["project"]["version"] + except (OSError, ValueError, KeyError) as exc: + return None, [f"{PYPROJECT}: cannot read project.version ({exc})"] + return str(version), [] + + +def manifest_version(path: Path) -> tuple[str | None, str | None]: + """Read the version out of a manifest in whichever format it uses.""" + if path.suffix in (".yml", ".yaml"): + try: + m = APM_VERSION_RE.search(path.read_text(encoding="utf-8")) + except OSError as exc: + return None, f"{path}: cannot read ({exc})" + return (m.group(2) if m else None), None + data, err = load_json(path) + if err: + return None, err + return data.get("version"), None + + +def write_manifest_version(path: Path, version: str) -> list[str]: + """Rewrite a manifest's version in place, touching nothing else.""" + pattern = APM_VERSION_RE if path.suffix in (".yml", ".yaml") else JSON_VERSION_RE + text = path.read_text(encoding="utf-8") + new, n = pattern.subn( + (rf"\g<1>{version}" if pattern is APM_VERSION_RE else rf"\g<1>{version}\g<2>"), + text, + count=1, + ) + if n != 1: + return [f"{path}: no 'version' declaration to rewrite"] + path.write_text(new, encoding="utf-8") + # The substitution is textual — confirm it produced the intended value. + have, err = manifest_version(path) + if err: + return [err] + if have != version: + return [f"{path}: rewrite produced {have!r}, expected {version!r}"] + return [] + + +def check_ecosystem_versions(version: str) -> list[str]: + """Every ecosystem manifest mirrors `pyproject.toml`'s version verbatim.""" + errors: list[str] = [] + for path in ECOSYSTEM_MANIFESTS: + if not path.is_file(): + errors.append(f"{path}: missing (declares the framework version)") + continue + have, err = manifest_version(path) + if err: + errors.append(err) + elif have != version: + errors.append( + f"{path}: version is {have!r}, expected {version!r} (from {PYPROJECT})" + ) + return errors + + +def validate_ap1_manifest(inherited: dict | None = None) -> list[str]: + """The root `plugin.json` conforms to Agent Plugins 1.0. + + Enforced here rather than by a JSON-Schema dependency: the schema is small, + closed, and the failure we actually care about is someone copying a + Claude-only field (`skills`, `hooks`, `mcpServers`) into it, which AP1 + clients reject as a fatal manifest error rather than ignore. + """ + if not AP1_MANIFEST.is_file(): + return [f"{AP1_MANIFEST}: missing (Agent Plugins 1.0 manifest)"] + data, err = load_json(AP1_MANIFEST) + if err: + return [err] + errors: list[str] = [] + + if data.get("$schema") != AP1_SCHEMA: + errors.append( + f"{AP1_MANIFEST}: '$schema' is {data.get('$schema')!r}, expected {AP1_SCHEMA!r} " + f"(without it, VS Code/Copilot fall back to the legacy format)" + ) + name = data.get("name") + if name != "magpie": + errors.append(f"{AP1_MANIFEST}: name is {name!r}, expected 'magpie'") + elif not AP1_NAME_RE.match(name): + errors.append(f"{AP1_MANIFEST}: name {name!r} violates the AP1 name pattern") + + extra = sorted(set(data) - AP1_FIELDS) + if extra: + errors.append( + f"{AP1_MANIFEST}: {', '.join(repr(k) for k in extra)} not permitted — " + f"the AP1 manifest schema is closed to {len(AP1_FIELDS)} fields " + f"(client-specific data belongs in 'extensions' or the client's own manifest)" + ) + + author = data.get("author") + if author is not None: + if not isinstance(author, dict): + errors.append(f"{AP1_MANIFEST}: 'author' must be an object") + else: + bad = sorted(set(author) - AP1_AUTHOR_FIELDS) + if bad: + errors.append( + f"{AP1_MANIFEST}: author has {', '.join(repr(k) for k in bad)}; " + f"AP1 permits only {', '.join(sorted(AP1_AUTHOR_FIELDS))}" + ) + + # AP1 fixes the skills location; there is no manifest field to point + # elsewhere, so the real `skills/` tree is what any AP1 client will load. + if not SKILLS.is_dir(): + errors.append(f"{SKILLS}: missing — AP1 clients discover skills only here") + + for key, want in (inherited or {}).items(): + if data.get(key) != want: + errors.append( + f"{AP1_MANIFEST}: {key!r} is {data.get(key)!r}, expected {want!r} " + f"(inherited from {ROOT_MANIFEST})" + ) + return errors + + +def check_client_catalogs() -> list[str]: + """The Codex and Copilot catalogs list the all-in-one plugin and only that.""" + errors: list[str] = [] + for path in CLIENT_CATALOGS: + if not path.is_file(): + errors.append(f"{path}: missing (client plugin catalog)") + continue + data, err = load_json(path) + if err: + errors.append(err) + continue + entries = data.get("plugins") + if not isinstance(entries, list): + errors.append(f"{path}: 'plugins' is missing or not a list") + continue + names = [e.get("name") for e in entries if isinstance(e, dict)] + if "magpie" not in names: + errors.append(f"{path}: missing the all-in-one 'magpie' plugin entry") + if families := sorted(n for n in names if n and n.startswith("magpie-")): + errors.append( + f"{path}: lists per-family plugin(s) {', '.join(families)} — the family " + f"plugins are Claude Code-only (their skill symlinks resolve outside the " + f"family plugin root, which Agent Plugins 1.0 forbids)" + ) + return errors + + +def root_metadata() -> tuple[dict, list[str]]: + """The subset of the all-in-one manifest that the family plugins inherit.""" + data, err = load_json(ROOT_MANIFEST) + if err: + return {}, [err] + missing = [k for k in INHERITED if not data.get(k)] + if missing: + return {}, [f"{ROOT_MANIFEST}: missing {', '.join(repr(k) for k in missing)}"] + return {k: data[k] for k in INHERITED}, [] + + +def families_from_frontmatter() -> dict[str, set[str]]: + fam: dict[str, set[str]] = {} + for path in sorted(SKILLS.glob("*/SKILL.md")): + m = re.search(r"^family:\s*(\S+)", path.read_text(encoding="utf-8"), re.M) + if m: + fam.setdefault(m.group(1), set()).add(path.parent.name) + return fam + + +def check(fam: dict[str, set[str]]) -> list[str]: + errors: list[str] = [] + + market, err = load_json(MARKETPLACE) + if err: + return [err] + entries = market.get("plugins", []) + listed = {p.get("name") for p in entries} + + shared, meta_errs = root_metadata() + errors += meta_errs + + # 0) Every ecosystem manifest mirrors pyproject.toml's version. + version, verr = pyproject_version() + errors += verr + if version is not None: + errors += check_ecosystem_versions(version) + + # 1) The all-in-one `magpie` plugin: well-formed manifest, listed, and its + # SessionStart hook script is present + referenced. + errors += validate_manifest(ROOT_MANIFEST, "magpie") + errors += validate_ap1_manifest(shared) + errors += check_client_catalogs() + if "magpie" not in listed: + errors.append(f"{MARKETPLACE}: missing the all-in-one 'magpie' plugin entry") + if not HOOK_SCRIPT.is_file(): + errors.append(f"{HOOK_SCRIPT}: missing (referenced by the all-in-one plugin's SessionStart hook)") + root_data, root_err = load_json(ROOT_MANIFEST) + if root_data is not None and "check-upgrade.sh" not in json.dumps(root_data.get("hooks", {})): + errors.append(f"{ROOT_MANIFEST}: SessionStart hook does not reference hooks/check-upgrade.sh") + + # 2) Every marketplace entry resolves to a matching, uniquely-named manifest. + seen: set[str] = set() + for ent in entries: + name = ent.get("name") + source = ent.get("source") + if not name or not source: + errors.append(f"{MARKETPLACE}: entry missing 'name'/'source': {ent}") + continue + if name in seen: + errors.append(f"{MARKETPLACE}: duplicate plugin entry '{name}'") + seen.add(name) + manifest = ROOT_MANIFEST if source == "." else Path(source) / ".claude-plugin" / "plugin.json" + if not manifest.is_file(): + errors.append(f"{MARKETPLACE}: '{name}' source '{source}' has no {manifest}") + continue + data, jerr = load_json(manifest) + if jerr is None and data.get("name") != name: + errors.append(f"{MARKETPLACE}: '{name}' resolves to plugin.json named {data.get('name')!r}") + if shared and ent.get("version") != shared["version"]: + errors.append( + f"{MARKETPLACE}: '{name}' version is {ent.get('version')!r}, " + f"expected {shared['version']!r} (from {ROOT_MANIFEST})" + ) + + # 3) Per-family plugins: manifest well-formed + symlinks match the frontmatter. + for family, skills in sorted(fam.items()): + name = f"magpie-{family}" + pdir = PLUGINS / name + manifest_errs = validate_manifest( + pdir / ".claude-plugin" / "plugin.json", name, inherited=shared or None + ) + errors += manifest_errs + if manifest_errs: + continue + if name not in listed: + errors.append(f"{MARKETPLACE}: missing entry for '{name}'") + + sdir = pdir / "skills" + have: dict[str, Path] = {} + for link in sorted(sdir.iterdir()) if sdir.is_dir() else []: + if link.is_symlink(): + have[link.name] = link.readlink() + else: + errors.append(f"{name}: {link} is not a symlink") + + for skill in sorted(skills - set(have)): + errors.append(f"{name}: missing symlink for '{skill}' (family={family})") + for skill in sorted(set(have) - skills): + errors.append(f"{name}: stale symlink '{skill}' — its skill is not family={family}") + for skill in sorted(skills & set(have)): + want = Path(SYMLINK_TARGET.format(skill=skill)) + if have[skill] != want: + errors.append(f"{name}: {sdir / skill} -> {have[skill]} (expected {want})") + + # 4) No orphan plugin dirs (a magpie- with no skills declaring family x). + for pdir in sorted(PLUGINS.glob("magpie-*")): + family = pdir.name[len("magpie-"):] + if family not in fam: + errors.append(f"orphan plugin '{pdir.name}': no skill declares family '{family}'") + + return errors + + +def unowned_entries(pdir: Path) -> list[str]: + """Anything in a family plugin dir that `--fix` did not generate. + + A blanket `rmtree` is safe only for as long as these directories hold + nothing but a generated manifest and symlinks. The moment a family grows a + `commands/`, an `agents/`, or a README, a blanket delete would silently + discard it. So enumerate what regeneration owns and report anything else, + so the operator gets an error naming the file instead of a quiet loss. + """ + if not pdir.is_dir(): + return [] + mdir, sdir = pdir / ".claude-plugin", pdir / "skills" + unexpected = sorted(p for p in pdir.iterdir() if p not in {mdir, sdir}) + if mdir.is_dir(): + unexpected += sorted(p for p in mdir.iterdir() if p.name != "plugin.json") + if sdir.is_dir(): + unexpected += sorted(p for p in sdir.iterdir() if not p.is_symlink()) + if not unexpected: + return [] + return [ + f"{pdir}: refusing to regenerate — not generated by --fix: " + f"{', '.join(str(p) for p in unexpected)} " + f"(move it out, or teach --fix to generate it)" + ] + + +def fix(fam: dict[str, set[str]]) -> int: + # Propagate the authoritative version outward before reading the root + # manifest's metadata, so a bump in pyproject.toml alone is enough. + version, verr = pyproject_version() + if verr or version is None: + for e in verr or [f"{PYPROJECT}: no project.version"]: + print(f" - {e}", file=sys.stderr) + return 1 + for path in ECOSYSTEM_MANIFESTS: + if not path.is_file(): + print(f" - {path}: missing (declares the framework version)", file=sys.stderr) + return 1 + have, err = manifest_version(path) + if err: + print(f" - {err}", file=sys.stderr) + return 1 + if have != version: + if write_errs := write_manifest_version(path, version): + for e in write_errs: + print(f" - {e}", file=sys.stderr) + return 1 + print(f"{path}: version {have} -> {version}") + + shared, meta_errs = root_metadata() + if meta_errs: + for e in meta_errs: + print(f" - {e}", file=sys.stderr) + print( + f"\nCannot regenerate: the family plugins inherit " + f"{', '.join(INHERITED)} from {ROOT_MANIFEST}.", + file=sys.stderr, + ) + return 1 + + # Check every family dir *before* deleting any of them, so a stray file in + # the last one does not leave the first nine already destroyed. + stale = sorted(PLUGINS.glob("magpie-*")) + if rm_errs := [e for pdir in stale for e in unowned_entries(pdir)]: + for e in rm_errs: + print(f" - {e}", file=sys.stderr) + return 1 + for pdir in stale: + shutil.rmtree(pdir) + for family, skills in sorted(fam.items()): + name = f"magpie-{family}" + pdir = PLUGINS / name + sdir = pdir / "skills" + (pdir / ".claude-plugin").mkdir(parents=True) + sdir.mkdir(parents=True) + for skill in sorted(skills): + (sdir / skill).symlink_to(SYMLINK_TARGET.format(skill=skill)) + manifest = { + "name": name, + "description": f"Apache Magpie — {DESC.get(family, family + ' family skills')}", + **shared, + "skills": "./skills", + } + (pdir / ".claude-plugin" / "plugin.json").write_text( + json.dumps(manifest, indent=2) + "\n", encoding="utf-8" + ) + + market, market_err = load_json(MARKETPLACE) + if market_err: + print(f" - {market_err}", file=sys.stderr) + return 1 + # The all-in-one entry is carried over, not regenerated, so it is the one + # thing here that `--fix` cannot reconstruct. Bail out rather than write a + # marketplace containing only the families: `check` would catch that on the + # next run, but the destructive rewrite would already have happened. + entries = market.get("plugins") + if not isinstance(entries, list): + print(f" - {MARKETPLACE}: 'plugins' is missing or not a list", file=sys.stderr) + return 1 + keep = [p | {"version": shared["version"]} for p in entries if p.get("name") == "magpie"] + if not keep: + print( + f" - {MARKETPLACE}: no all-in-one 'magpie' entry to carry over " + f"(--fix regenerates only the family entries; restore it before rerunning)", + file=sys.stderr, + ) + return 1 + for family, skills in sorted(fam.items()): + keep.append({ + "name": f"magpie-{family}", + "source": f"./plugins/magpie-{family}", + "version": shared["version"], + "description": f"Apache Magpie {family} family ({len(skills)} skills).", + }) + market["plugins"] = keep + MARKETPLACE.write_text(json.dumps(market, indent=2) + "\n", encoding="utf-8") + print("Regenerated per-family plugins + marketplace entries from frontmatter.") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--fix", action="store_true", help="regenerate plugins from frontmatter") + ap.add_argument("files", nargs="*", help="(ignored; present for pre-commit)") + args = ap.parse_args() + + fam = families_from_frontmatter() + if args.fix: + return fix(fam) + + errors = check(fam) + if errors: + print("Family plugins are out of sync with skills' `family:` frontmatter:", file=sys.stderr) + for e in errors: + print(f" - {e}", file=sys.stderr) + print("\nRun `python3 tools/dev/check-family-plugins.py --fix` to regenerate.", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())