diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json deleted file mode 100644 index 18d3784..0000000 --- a/.agents/plugins/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "cracking-shells", - "interface": { - "displayName": "CrackingShells" - }, - "plugins": [ - { - "name": "colgrep-mcp", - "source": { - "source": "local", - "path": "./" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "NONE" - }, - "category": "Developer Tools" - } - ] -} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json deleted file mode 100644 index f3a643d..0000000 --- a/.claude-plugin/marketplace.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "cracking-shells", - "description": "CrackingShells marketplace. Here: colgrep-mcp, the product plugin (semantic and hybrid code search as MCP tools), and colgrep-mcp-dev, the maintainer's plugin of skills.", - "owner": { - "name": "Eliott Jacopin", - "email": "eliott.jacopin@riken.jp" - }, - "plugins": [ - { - "name": "colgrep-mcp", - "source": "./", - "description": "Semantic and hybrid code search for coding agents, exposed as MCP tools backed by colgrep." - }, - { - "name": "colgrep-mcp-dev", - "source": "./dev", - "description": "Maintainer skills for this repository (policy, campaign leadership, landing and release, stack traps). Not for end users." - } - ] -} diff --git a/.claude-plugin/mcp.json b/.claude-plugin/mcp.json index 5a84d14..fcb70c5 100644 --- a/.claude-plugin/mcp.json +++ b/.claude-plugin/mcp.json @@ -2,7 +2,9 @@ "mcpServers": { "colgrep": { "command": "uvx", - "args": ["colgrep-mcp==0.5.1"], + "args": [ + "colgrep-mcp==0.5.1" + ], "env": { "COLGREP_MCP_ROOT": "${CLAUDE_PROJECT_DIR}" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index f94dfe8..ed8f952 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -10,7 +10,13 @@ "homepage": "https://github.com/CrackingShells/colgrep-mcp", "repository": "https://github.com/CrackingShells/colgrep-mcp", "license": "AGPL-3.0-or-later", - "keywords": ["mcp", "colgrep", "semantic-search", "code-search", "agents"], + "keywords": [ + "mcp", + "colgrep", + "semantic-search", + "code-search", + "agents" + ], "mcpServers": "./.claude-plugin/mcp.json", "hooks": "./hooks/worktree-remove.json", "skills": "./skills/" diff --git a/.codex-plugin/mcp.json b/.codex-plugin/mcp.json deleted file mode 100644 index 54631b6..0000000 --- a/.codex-plugin/mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "colgrep": { - "command": "uvx", - "args": ["colgrep-mcp==0.5.1"] - } - } -} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json deleted file mode 100644 index a889ece..0000000 --- a/.codex-plugin/plugin.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "colgrep-mcp", - "version": "0.5.1", - "description": "Semantic and hybrid code search for coding agents, exposed as MCP tools backed by colgrep.", - "author": { - "name": "Eliott Jacopin", - "email": "eliott.jacopin@riken.jp" - }, - "repository": "https://github.com/CrackingShells/colgrep-mcp", - "keywords": ["mcp", "colgrep", "semantic-search", "code-search", "agents"], - "skills": "./skills/", - "mcpServers": "./.codex-plugin/mcp.json", - "hooks": "./hooks/hooks.json", - "interface": { - "displayName": "colgrep", - "shortDescription": "Semantic and hybrid code search for coding agents, backed by colgrep.", - "longDescription": "Exposes colgrep's semantic and hybrid (grep + ranking) code search as MCP tools, resources and prompts, so agents search code by meaning instead of shelling out to grep. Supports 18+ languages and incremental indexing, with agent-facing usage guidance via a bundled skill.", - "developerName": "Eliott Jacopin", - "category": "Developer Tools", - "capabilities": ["Read"], - "defaultPrompt": [ - "Search this repo for the authentication logic.", - "Find where errors are handled for database connections.", - "Show the index status for this project." - ] - } -} diff --git a/AGENTS.md b/AGENTS.md index 22b0ea0..674d322 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,7 +35,7 @@ runs the same roadmap sequentially under the same discipline. Load the ```bash claude --plugin-dir ./dev # from a clone -claude plugin install colgrep-mcp-dev@cracking-shells # from the repo's marketplace +claude plugin install colgrep-mcp-dev@cracking-shells # from CrackingShells/Nest, after `claude plugin marketplace add CrackingShells/Nest` ``` `colgrep-mcp-dev` (`dev/`) ships the maintainer skills; the product plugin @@ -55,7 +55,8 @@ claude plugin install colgrep-mcp-dev@cracking-shells # from the repo's market | `server/colgrep_mcp/` | the server: `server.py` (assembly), `adapter.py` (colgrep subprocess), `tools_search.py`, `tools_index.py`, `resources.py`, `prompts.py`, `errors.py`, `guide.md` (agent guide served as a resource) | implementers | | `server/tests/` | pytest suite against `fake_colgrep.py`; `test_manifests.py`, `test_version.py`, `test_changelog.py`, `test_readme.py`, `test_dev_plugin.py` are drift guards; `e2e/run_e2e.py` drives the real binary (not collected by pytest) | implementers | | `server/pyproject.toml` | the only version source, dependencies, `[tool.commitizen]`, `[tool.ruff]`, pytest config | `cz bump` for the version; humans/agents for the rest | -| `plugin.json`, `mcp.json`, `.claude-plugin/`, `.codex-plugin/`, `.agents/` | product plugin manifests for the three ecosystems; each MCP config (`.claude-plugin/mcp.json`, `.codex-plugin/mcp.json`, `mcp.json`) launches `uvx colgrep-mcp==`; `version` fields and the pins are written by `cz bump` only | launcher/packaging changes | +| `plugin.json`, `mcp.json`, `.claude-plugin/`, `.agents/` | product plugin manifests for the three ecosystems; Codex reads the root `plugin.json` and its `extensions["com.openai"]` block rather than a directory of its own; each MCP config (`.claude-plugin/mcp.json`, `mcp.json`) launches `uvx colgrep-mcp==`; `version` fields and the pins are written by `cz bump` only | launcher/packaging changes | +| `colgrep-mcp.spec.json` | the spec the manifests above are regenerated from, by the playbook's `spawning-agent-plugins` generator. Declares hub mode (`marketplace.hub` names `CrackingShells/Nest`), so regenerating writes no marketplace file here. Must stay byte-identical to the playbook's `assets/examples/colgrep-mcp.spec.json`: that copy is what the playbook's regeneration guard measures against this repo, so a divergence leaves the guard green while measuring a spec nobody uses | maintainers, in lockstep with the playbook's copy | | `skills/colgrep-search/SKILL.md` | the end-user skill that teaches agents when to use the tools | when tool semantics change | | `hooks/` | the plugin hooks: `hooks.json` (events every hook-capable harness knows), one `.json` per event not every harness knows (`worktree-remove.json`), `colgrep_policy.py` (one stdlib script serving all events); pinned by `server/tests/test_hooks.py` | when the search policy or the harness wiring changes | | `dev/` | the `colgrep-mcp-dev` plugin: `skills//` (SKILL.md, `references/`, `scripts/`), `evals/-triggers/case.yaml`; versioned by `cz bump` with the product | maintainers, when a cycle learns something | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5221d64..2df022e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ Pushing the tag is the publish decision: `.github/workflows/publish.yml` builds distribution, uploads it to PyPI through trusted publishing (no token; the publisher registered on PyPI names `publish.yml` and the `pypi` environment) and creates the GitHub release from the tag's `CHANGELOG.md` section. The `uvx colgrep-mcp==` -pin in the three MCP manifests is a `version_files` target, so the bump moves it too. +pin in the two MCP manifests is a `version_files` target, so the bump moves it too. ## Gates diff --git a/README.md b/README.md index 2f1f553..5dc840a 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The server is the [`colgrep-mcp` package on PyPI](https://pypi.org/project/colgr ### Claude Code ```bash -claude plugin marketplace add CrackingShells/colgrep-mcp +claude plugin marketplace add CrackingShells/Nest ``` ```bash @@ -37,14 +37,14 @@ Add `--scope project` to the marketplace command to declare it in the repository ### Codex ```bash -codex plugin marketplace add CrackingShells/colgrep-mcp +codex plugin marketplace add CrackingShells/Nest ``` ```bash codex plugin add colgrep-mcp@cracking-shells ``` -The Codex manifests are `.agents/plugins/marketplace.json` and `.codex-plugin/plugin.json`. +The Codex manifest is the root `plugin.json` (Codex extras live under its `extensions["com.openai"]`); this repository carries no marketplace file of its own — see [Migrating from this repository's old marketplace](#migrating-from-this-repositorys-old-marketplace) below. ### Agent Plugins 1.0 clients (Cursor, GitHub Copilot, VS Code, Kiro) @@ -104,6 +104,20 @@ claude mcp add colgrep -- uv run --quiet --directory /path/to/colgrep-mcp/server `uvx --from /path/to/colgrep-mcp/server colgrep-mcp` is the one-off equivalent. Note that `claude --plugin-dir /path/to/colgrep-mcp` loads the clone's *skill* but launches the manifest's PyPI pin, not the clone's code — use one of the commands above to test a change. +### Migrating from this repository's old marketplace + +This repository used to declare the `cracking-shells` marketplace itself (`.claude-plugin/marketplace.json`, `.agents/plugins/marketplace.json`); that catalogue now lives in [`CrackingShells/Nest`](https://github.com/CrackingShells/Nest), which lists both `colgrep-mcp` and `colgrep-mcp-dev`. A client keeps whichever repository it registered under a marketplace name at add time, so if you already have `cracking-shells` registered as pointing at this repository, adding `CrackingShells/Nest` under the same name does **not** happen automatically — your client will silently keep resolving `cracking-shells` to the old, now-unpublished two-plugin catalogue here, with no error to tell you it's stale. Remove the old registration first: + +```bash +claude plugin marketplace remove cracking-shells +``` + +```bash +codex plugin marketplace remove cracking-shells +``` + +Then add `CrackingShells/Nest` as shown above. `colgrep-mcp@cracking-shells` and `colgrep-mcp-dev@cracking-shells` install the same way afterward — only the marketplace's home moved, not its name. + ## What the agent gets ### Tools @@ -207,9 +221,11 @@ It refuses to run against this repository, its worktrees or anything under `/pri The repository root is simultaneously: -- a [Claude Code](https://code.claude.com/docs/en/plugins-reference) plugin (`.claude-plugin/plugin.json`, `.claude-plugin/mcp.json`) and a one-plugin marketplace (`.claude-plugin/marketplace.json`); +- a [Claude Code](https://code.claude.com/docs/en/plugins-reference) plugin (`.claude-plugin/plugin.json`, `.claude-plugin/mcp.json`); - an [Agent Plugins 1.0](https://agent-plugins.org/specification) plugin (`plugin.json`, `mcp.json`); -- a Codex plugin (`.codex-plugin/plugin.json`, `.codex-plugin/mcp.json`) and marketplace (`.agents/plugins/marketplace.json`). +- a Codex plugin served by the root `plugin.json`'s `extensions["com.openai"]` block, which Codex parses directly (it auto-wires `skills` to `./skills` and the MCP servers to `mcp.json` by convention). + +The `cracking-shells` marketplace catalogue itself — for both Claude Code and Codex — lives in [`CrackingShells/Nest`](https://github.com/CrackingShells/Nest), not in this repository; see [Install](#install). The Claude Code and Codex plugins share the `hooks/` component ([Hooks](#hooks)); its commands carry the one placeholder both ecosystems expand, `${CLAUDE_PLUGIN_ROOT}`. Every MCP manifest launches the same argv, with no shell script and no root placeholder: `uvx colgrep-mcp==`, where the pin is the plugin's own version — `cz bump` rewrites it with the manifests' `version` fields, so a plugin update always launches its matching server and never a stale cached one. The only placeholder left is `COLGREP_MCP_ROOT=${CLAUDE_PROJECT_DIR}` in the Claude Code manifest's `env`, the one client documented to expand it. `uvx` and `colgrep` must be on `PATH` (see Troubleshooting for GUI clients that start without one). CI runs the suite on Windows as well as macOS and Linux, builds the distribution and checks its metadata on every pull request; pushing a release tag runs `.github/workflows/publish.yml`, which uploads to PyPI through trusted publishing and creates the GitHub release. diff --git a/__roadmap__/nest_migration/README.md b/__roadmap__/nest_migration/README.md new file mode 100644 index 0000000..9fb4a71 --- /dev/null +++ b/__roadmap__/nest_migration/README.md @@ -0,0 +1,58 @@ +# Nest Migration + +## Context +colgrep-mcp currently owns the `cracking-shells` marketplace name in its own repository, listing +`colgrep-mcp` and the maintainer plugin `colgrep-mcp-dev`. `CrackingShells/Nest` is taking that name +over as the organisation's single catalogue. This campaign hands the name across and moves this +repo's manifests to the reshaped generator's output. It is **fully gated** on the +`agent_plugin_nest` campaign in the playbook: nothing here starts until Nest provably installs +plugins, because relinquishing a working catalogue before its replacement is proven would leave +users with neither. + +## Reference Documents +- [R01 Implementation Plan](~/.claude/plans/good-news-overall-it-s-gleaming-wreath.md) — the campaign gate and the Nest entry shape +- [R02 Traps](https://github.com/CrackingShells/cracking-shells-playbook/blob/main/skills/spawning-agent-plugins/references/traps.md) — marketplace naming and the hooks-duplicate install failure (lives in the playbook repo, not here) + +## Goal +colgrep-mcp ships no marketplace, points users at Nest, and carries manifests in the Agent-Plugins-plus-extensions shape. + +## Pre-conditions +- [ ] `relinquish_marketplace` waits on `agent_plugin_nest/generator/rollout/verify/end_to_end.md` — Nest must install playbook plugins for real before this repo gives up a working catalogue +- [ ] `regenerate_manifests` waits only on `agent_plugin_nest/generator/generator_reshape.md`, so it can start earlier than its sibling +- [ ] Nest lists `colgrep-mcp` and `colgrep-mcp-dev`, so nothing is dropped when this repo stops listing them +- [ ] A playbook checkout containing the reshaped generator is available, and its path is named in the implementer's brief + +## Success Gates +- ✅ Neither `.claude-plugin/marketplace.json` nor `.agents/plugins/marketplace.json` exists [run] +- ✅ No `.codex-plugin/` directory remains [run] +- ✅ `check_plugin.py` reports no problems for this repo [run] +- ✅ README install snippets name `CrackingShells/Nest` and no longer name `CrackingShells/colgrep-mcp` [run] +- ✅ Installing colgrep-mcp from Nest connects the MCP server and fires the hooks [behavioral] + +## Status +```mermaid +graph TD + relinquish_marketplace[Relinquish Marketplace]:::done + regenerate_manifests[Regenerate Manifests]:::done + verify[Verification]:::inprogress + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes +| Node | Type | Status | +|:-----|:-----|:-------| +| `relinquish_marketplace.md` | 📄 Leaf Task | ✅ Done | +| `regenerate_manifests.md` | 📄 Leaf Task | ✅ Done | +| `verify/` | 📁 Directory | 🔄 In Progress | + +## Amendment Log +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| + +## Progress +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| diff --git a/__roadmap__/nest_migration/regenerate_manifests.md b/__roadmap__/nest_migration/regenerate_manifests.md new file mode 100644 index 0000000..970dd42 --- /dev/null +++ b/__roadmap__/nest_migration/regenerate_manifests.md @@ -0,0 +1,47 @@ +# Regenerate Manifests + +**Goal**: Move this repository's manifests to the reshaped generator's output — a root Agent-Plugins `plugin.json` carrying `extensions["com.openai"]`, with `.codex-plugin/` gone and no marketplace written back. +**Pre-conditions**: +- [ ] `agent_plugin_nest/generator/generator_reshape.md` is merged and its regeneration guard is green on the new baseline +- [ ] A playbook checkout containing the **reshaped** generator is available; its absolute path is named in the brief +**Success Gates**: +- ⬜ No `.codex-plugin/` directory remains [run] +- ⬜ Root `plugin.json` carries `extensions["com.openai"]` with the full interface block [run] +- ⬜ Regenerating writes no marketplace file into this repository [run] +- ⬜ `check_plugin.py` reports no problems [run] +- ⬜ The version in every manifest is still `0.5.1`; this leaf reshapes, it does not release [run] +**References**: [R01 §Decisions already settled](~/.claude/plans/good-news-overall-it-s-gleaming-wreath.md) — why the extensions namespace replaces `.codex-plugin/` + +## Step 1: Switch the spec to hub mode + +**Goal**: Stop the spec from declaring a marketplace, so regeneration cannot resurrect one. + +**Implementation Logic**: +The spec still carries `claude_marketplace` and `codex.marketplace_name`, which is what made this repository the owner of the `cracking-shells` name. Replace them with the generator's hub mode, so `spawn` writes no `.claude-plugin/marketplace.json` and no `.agents/plugins/marketplace.json`. + +This step exists because of an ordering hazard, and skipping it is silent: regenerating with the old spec writes both marketplace files back, undoing `relinquish_marketplace` without any error, and leaving two repositories declaring one marketplace name again — the exact condition this whole migration removes. With hub mode set, the two leaves become genuinely order-independent. +**Deliverables**: the spec consumed by `spawn` (the playbook's `assets/examples/colgrep-mcp.spec.json`, or a repo-local copy if the implementer prefers not to edit the skill's example) — `claude_marketplace` and `codex.marketplace_name` replaced by the hub-mode key +**Consistency Checks**: `test ! -f .claude-plugin/marketplace.json || echo "marketplace still present - relinquish has not run yet, which is allowed"` (expected: PASS) +**Commit**: `refactor(plugin): declare hub mode so no marketplace is generated here` + +## Step 2: Regenerate with the reshaped generator + +**Goal**: Produce the new manifest shape from the spec rather than by hand. + +**Implementation Logic**: +Run the reshaped `spawn_plugin.py --root . spawn --spec --force` from **the playbook checkout that contains the reshape**. Running the unextended generator would regenerate the old three-manifest shape and appear to succeed; the brief must name the path explicitly, and the implementer should confirm the generator it invoked actually emits `extensions` rather than `.codex-plugin/` before trusting the result. + +`--force` is required because the manifests already exist and `put` refuses to overwrite without it. With hub mode set in step 1 there is no `merge_marketplace` call for `--force` to turn destructive, which is the whole reason step 1 comes first. Afterwards delete the `.codex-plugin/` directory: the new generator no longer writes it, but it does not remove what an earlier run left behind. +**Deliverables**: `plugin.json` — `extensions["com.openai"]` containing the interface block and the Codex hooks path; `.claude-plugin/plugin.json` unchanged in shape; deletion of `.codex-plugin/` +**Consistency Checks**: `test ! -d .codex-plugin && python3 -c "import json;d=json.load(open('plugin.json'));assert 'com.openai' in d['extensions']"` (expected: PASS) +**Commit**: `refactor(plugin): express Codex through the com.openai extensions namespace` + +## Step 3: Check the tree against itself + +**Goal**: Confirm the reshape left a self-consistent plugin, not merely a changed one. + +**Implementation Logic**: +Run `check_plugin.py` and `claude plugin validate .` over the repo. Confirm the version is still `0.5.1` everywhere including the `uvx` pin in the MCP manifests — this leaf changes manifest *shape*, and a version move here would desynchronise the pin from the published artifact and break every launch until a release caught up. Confirm no marketplace file reappeared. Do not point Claude's validator at anything but the Claude manifest and the repo root; it rejects the Codex `interface` block as an unknown field, which is a false alarm rather than a finding. +**Deliverables**: no new files — validator output recorded in the commit body +**Consistency Checks**: `python3 -c "import json;d=json.load(open('plugin.json'));assert d['version']=='0.5.1'" && test ! -f .claude-plugin/marketplace.json` (expected: PASS) +**Commit**: `test(plugin): verify the reshaped manifests are self-consistent` diff --git a/__roadmap__/nest_migration/relinquish_marketplace.md b/__roadmap__/nest_migration/relinquish_marketplace.md new file mode 100644 index 0000000..cc126f7 --- /dev/null +++ b/__roadmap__/nest_migration/relinquish_marketplace.md @@ -0,0 +1,32 @@ +# Relinquish Marketplace + +**Goal**: Stop this repository from declaring the `cracking-shells` marketplace, and point every install instruction at Nest instead. +**Pre-conditions**: +- [ ] Nest lists both `colgrep-mcp` and `colgrep-mcp-dev`, verified by reading Nest's marketplace files +- [ ] The playbook campaign's end-to-end install verification is done +**Success Gates**: +- ⬜ `.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json` no longer exist [run] +- ⬜ `.claude-plugin/plugin.json` and `.claude-plugin/mcp.json` still exist and are untouched [run] +- ⬜ No file in the repo still contains `marketplace add CrackingShells/colgrep-mcp` [run] +- ⬜ README carries a migration note telling existing users to remove the old marketplace first [run] +**References**: [R02 §namespaces](https://github.com/CrackingShells/cracking-shells-playbook/blob/main/skills/spawning-agent-plugins/references/traps.md#namespaces) — a client keeps the marketplace name it had at add time + +## Step 1: Delete the two marketplace files + +**Goal**: Leave exactly one repository in the organisation declaring `cracking-shells`. + +**Implementation Logic**: +Delete `.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json`. Delete only these two: `.claude-plugin/plugin.json` and `.claude-plugin/mcp.json` are this plugin's own manifests and must survive — a plugin stops *publishing a catalogue*, it does not stop being a plugin. Both files currently declare `name: "cracking-shells"`, and the Claude one lists `colgrep-mcp` plus `colgrep-mcp-dev`; confirm both names appear in Nest's catalogue before deleting, because after this commit nothing in this repository records that the dev plugin was ever published. +**Deliverables**: deletion of `.claude-plugin/marketplace.json` and `.agents/plugins/marketplace.json` +**Consistency Checks**: `test ! -f .claude-plugin/marketplace.json && test ! -f .agents/plugins/marketplace.json && test -f .claude-plugin/plugin.json` (expected: PASS) +**Commit**: `refactor(marketplace): hand the cracking-shells catalogue to CrackingShells/Nest` + +## Step 2: Repoint the install instructions + +**Goal**: Stop telling readers to add a marketplace this repo no longer serves. + +**Implementation Logic**: +Rewrite the README's install section so both harnesses add `CrackingShells/Nest` rather than `CrackingShells/colgrep-mcp`; the right-hand side of `colgrep-mcp@cracking-shells` is unchanged, because the marketplace name is the same — only its home moved. Add a migration note for existing users: a client keeps whichever marketplace it registered under a name at add time, so anyone who added `cracking-shells` from this repo must run `claude plugin marketplace remove cracking-shells` before adding Nest, or they will silently keep the old two-plugin catalogue. Sweep the whole repository, not just the README — the install snippet is reproduced in `dev/README.md` and may appear in reports. +**Deliverables**: `README.md` — install section repointed at Nest, new migration subsection; any other file carrying the old snippet +**Consistency Checks**: `test $(grep -rl "marketplace add CrackingShells/colgrep-mcp" . --exclude-dir=.git | wc -l) -eq 0` (expected: PASS) +**Commit**: `docs(readme): point installs at the Nest marketplace and note the migration` diff --git a/__roadmap__/nest_migration/verify/README.md b/__roadmap__/nest_migration/verify/README.md new file mode 100644 index 0000000..16562b7 --- /dev/null +++ b/__roadmap__/nest_migration/verify/README.md @@ -0,0 +1,43 @@ +# Verification + +## Context +Sits below both leaves as verification after parallel siblings: it needs the marketplace gone *and* +the manifests reshaped. Exercises the real marketplace-install path, which is the only one that +reproduces the hooks-duplicate class of failure. Produces the evidence that this repo's migration is +complete. + +## Goal +Prove colgrep-mcp installs from Nest with its server connected and its hooks firing. + +## Pre-conditions +- [ ] `relinquish_marketplace` and `regenerate_manifests` are both done and pushed +- [ ] The local `cracking-shells` registration already points at Nest + +## Success Gates +- ✅ `claude plugin list` shows `colgrep-mcp` with no `failed to load` line [run] +- ✅ `claude mcp list` reports the colgrep server connected [run] +- ✅ The search policy hook fires in a fresh session [behavioral] + +## Status +```mermaid +graph TD + install_check[Install Check]:::planned + classDef done fill:#166534,color:#bbf7d0 + classDef inprogress fill:#854d0e,color:#fef08a + classDef planned fill:#374151,color:#e5e7eb + classDef amendment fill:#1e3a5f,color:#bfdbfe + classDef blocked fill:#7f1d1d,color:#fecaca +``` + +## Nodes +| Node | Type | Status | +|:-----|:-----|:-------| +| `install_check.md` | 📄 Leaf Task | ⬜ Planned | + +## Amendment Log +| ID | Date | Source | Nodes Added | Rationale | +|:---|:-----|:-------|:------------|:----------| + +## Progress +| Node | Branch | Commits | Notes | +|:-----|:-------|:--------|:------| diff --git a/__roadmap__/nest_migration/verify/install_check.md b/__roadmap__/nest_migration/verify/install_check.md new file mode 100644 index 0000000..e56f2e5 --- /dev/null +++ b/__roadmap__/nest_migration/verify/install_check.md @@ -0,0 +1,34 @@ +# Install Check + +**Goal**: Prove colgrep-mcp installs from Nest with its server connected and its hooks firing, closing the migration. +**Pre-conditions**: +- [ ] `relinquish_marketplace` and `regenerate_manifests` are both merged and pushed — a git source resolves against the remote, not the worktree +- [ ] The local `cracking-shells` registration already points at `CrackingShells/Nest` +**Success Gates**: +- ⬜ `claude plugin list` shows `colgrep-mcp` with no `failed to load` line [run] +- ⬜ `claude mcp list` reports the colgrep server connected [run] +- ⬜ The search policy hook fires at the start of a fresh session [behavioral] +- ⬜ `colgrep-mcp-dev` is installable from Nest as well [run] +**References**: [R02 §hooks-manifest-duplicate](https://github.com/CrackingShells/cracking-shells-playbook/blob/main/skills/spawning-agent-plugins/references/traps.md#hooks-manifest-duplicate) — why only a marketplace install reproduces this failure + +## Step 1: Install from Nest and read the load state + +**Goal**: Exercise the path that no tree-level command reproduces. + +**Implementation Logic**: +Reinstall `colgrep-mcp@cracking-shells` from Nest and read `claude plugin list`. This is the only path that surfaces "Duplicate hooks file detected": `--plugin-dir`, `plugin details` and `plugin validate` all accept a tree that fails a real marketplace install, and this repo has hit exactly that failure before, at 0.4.0 and 0.5.0. Install `colgrep-mcp-dev` too — it moved catalogues in this migration and has never been installed from Nest. + +Then confirm the server actually starts with `claude mcp list`, and open a fresh session to confirm the search policy hook fires. A plugin can load with its hooks silently inert, which is the failure this check exists to catch. +**Deliverables**: no files — observed outputs recorded in the commit body +**Consistency Checks**: `claude plugin list 2>&1 | grep -c "failed to load" | grep -qx 0` (expected: PASS) +**Commit**: `test(plugin): verify colgrep-mcp installs from the Nest marketplace` + +## Step 2: Record what the migration proved + +**Goal**: Close the campaign with an honest account of what was exercised. + +**Implementation Logic**: +Write a findings report under `__reports__/nest_migration/`. Separate what was exercised live in Claude Code — marketplace install, server connection, hooks firing — from what was only validated statically. No Codex CLI is available here, so every Codex claim rests on the published manifest format and the loader source, and must be labelled as such; `manifests.md` already keeps a verification-status table that draws this line, and it must not blur now that the catalogue has moved. Note any user-visible migration step still outstanding, in particular that existing clients keep the old marketplace until they remove it by hand. +**Deliverables**: `__reports__/nest_migration/00-findings_migration_v0.md` — sections Exercised, Validated only, Outstanding user actions +**Consistency Checks**: `test -f __reports__/nest_migration/00-findings_migration_v0.md` (expected: PASS) +**Commit**: `docs(reports): record the Nest migration outcome and its limits` diff --git a/colgrep-mcp.spec.json b/colgrep-mcp.spec.json new file mode 100644 index 0000000..be88336 --- /dev/null +++ b/colgrep-mcp.spec.json @@ -0,0 +1,97 @@ +{ + "name": "colgrep-mcp", + "displayName": "colgrep", + "description": "Semantic and hybrid code search for coding agents, exposed as MCP tools backed by colgrep.", + "author": { + "name": "Eliott Jacopin", + "email": "eliott.jacopin@riken.jp" + }, + "homepage": "https://github.com/CrackingShells/colgrep-mcp", + "repository": "https://github.com/CrackingShells/colgrep-mcp", + "license": "AGPL-3.0-or-later", + "keywords": [ + "mcp", + "colgrep", + "semantic-search", + "code-search", + "agents" + ], + "version_from": "pyproject:server/pyproject.toml", + "marketplace": { + "hub": "https://github.com/CrackingShells/Nest" + }, + "ecosystems": [ + "claude", + "codex", + "agent-plugins" + ], + "skills": "./skills/", + "mcp": { + "server": "colgrep", + "command": "uvx", + "args": [ + "colgrep-mcp=={version}" + ], + "claude_env": { + "COLGREP_MCP_ROOT": "${CLAUDE_PROJECT_DIR}" + } + }, + "hooks": { + "script": "hooks/colgrep_policy.py", + "codex": true, + "portable_description": "colgrep-mcp: state the search policy at session and subagent start; deny the built-in Grep tool and shell corpus searches in favour of the colgrep MCP tools. Only events every hook-capable harness (Claude Code, Codex, Cursor) understands live here, in the one file both Claude Code and Codex load by default; an event not every harness knows gets its own file named after it (worktree-remove.json).", + "portable": [ + { + "event": "SessionStart", + "statusMessage": "Loading the colgrep search policy" + }, + { + "event": "SubagentStart", + "statusMessage": "Loading the colgrep search policy" + }, + { + "event": "PreToolUse", + "matcher": "Grep|Bash" + } + ], + "extra": [ + { + "event": "WorktreeRemove", + "timeout": 90, + "description": "colgrep-mcp: the WorktreeRemove event, which only Claude Code fires. One file per event that not every hook-capable harness knows, named after the event; named from .claude-plugin/plugin.json only, never from the Codex manifest, so a parser that knows no WorktreeRemove never sees it. Clears the colgrep index a removed worktree owned (never one it was folded into)." + } + ] + }, + "codex": { + "marketplace_name": "cracking-shells", + "shortDescription": "Semantic and hybrid code search for coding agents, backed by colgrep.", + "longDescription": "Exposes colgrep's semantic and hybrid (grep + ranking) code search as MCP tools, resources and prompts, so agents search code by meaning instead of shelling out to grep. Supports 18+ languages and incremental indexing, with agent-facing usage guidance via a bundled skill.", + "category": "Developer Tools", + "capabilities": [ + "Read" + ], + "defaultPrompt": [ + "Search this repo for the authentication logic.", + "Find where errors are handled for database connections.", + "Show the index status for this project." + ], + "marketplace_displayName": "CrackingShells" + }, + "claude_marketplace": { + "name": "cracking-shells", + "description": "CrackingShells marketplace. Here: colgrep-mcp, the product plugin (semantic and hybrid code search as MCP tools), and colgrep-mcp-dev, the maintainer's plugin of skills." + }, + "dev": { + "dir": "dev", + "name": "colgrep-mcp-dev", + "displayName": "colgrep-mcp maintainer skills", + "description": "The maintainer's dev environment for colgrep-mcp as skills: policy, campaign leadership, landing and release mechanics, stack traps. For agents maintaining this repository, not for end users of the colgrep tools.", + "marketplace_description": "Maintainer skills for this repository (policy, campaign leadership, landing and release, stack traps). Not for end users.", + "keywords": [ + "colgrep-mcp", + "maintainer", + "skills", + "agents" + ] + } +} diff --git a/dev/.claude-plugin/plugin.json b/dev/.claude-plugin/plugin.json index f63ab18..ebf2247 100644 --- a/dev/.claude-plugin/plugin.json +++ b/dev/.claude-plugin/plugin.json @@ -10,6 +10,11 @@ "homepage": "https://github.com/CrackingShells/colgrep-mcp", "repository": "https://github.com/CrackingShells/colgrep-mcp", "license": "AGPL-3.0-or-later", - "keywords": ["colgrep-mcp", "maintainer", "skills", "agents"], + "keywords": [ + "colgrep-mcp", + "maintainer", + "skills", + "agents" + ], "skills": "./skills/" } diff --git a/dev/README.md b/dev/README.md index c035995..d0c42e4 100644 --- a/dev/README.md +++ b/dev/README.md @@ -7,7 +7,7 @@ installs it: ```bash claude --plugin-dir ./dev # from a clone -claude plugin install colgrep-mcp-dev@cracking-shells # from the repo's own marketplace +claude plugin install colgrep-mcp-dev@cracking-shells # from CrackingShells/Nest, after `claude plugin marketplace add CrackingShells/Nest` ``` The product plugin (`colgrep-mcp`, repository root) never ships these skills; diff --git a/dev/skills/landing-and-release/SKILL.md b/dev/skills/landing-and-release/SKILL.md index d116d73..b4f915c 100644 --- a/dev/skills/landing-and-release/SKILL.md +++ b/dev/skills/landing-and-release/SKILL.md @@ -92,9 +92,9 @@ uv run pytest # uv run re-syncs the editable install git push origin main v # the tag is lightweight; --follow-tags skips it ``` -`cz bump` rewrites `pyproject.toml`, `uv.lock` (pre-bump hook), the four -version-tracked manifests, the `uvx colgrep-mcp==` pin in the three -MCP manifests, and `CHANGELOG.md`, and writes its own +`cz bump` rewrites `pyproject.toml`, `uv.lock` (pre-bump hook), the three +version-tracked plugin manifests, the `uvx colgrep-mcp==` pin in the +two MCP manifests, and `CHANGELOG.md`, and writes its own `release(colgrep-mcp): v` commit — never author that commit or edit a version by hand; a version-drift test failing means a file was hand-edited, not that the environment is stale. (`OBS-H` Check 4, `CONTRIBUTING.md` diff --git a/dev/skills/maintainer-policy/references/drift-tests.md b/dev/skills/maintainer-policy/references/drift-tests.md index 543471c..e1e53cf 100644 --- a/dev/skills/maintainer-policy/references/drift-tests.md +++ b/dev/skills/maintainer-policy/references/drift-tests.md @@ -13,7 +13,7 @@ in a leaf or a PR template. | Test file | What it pins | |:--|:--| | `tests/test_version.py` | `colgrep_mcp.__version__` equals `pyproject.toml`'s version; every manifest's `version` field equals it too; `uv.lock` records exactly that version for this package. | -| `tests/test_manifests.py` | All four plugin manifests (`plugin.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, `dev/.claude-plugin/plugin.json`) carry the same version; the Agent Plugins 1.0 manifest only uses its permitted field set and the right `$schema`; the three ecosystems' MCP server configs launch the server the same way (same `uv run` invocation shape, no unexpanded `$` placeholders outside project scope). | +| `tests/test_manifests.py` | All three plugin manifests (`plugin.json`, `.claude-plugin/plugin.json`, `dev/.claude-plugin/plugin.json`) carry the same version; the Agent Plugins 1.0 manifest only uses its permitted field set and the right `$schema`; its `extensions["com.openai"]` block carries the full Codex interface block and hooks path, which is how Codex is served now that `.codex-plugin/` is gone; the two MCP server configs launch the server the same way (same `uv run` invocation shape, no unexpanded `$` placeholders outside project scope). | | `tests/test_changelog.py` | Every `## ` version heading in `CHANGELOG.md` is shaped so commitizen's incremental changelog mode can parse it — a hand-written Keep-a-Changelog heading in the wrong shape would make `cz bump --changelog` duplicate that section instead of appending to it. | | `tests/test_readme.py` | The tool names listed in the Tools table of `README.md` and of `server/README.md` (the PyPI page) match the tools the server actually registers — a renamed or removed tool that isn't updated in a README fails here instead of shipping stale docs. | | `tests/test_packaging.py` | `server/LICENSE` is a byte copy of the repository `LICENSE` (hatchling only packages files under `server/`); `[project.urls]` point at the repository; no tracked text file carries a path from a maintainer's machine. | diff --git a/dev/skills/stack-traps/SKILL.md b/dev/skills/stack-traps/SKILL.md index 557f86b..60aa63d 100644 --- a/dev/skills/stack-traps/SKILL.md +++ b/dev/skills/stack-traps/SKILL.md @@ -25,6 +25,7 @@ linked section, don't re-derive the trap from scratch. | a Windows client's root path won't resolve | mcp-sdk | `references/mcp-sdk.md#windows-roots` | | opening this repo as a plain project shows a pending colgrep entry or a `spawn ENOENT` | claude-code | `references/claude-code.md#root-mcp-json` | | an install/list command's plugin and marketplace names look mismatched or don't carry to another ecosystem | claude-code | `references/claude-code.md#namespaces` | +| `claude plugin validate .` reports a warning it never used to, or you want to know which manifest it actually checked | claude-code | `references/claude-code.md#validate-picks-one` | | `claude -p` or `claude plugin eval` fails with an OAuth error | claude-code | `references/claude-code.md#oauth` | | `claude --plugin-dir . mcp list` says Connected but your edit is not in the server, or the plugin fails right after a bump | claude-code | `references/claude-code.md#uvx-pin` | | the plugin's hooks don't fire, or still run the old text, after an edit or an update; Codex lists them but never runs them | claude-code | `references/claude-code.md#plugin-hooks` | diff --git a/dev/skills/stack-traps/references/claude-code.md b/dev/skills/stack-traps/references/claude-code.md index b734e86..5ab665a 100644 --- a/dev/skills/stack-traps/references/claude-code.md +++ b/dev/skills/stack-traps/references/claude-code.md @@ -90,6 +90,38 @@ reinstall `colgrep-mcp@cracking-shells`. When writing install instructions for a second ecosystem, copy that ecosystem's own marketplace `name`; here the two happen to match, which is a choice, not a rule. +## `claude plugin validate .` validates the marketplace, not the plugin {#validate-picks-one} + +`claude plugin validate ` validates **one** manifest: it looks for +`.claude-plugin/marketplace.json` first and, finding one, validates that and +stops. It never reaches `.claude-plugin/plugin.json`. So for as long as this +repository shipped its own marketplace, the gate `claude plugin validate .` +in `AGENTS.md` was checking the catalogue and nothing else — the plugin +manifest was unvalidated by that command, and a green result said less than +it appeared to. + +Deleting the marketplace in the `nest_migration` campaign made the command +fall through to the plugin manifest for the first time, which immediately +surfaced a warning that had been latent all along: + +``` +⚠ root: CLAUDE.md at the plugin root is not loaded as project context. + To ship context with your plugin, use a skill (skills//SKILL.md) instead. +``` + +That warning is **expected and benign here, and `CLAUDE.md` must stay.** It is +an eleven-byte `@AGENTS.md` pointer that exists for agents working in a clone +of this repository; it is not meant to travel to end users who install the +plugin, and the maintainer knowledge it points at ships separately as the +`colgrep-mcp-dev` plugin's skills. The validator is right that it does +nothing for an installed plugin, and that is fine. + +The trap is the masking, not the warning: a validator that silently changes +*what it validates* depending on which manifest it finds means "validation +passed" is only as strong as the manifest it happened to pick. When you want +the plugin manifest checked, point the command at a tree with no marketplace +in it, or read the first line of the output — it names the manifest it chose. + ## `claude -p` / `claude plugin eval` fails with an OAuth error {#oauth} **Symptom**: `claude -p` fails with something like `Failed to authenticate: diff --git a/mcp.json b/mcp.json index 0abc598..1cebb45 100644 --- a/mcp.json +++ b/mcp.json @@ -4,7 +4,9 @@ "colgrep": { "type": "stdio", "command": "uvx", - "args": ["colgrep-mcp==0.5.1"] + "args": [ + "colgrep-mcp==0.5.1" + ] } } } diff --git a/plugin.json b/plugin.json index 710a5f5..dc54500 100644 --- a/plugin.json +++ b/plugin.json @@ -10,5 +10,31 @@ "homepage": "https://github.com/CrackingShells/colgrep-mcp", "repository": "https://github.com/CrackingShells/colgrep-mcp", "license": "AGPL-3.0-or-later", - "keywords": ["mcp", "colgrep", "semantic-search", "code-search", "agents"] + "keywords": [ + "mcp", + "colgrep", + "semantic-search", + "code-search", + "agents" + ], + "extensions": { + "com.openai": { + "interface": { + "displayName": "colgrep", + "shortDescription": "Semantic and hybrid code search for coding agents, backed by colgrep.", + "longDescription": "Exposes colgrep's semantic and hybrid (grep + ranking) code search as MCP tools, resources and prompts, so agents search code by meaning instead of shelling out to grep. Supports 18+ languages and incremental indexing, with agent-facing usage guidance via a bundled skill.", + "developerName": "Eliott Jacopin", + "category": "Developer Tools", + "capabilities": [ + "Read" + ], + "defaultPrompt": [ + "Search this repo for the authentication logic.", + "Find where errors are handled for database connections.", + "Show the index status for this project." + ] + }, + "hooks": "./hooks/hooks.json" + } + } } diff --git a/server/pyproject.toml b/server/pyproject.toml index fb07c2d..f0bb504 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -57,16 +57,18 @@ update_changelog_on_bump = true changelog_incremental = true changelog_file = "../CHANGELOG.md" version_files = [ + # Codex parses this same root manifest natively (extras under + # extensions["com.openai"]); it has no plugin.json of its own since + # nest_migration/regenerate_manifests retired .codex-plugin/. "../plugin.json:\"version\"", "../.claude-plugin/plugin.json:\"version\"", - "../.codex-plugin/plugin.json:\"version\"", "../dev/.claude-plugin/plugin.json:\"version\"", - # The three MCP manifests launch `uvx colgrep-mcp==`: the pin is + # The two MCP manifests launch `uvx colgrep-mcp==`: the pin is # the plugin version by construction, and moving it changes uvx's cache # key so a plugin update never reuses a stale server environment - # (pypi_publication R01 §C6, D4). + # (pypi_publication R01 §C6, D4). Codex reads the same root mcp.json as + # Agent Plugins 1.0, so one entry covers both. "../.claude-plugin/mcp.json:colgrep-mcp==", - "../.codex-plugin/mcp.json:colgrep-mcp==", "../mcp.json:colgrep-mcp==", # uv.lock records this project's own version too. The pattern below never # contains a version string, so commitizen rewrites nothing here, but it diff --git a/server/tests/test_dev_plugin.py b/server/tests/test_dev_plugin.py index 17e2bb0..d881b12 100644 --- a/server/tests/test_dev_plugin.py +++ b/server/tests/test_dev_plugin.py @@ -82,11 +82,27 @@ def test_dev_plugin_manifest_is_the_versioned_skills_plugin(): assert "mcpServers" not in manifest, "the dev plugin carries knowledge, never a server" -def test_marketplace_lists_both_plugins_from_disjoint_sources(): - plugins = {p["name"]: p for p in _load(".claude-plugin/marketplace.json")["plugins"]} - assert set(plugins) == {"colgrep-mcp", "colgrep-mcp-dev"} - assert plugins["colgrep-mcp-dev"]["source"] == "./dev" - assert plugins["colgrep-mcp"]["source"] == "./" +def test_product_and_dev_plugins_are_named_and_placed_disjointly(): + """This used to read the two plugins' entries out of `.claude-plugin/marketplace.json` + (name, source), but relinquish_marketplace deleted that file: the catalogue that lists + both plugins now lives in CrackingShells/Nest, not in this repository. What is still + ours to guard is that the two manifests name disjoint plugins at disjoint locations.""" + product = _load(".claude-plugin/plugin.json") + dev = _load("dev/.claude-plugin/plugin.json") + assert product["name"] == "colgrep-mcp" + assert dev["name"] == "colgrep-mcp-dev" + + # Resolve each manifest's *declared* skills path against its own plugin root. Comparing + # two hardcoded manifest paths instead would be a tautology: distinct literals are never + # equal, so the assertion could not fail and would guard nothing. + product_skills = (REPO_ROOT / product["skills"]).resolve() + dev_skills = (DEV_PLUGIN / dev["skills"]).resolve() + assert product_skills.is_dir(), product_skills + assert dev_skills.is_dir(), dev_skills + assert product_skills != dev_skills + assert not product_skills.is_relative_to(DEV_PLUGIN), ( + "the product plugin must not point its skills at the maintainer tree" + ) def test_product_plugin_never_ships_the_dev_skills(): diff --git a/server/tests/test_hooks.py b/server/tests/test_hooks.py index a05b052..387875a 100644 --- a/server/tests/test_hooks.py +++ b/server/tests/test_hooks.py @@ -368,13 +368,15 @@ def test_manifests_name_the_hook_files_per_ecosystem(): """The two loaders read the same field with opposite semantics. Claude Code always loads `hooks/hooks.json` and treats `hooks` as *additional* files: naming the default again fails the whole plugin at install time ("Duplicate hooks file detected", Claude Code 2.1.270) while - `--plugin-dir` accepts it silently. Codex discovers `hooks/hooks.json` only when the manifest + `--plugin-dir` accepts it silently. Codex discovers `hooks/hooks.json` only when its manifest defines no `hooks`, and an explicit value *replaces* that discovery. So the Claude manifest - names exactly the extra files (today all Claude-only) and the Codex manifest the portable - file (stack-traps `claude-code.md#hooks-manifest-duplicate`).""" + names exactly the extra files (today all Claude-only) and Codex's own extras — now + `plugin.json`'s `extensions["com.openai"]`, since nest_migration/regenerate_manifests retired + the separate `.codex-plugin/` manifest — name the portable file (stack-traps + `claude-code.md#hooks-manifest-duplicate`).""" claude = _load(REPO_ROOT / ".claude-plugin" / "plugin.json") - codex = _load(REPO_ROOT / ".codex-plugin" / "plugin.json") agent = _load(REPO_ROOT / "plugin.json") + codex_ext = agent["extensions"]["com.openai"] named = [claude["hooks"]] if isinstance(claude["hooks"], str) else list(claude["hooks"]) assert "./hooks/hooks.json" not in named, ( @@ -382,8 +384,8 @@ def test_manifests_name_the_hook_files_per_ecosystem(): ) assert set(named) == {f"./hooks/{p.name}" for p in EXTRA_FILES}, "every extra file is Claude-only today" assert claude["hooks"] == "./hooks/worktree-remove.json" - assert codex["hooks"] == "./hooks/hooks.json", "Codex: the field replaces default discovery, so name hooks.json" - assert "hooks" not in agent, "Agent Plugins 1.0 defines no hooks component" + assert codex_ext["hooks"] == "./hooks/hooks.json", "Codex: the field replaces default discovery, so name hooks.json" + assert "hooks" not in agent, "Agent Plugins 1.0 defines no hooks component at its top level" def test_hook_script_is_stdlib_only_and_never_imports_the_server(): diff --git a/server/tests/test_manifests.py b/server/tests/test_manifests.py index 0e8dd00..0390cb7 100644 --- a/server/tests/test_manifests.py +++ b/server/tests/test_manifests.py @@ -1,10 +1,14 @@ """Consistency guards across the plugin packaging manifests. -`plugin.json` and `mcp.json` at the repo root, `.claude-plugin/plugin.json` -plus `.claude-plugin/mcp.json`, and `.codex-plugin/plugin.json` plus -`.codex-plugin/mcp.json` describe the same server to three different plugin -ecosystems (Claude Code, Agent Plugins 1.0, Codex). Nothing enforces that -they stay in sync on a version bump or a manifest edit except these tests. +`plugin.json` and `mcp.json` at the repo root, plus `.claude-plugin/plugin.json` +and `.claude-plugin/mcp.json`, describe the same server to Claude Code, Codex +and Agent Plugins 1.0. Codex parses the root Agent Plugins manifest natively +and auto-wires its MCP servers from `mcp.json` by convention (`references/ +manifests.md#codex`, nest_migration `regenerate_manifests`); its own extras +live under `plugin.json`'s `extensions["com.openai"]` rather than in a +`.codex-plugin/` directory of its own, which no longer exists. Nothing +enforces that these files stay in sync on a version bump or a manifest edit +except these tests. Every MCP manifest launches `uvx colgrep-mcp==` from PyPI: no root placeholder anywhere in `args`, because a client that leaves @@ -16,10 +20,10 @@ The Claude Code manifest lives at `.claude-plugin/mcp.json`, not at a root-level `.mcp.json`: Claude Code's project-scope MCP auto-discovery only ever looks for a root `.mcp.json`, and this repository is itself sometimes -opened as a plain project rather than loaded as a plugin. Codex has its own -`.codex-plugin/mcp.json` so that the only placeholder left — Claude Code's -`COLGREP_MCP_ROOT=${CLAUDE_PROJECT_DIR}` in `env` — is read only by the -client documented to expand it. +opened as a plain project rather than loaded as a plugin. Codex reads the +root `mcp.json` (the same file Agent Plugins 1.0 clients read) so that the +only placeholder left — Claude Code's `COLGREP_MCP_ROOT=${CLAUDE_PROJECT_DIR}` +in `env` — is read only by the client documented to expand it. """ from __future__ import annotations @@ -47,7 +51,7 @@ FORBIDDEN_MCP_ENV_KEYS = {"PLUGIN_ROOT", "PLUGIN_DATA"} -MCP_MANIFESTS = (".claude-plugin/mcp.json", ".codex-plugin/mcp.json", "mcp.json") +MCP_MANIFESTS = (".claude-plugin/mcp.json", "mcp.json") def _load(relpath: str) -> dict: @@ -67,8 +71,9 @@ def test_versions_aligned(): version = colgrep_mcp.__version__ assert _load(".claude-plugin/plugin.json")["version"] == version + # Codex parses this same root manifest natively (no manifest of its own), + # so checking it here also covers Codex's version. assert _load("plugin.json")["version"] == version - assert _load(".codex-plugin/plugin.json")["version"] == version # The dev plugin is versioned with the product: its skills describe how to # maintain *this* repository at *this* version, so one `cz bump` moves both. assert _load("dev/.claude-plugin/plugin.json")["version"] == version @@ -81,6 +86,17 @@ def test_agent_plugin_fields_whitelist(): assert manifest["$schema"] == "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" +def test_codex_extension_carries_the_interface_block(): + """Codex's extras live under `extensions["com.openai"]`; identity (name, version, + author) is read off the shared root manifest fields checked elsewhere in this file.""" + manifest = _load("plugin.json") + codex_ext = manifest["extensions"]["com.openai"] + + for key in ("displayName", "shortDescription", "longDescription", "developerName", "category", "capabilities"): + assert codex_ext["interface"].get(key), f"extensions.com.openai.interface lacks {key}" + assert codex_ext.get("hooks") == "./hooks/hooks.json" + + def test_every_mcp_manifest_launches_the_pinned_pypi_release(): """`uvx colgrep-mcp==`: a bare executable in `command`, one requirement in `args`, nothing to expand.""" @@ -94,7 +110,6 @@ def test_every_mcp_manifest_launches_the_pinned_pypi_release(): def test_plugin_manifests_point_at_their_own_mcp_file(): assert _load(".claude-plugin/plugin.json")["mcpServers"] == "./.claude-plugin/mcp.json" - assert _load(".codex-plugin/plugin.json")["mcpServers"] == "./.codex-plugin/mcp.json" assert not (REPO_ROOT / ".mcp.json").exists(), "a root .mcp.json is read as project-scope config; see stack-traps" @@ -115,13 +130,15 @@ def test_placeholders_only_in_claude_code_env(): so only its manifest may carry one, and only in `env`.""" claude_env = _server(".claude-plugin/mcp.json")["env"] assert claude_env == {"COLGREP_MCP_ROOT": "${CLAUDE_PROJECT_DIR}"} - for relpath in (".codex-plugin/mcp.json", "mcp.json"): - assert "$" not in json.dumps(_server(relpath).get("env", {})), relpath + # Codex reads this same root mcp.json (no manifest of its own), so it is + # covered by the same placeholder-free assertion as Agent Plugins 1.0. + assert "$" not in json.dumps(_server("mcp.json").get("env", {})) def test_names_aligned(): claude_name = _load(".claude-plugin/plugin.json")["name"] agent_name = _load("plugin.json")["name"] - codex_name = _load(".codex-plugin/plugin.json")["name"] + # Codex parses the same root manifest as agent-plugins (no manifest of its own), + # so its name is agent_name; nothing further to compare here. - assert claude_name == agent_name == codex_name + assert claude_name == agent_name diff --git a/server/tests/test_version.py b/server/tests/test_version.py index 73a0756..cc42811 100644 --- a/server/tests/test_version.py +++ b/server/tests/test_version.py @@ -2,7 +2,8 @@ `colgrep_mcp.__version__` is derived from installed package metadata (see `colgrep_mcp/__init__.py`); the plugin manifests and the `uvx -colgrep-mcp==` pin in the three MCP manifests are rewritten by +colgrep-mcp==` pin in the two MCP manifests (`.claude-plugin/mcp.json` +and the shared root `mcp.json`, which Codex also reads) are rewritten by `cz bump` through `version_files` (pypi_publication R01 §C6). Nothing else enforces that they stay aligned except this test and `tests/test_manifests.py`. @@ -31,18 +32,23 @@ def test_version_matches_pyproject(): def test_manifests_match_pyproject(): + """`.codex-plugin/plugin.json` no longer exists: Codex parses the shared root + `plugin.json` natively (extras under `extensions["com.openai"]`), so checking + `plugin.json` here also covers Codex's version.""" version = _pyproject_version() - for relpath in ("plugin.json", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json"): + for relpath in ("plugin.json", ".claude-plugin/plugin.json"): manifest = json.loads((REPO_ROOT / relpath).read_text()) assert manifest["version"] == version, f"{relpath} version mismatch" def test_mcp_manifests_pin_the_pyproject_version(): - """The `colgrep-mcp==` regex in `version_files` must have rewritten every pin; a lag means a hand edit.""" + """The `colgrep-mcp==` regex in `version_files` must have rewritten every pin; a lag + means a hand edit. Codex reads the same root `mcp.json` as Agent Plugins 1.0 (no + manifest of its own), so it is covered by that one entry.""" version = _pyproject_version() - for relpath in (".claude-plugin/mcp.json", ".codex-plugin/mcp.json", "mcp.json"): + for relpath in (".claude-plugin/mcp.json", "mcp.json"): server = json.loads((REPO_ROOT / relpath).read_text())["mcpServers"]["colgrep"] assert server["args"] == [f"colgrep-mcp=={version}"], f"{relpath} pin mismatch"