diff --git a/.archcore/plugin/bump-plugin-version.cpat.md b/.archcore/plugin/bump-plugin-version.cpat.md index 7a7b147..35e0e2e 100644 --- a/.archcore/plugin/bump-plugin-version.cpat.md +++ b/.archcore/plugin/bump-plugin-version.cpat.md @@ -8,21 +8,22 @@ tags: ## Pattern -The plugin version is declared in **three per-host manifests** and nowhere else. On every release, the same version string is bumped in all three, kept **byte-identical**, then the merge commit is tagged `vX.Y.Z`. +The plugin version is declared in **four per-host manifests** and nowhere else. On every release, the same version string is bumped in all four, kept **byte-identical**, then the merge commit is tagged `vX.Y.Z`. -The three files (plugin root is `plugins/archcore/`): +The four files (plugin root is `plugins/archcore/`): - `plugins/archcore/.claude-plugin/plugin.json` - `plugins/archcore/.cursor-plugin/plugin.json` - `plugins/archcore/.codex-plugin/plugin.json` +- `plugins/archcore/.plugin/plugin.json` -Set the same new `version` in all three. Do not touch anything else — the marketplace catalogs, MCP configs, and hook JSON carry no plugin version. +Set the same new `version` in all four. Do not touch anything else — the marketplace catalogs, MCP configs, and hook JSON carry no plugin version. -**Version source of truth is the latest git tag, not the manifest.** Compute the next version relative to the most recent tag (`git describe --tags --abbrev=0`, strip the leading `v`), apply the requested step (default `patch`, else `minor` / `major` / an explicit `X.Y.Z`), and write the result. Deriving from the tag rather than the manifest reconciles drift when a tag was cut without a manifest bump. +**Version source of truth is the latest git tag, not the manifest.** Compute the next version relative to the highest semantic version tag (`git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1`, strip the leading `v`), apply the requested step (default `patch`, else `minor` / `major` / an explicit `X.Y.Z`), and write the result. Deriving from the tag rather than the manifest reconciles drift when a tag was cut without a manifest bump. ## Before -`plugins/archcore/.claude-plugin/plugin.json` (and the `.cursor-plugin` / `.codex-plugin` siblings): +`plugins/archcore/.claude-plugin/plugin.json` (and the `.cursor-plugin` / `.codex-plugin` / `.plugin` siblings): ```json { @@ -35,7 +36,7 @@ Set the same new `version` in all three. Do not touch anything else — the mark ## After -All three manifests, identically (latest tag `v0.4.18` → patch step → `0.4.19`): +All four manifests, identically (latest tag `v0.4.18` → patch step → `0.4.19`): ```json { @@ -46,11 +47,11 @@ All three manifests, identically (latest tag `v0.4.18` → patch step → `0.4.1 } ``` -Only the `version` line changes. `name` and `description` MUST remain byte-identical across all three (cross-host parity), so leave them untouched. +Only the `version` line changes. `name` and `description` MUST remain byte-identical across all four (cross-host parity), so leave them untouched. ## Scope -Exactly the three `plugin.json` manifests under `plugins/archcore/{.claude-plugin,.cursor-plugin,.codex-plugin}/`. Nothing else in the repo declares the plugin version: +Exactly the four `plugin.json` manifests under `plugins/archcore/{.claude-plugin,.cursor-plugin,.codex-plugin,.plugin}/`. Nothing else in the repo declares the plugin version: - Marketplace catalogs (`.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/marketplace.json`) carry no `version` field — do not add one. - `hooks/cursor.hooks.json` has `"version": 1` — that is the **hook-schema** version, not the plugin version. Do not touch. @@ -61,21 +62,21 @@ The bump edits files only. Merging, tagging (`git tag vX.Y.Z && git push origin ## Rationale -- **Single source, three copies.** Each host loads its own manifest; there is no shared version file. The version is duplicated by necessity, so the bump must fan out to all three or a host ships stale. -- **Parity is test-enforced.** `test/structure/json-configs.bats` asserts the version matches across Claude Code and Cursor; `test/structure/codex-plugin.bats` asserts the Codex manifest metadata (incl. `.version`) matches Claude Code. Bumping one host and forgetting another turns CI red — this is the single most common release regression. +- **Single source, four copies.** Each host loads its own manifest; there is no shared version file. The version is duplicated by necessity, so the bump must fan out to all four or a host ships stale. +- **Parity is test-enforced.** `test/structure/json-configs.bats` asserts the version matches across Claude Code and Cursor; `test/structure/codex-plugin.bats` asserts the Codex manifest metadata (incl. `.version`) matches Claude Code; `test/structure/copilot-plugin.bats` does the same for Copilot. Bumping one host and forgetting another turns CI red — this is the single most common release regression. - **Tag-relative stepping avoids drift decisions.** The git tag drives the release workflow (`.github/workflows/release.yml` triggers on `v*`). Computing the next version from the latest tag keeps the manifest and the tag lineage aligned, even if a previous tag was cut on a docs commit without a manifest bump. -- **Mechanical and low-risk, but easy to do incompletely.** The change is three one-line edits — cheap to script, but the "identical across three files" invariant is exactly what a human eye skips. A dedicated flow (the `/bump-plugin-version` skill) removes that failure mode. +- **Mechanical and low-risk, but easy to do incompletely.** The change is four one-line edits — cheap to script, but the "identical across four files" invariant is exactly what a human eye skips. A dedicated flow (the `/bump-plugin-version` skill) removes that failure mode. ## Enforcement going forward -- Any version bump MUST update all three manifests to the same value in one change. +- Any version bump MUST update all four manifests to the same value in one change. - Never bump the version directly on `main` — `main` is synthesized from a tagged `dev` commit (`docs/release.md`). -- The `/bump-plugin-version` local skill is the canonical way to perform this pattern: it reads this cpat, derives the next version from the latest tag, and edits the three manifests. Prefer it over hand-editing. -- After bumping, verify parity — `verify-plugin-integrity` Section 4 (cross-host consistency) checks `name`/`description`/`version` across the three manifests. +- The `/bump-plugin-version` local skill is the canonical way to perform this pattern: it reads this cpat, derives the next version from the latest tag, and edits the four manifests. Prefer it over hand-editing. +- After bumping, verify parity — `verify-plugin-integrity` Section 4 (cross-host consistency) checks `name`/`description`/`version` across the four manifests. ## Edge cases - **Tag ahead of manifest.** A tag can exist that is newer than the manifest `version` (a tag cut without a bump). Because the next version is derived from the tag, not the manifest, the bump naturally moves forward from the true release marker rather than repeating a stale manifest value. -- **No tags yet.** If `git describe --tags` finds nothing, fall back to the current manifest `version` (or seed `0.0.0`) and step from there; surface the fallback to the user. +- **No tags yet.** If the semantic-version tag query finds nothing, fall back to the current manifest `version` (or seed `0.0.0`) and step from there; surface the fallback to the user. - **First-digit/pre-1.0 semantics.** These are ordinary semver bumps — `minor` resets patch to 0, `major` resets minor and patch to 0. Pre-1.0 (`0.y.z`) is treated as normal semver here; a `major` step goes `0.y.z → 1.0.0` only when explicitly requested. -- **Explicit version.** When an exact `X.Y.Z` is provided, write it verbatim (no tag arithmetic), but still enforce it across all three files and that it is greater than the latest tag. +- **Explicit version.** When an exact `X.Y.Z` is provided, write it verbatim (no tag arithmetic), but still enforce it across all four files and that it is greater than the latest tag. diff --git a/.archcore/plugin/component-registry.doc.md b/.archcore/plugin/component-registry.doc.md index 0eb0d3c..f750fce 100644 --- a/.archcore/plugin/component-registry.doc.md +++ b/.archcore/plugin/component-registry.doc.md @@ -10,7 +10,7 @@ tags: Reference document listing all components of the Archcore Plugin (multi-host: Claude Code, Cursor, Codex CLI). -Note: Claude Code and Cursor surface user-invoked workflows directly from skills (`skills//SKILL.md`). Codex CLI discovers slash commands from root-level `commands/*.md` wrappers — thin host-adapter shims that delegate to the matching skill. The skill remains the single behavioral source of truth across all three hosts; no workflow logic lives in `commands/`. +Note: Claude Code and Cursor surface user-invoked workflows directly from skills (`skills//SKILL.md`). Codex CLI discovers slash commands from root-level `commands/*.md` wrappers — thin host-adapter shims that delegate to the matching skill. The skill remains the single behavioral source of truth across all four hosts; no workflow logic lives in `commands/`. Per `skill-surface-collapse.adr.md`, the visible `/` palette is exactly **7 auto-invocable intent skills**. There are no per-document-type skills, no track skills, and no utility skills. Track flows live as references under `skills/plan/references/`; drift detection lives at `skills/audit/lib/drift-detection.md`. @@ -100,7 +100,7 @@ For Codex CLI, both subagents also ship as TOML variants (`agents/archcore-assis | 5 | PostToolUse | `mcp__archcore__update_document` | `bin/check-cascade` | 3s | | 6 | PostToolUse | `mcp__archcore__create_document\|mcp__archcore__update_document` | `bin/check-precision` | 3s | -Hook configs: `hooks/hooks.json` (Claude Code, PascalCase events), `hooks/cursor.hooks.json` (Cursor, camelCase events + `afterMCPExecution`; its `preToolUse` matcher is `Write` only — Cursor exposes no Edit tool), `hooks/codex.hooks.json` (Codex CLI, PascalCase events with `apply_patch` matcher addition for Codex's native edit primitive; commands use Codex's canonical `${PLUGIN_ROOT}` substitution). +Hook configs: `hooks/hooks.json` (Claude Code, PascalCase events), `hooks/cursor.hooks.json` (Cursor, camelCase events + `afterMCPExecution`; its `preToolUse` matcher is `Write` only — Cursor exposes no Edit tool), `hooks/codex.hooks.json` (Codex CLI, PascalCase events with `apply_patch` matcher addition for Codex's native edit primitive; commands use Codex's canonical `${PLUGIN_ROOT}` substitution), `hooks/copilot.hooks.json` (Copilot CLI, native camelCase events, `${COPILOT_PLUGIN_ROOT}` commands, deterministic `ARCHCORE_HOST=copilot`, and `cwd: "."` so hooks run from the user project). Hook 2 and Hook 3 share the `Write|Edit` matcher. Hook 2 (`check-archcore-write`) blocks direct writes to `.archcore/*.md`. Hook 3 (`check-code-alignment`) injects relevant `.archcore/` context for source-file edits via `hookSpecificOutput.additionalContext`. They act on disjoint path sets by construction — no conflict. @@ -108,7 +108,7 @@ Hook 6 (`check-precision`) is the Phase 1 implementation of the Precision Initia Historical note: a prior revision had a `PostToolUse` entry with matcher `Write|Edit` invoking `validate-archcore`. It was removed because PreToolUse already blocks all Write/Edit to `.archcore/*.md` (PostToolUse fires only on success), so the matcher was dead weight forking a shell on every Write/Edit anywhere in the repo. See `hooks-validation-system.spec.md` for the rationale. Structure tests guard against its re-introduction. -`bin/session-start` carries a plugin-install-dir guard (see `cursor-mcp-architecture.adr.md`): when cwd contains a sibling `.cursor-plugin/`, `.claude-plugin/`, or `.codex-plugin/` manifest, the hook exits silently. This prevents the hook from emitting the plugin's bundled `.archcore/` (if any future regression reintroduces it) as the user's knowledge base. +`bin/session-start` carries a plugin-install-dir guard (see `cursor-mcp-architecture.adr.md`): when cwd contains a sibling `.cursor-plugin/`, `.claude-plugin/`, `.codex-plugin/`, or `.plugin/` manifest, the hook exits silently. This prevents the hook from emitting the plugin's bundled `.archcore/` (if any future regression reintroduces it) as the user's knowledge base. ### Bin Scripts @@ -132,7 +132,7 @@ The `bin/` tree contains hook scripts, shared shell libraries under `bin/lib/`, | Component | Location | Tests | Description | | --- | --- | --- | --- | | Unit tests | `test/unit/` | — | Test each bin script: stdin parsing, host detection, exit codes, output format, edge cases. | -| Structure tests | `test/structure/` | — | Validate JSON configs, skill frontmatter, agent frontmatter, hook references, script permissions, rules. `hooks.bats` includes anti-regression invariants. `cursor-plugin.bats` locks `docs/cursor.mcp.example.json` shape. `codex-plugin.bats` enforces Codex manifest, marketplace schema, hooks shape, MCP wiring, TOML agents, and parity between `commands/*.md` wrappers (7) and `skills//SKILL.md`. `marketplace-discovery.bats` pins all three catalogs' `source`/`path` to the `plugins/archcore` subdirectory (issue #2 regression). | +| Structure tests | `test/structure/` | — | Validate JSON configs, skill frontmatter, agent frontmatter, hook references, script permissions, rules. `hooks.bats` includes anti-regression invariants. `cursor-plugin.bats` locks `docs/cursor.mcp.example.json` shape. `codex-plugin.bats` enforces Codex manifest, marketplace schema, hooks shape, MCP wiring, TOML agents, and parity between `commands/*.md` wrappers (7) and `skills//SKILL.md`. `copilot-plugin.bats` enforces Copilot manifest pointers, hooks shape, project-root execution, and version parity. `marketplace-discovery.bats` pins all three catalogs' `source`/`path` to the `plugins/archcore` subdirectory (issue #2 regression). | | Fixtures | `test/fixtures/stdin/` | — | Mock stdin JSON for Claude Code, Cursor, Copilot, Codex CLI, and malformed inputs | | Helpers | `test/helpers/` | — | common.bash (setup, mocks, timeout shim, exports `REPO_ROOT` + `PLUGIN_ROOT`), bats-support, bats-assert (git submodules) | | Makefile | `Makefile` | — | Targets: `test`, `test-unit`, `test-structure`, `lint`, `check-json`, `check-perms`, `verify`. Dev-only — stripped from `main` distribution. | @@ -143,7 +143,7 @@ Run `make verify` for full check. Run `make test` for tests only. See `plugin-te ### MCP Server -The plugin **ships MCP registration** for Claude Code via `.mcp.json` at the plugin root (`plugins/archcore/.mcp.json`): +The plugin **ships shared MCP registration** for Claude Code and GitHub Copilot CLI via `.mcp.json` at the plugin root (`plugins/archcore/.mcp.json`): ```json { @@ -156,7 +156,7 @@ The plugin **ships MCP registration** for Claude Code via `.mcp.json` at the plu } ``` -The `command` resolves through PATH — users must have the Archcore CLI installed globally (see https://docs.archcore.ai/cli/install/). If the CLI is missing at session start, the MCP server fails to register and `bin/session-start` prints the install instructions. +The `command` resolves through PATH — users must have the Archcore CLI installed globally (see https://docs.archcore.ai/cli/install/). If the CLI is missing at session start, the MCP server fails to register and `bin/session-start` prints the install instructions. Copilot CLI currently launches plugin MCP children from the plugin directory without exposing its project path; correct project-root MCP operation depends on github/copilot-cli#4234. Codex CLI uses `.codex-plugin/plugin.json` to point at plugin-root `.codex.mcp.json`, which uses Codex's direct server map shape: `archcore.command: "archcore"`, `archcore.args: ["mcp"]`. @@ -173,15 +173,17 @@ Component manifests, hooks, and MCP configs are plugin-root-relative (under `plu | `.claude-plugin/plugin.json` | Claude Code | Plugin manifest (plugin root) | | `.cursor-plugin/plugin.json` | Cursor | Plugin manifest (plugin root; with explicit component paths; **no `mcpServers` field** — deliberately disabled, see `cursor-mcp-architecture.adr.md`) | | `.codex-plugin/plugin.json` | Codex CLI | Plugin manifest (plugin root) with `skills`, `hooks`, and `mcpServers` pointers | +| `.plugin/plugin.json` | GitHub Copilot CLI | Plugin manifest (plugin root) with explicit `skills`, `agents`, `hooks`, and `mcpServers` pointers | | `.claude-plugin/marketplace.json` | Claude Code | Marketplace catalog — **repo root**, `source: ./plugins/archcore` | | `.cursor-plugin/marketplace.json` | Cursor | Marketplace catalog — **repo root**, `source: ./plugins/archcore` | | `.agents/plugins/marketplace.json` | Codex CLI | Marketplace catalog + default-install policy — **repo root**, `source.path: ./plugins/archcore` | -| `.mcp.json` | Claude Code | Plugin-provided MCP registration (plugin root; `command: "archcore"` on PATH) | +| `.mcp.json` | Claude Code / GitHub Copilot CLI | Shared plugin-provided MCP registration (plugin root; `command: "archcore"` on PATH) | | `.codex.mcp.json` | Codex CLI | Plugin-provided MCP registration (plugin root; `command: "archcore"` on PATH) | | `docs/cursor.mcp.example.json` | Cursor | Reference MCP config for users to copy into `~/.cursor/mcp.json` or `.cursor/mcp.json` (**repo root**). | | `hooks/hooks.json` | Claude Code | Hook event config (PascalCase) | | `hooks/cursor.hooks.json` | Cursor | Hook event config (camelCase + afterMCPExecution) | | `hooks/codex.hooks.json` | Codex CLI | Hook event config (PascalCase + apply_patch matcher) | +| `hooks/copilot.hooks.json` | GitHub Copilot CLI | Hook event config (camelCase + native mutation matchers + project-root cwd) | | `commands/*.md` | Codex CLI | Slash command wrappers (7) — thin shims delegating to `skills//SKILL.md` | | `agents/archcore-assistant.toml` | Codex CLI | Codex TOML subagent (`sandbox_mode = "workspace-write"`); MD original used by Claude Code/Cursor | | `agents/archcore-auditor.toml` | Codex CLI | Codex TOML subagent (`sandbox_mode = "read-only"` + `disabled_tools[]`); MD original used by Claude Code/Cursor | diff --git a/.claude/skills/bump-plugin-version/SKILL.md b/.claude/skills/bump-plugin-version/SKILL.md index 62ca6af..1d799e7 100644 --- a/.claude/skills/bump-plugin-version/SKILL.md +++ b/.claude/skills/bump-plugin-version/SKILL.md @@ -1,20 +1,20 @@ --- name: bump-plugin-version argument-hint: "[patch | minor | major | X.Y.Z] (default: patch)" -description: Bump the archcore plugin version across all three host manifests (.claude-plugin, .cursor-plugin, .codex-plugin) in one step, keeping them byte-identical. Derives the next version from the latest git tag. Triggers on /bump-plugin-version. Edits files only — merging, tagging, and publishing stay manual per docs/release.md. +description: Bump the archcore plugin version across all four host manifests (.claude-plugin, .cursor-plugin, .codex-plugin, .plugin) in one step, keeping their versions byte-identical. Derives the next version from the latest git tag. Triggers on /bump-plugin-version. Edits files only — merging, tagging, and publishing stay manual per docs/release.md. disable-model-invocation: true model: haiku --- # /bump-plugin-version -Bump the plugin `version` in all three per-host manifests at once, following the +Bump the plugin `version` in all four per-host manifests at once, following the canonical pattern in the `bump-plugin-version` cpat. The next version is derived from the **latest git tag** (the release source of truth), not the manifest. ## Hard constraints (non-negotiable) -- Edit ONLY the three `plugin.json` files listed in Step 1. Nothing else in the +- Edit ONLY the four `plugin.json` files listed in Step 1. Nothing else in the repo carries the plugin version — not the marketplace catalogs, MCP configs, or hook JSON (`cursor.hooks.json`'s `"version": 1` is a schema version, not this). - Use the `Edit` tool for a targeted `"version": ""` → `"version": ""` @@ -48,11 +48,12 @@ Read the canonical pattern so this run stays aligned with it: `mcp__archcore__get_document(path=".archcore/plugin/bump-plugin-version.cpat.md")` The cpat is authoritative for **which files change** and **what must not**. The -three manifests (plugin root is `plugins/archcore/`): +four manifests (plugin root is `plugins/archcore/`): - `plugins/archcore/.claude-plugin/plugin.json` - `plugins/archcore/.cursor-plugin/plugin.json` - `plugins/archcore/.codex-plugin/plugin.json` +- `plugins/archcore/.plugin/plugin.json` ### Step 2 — Resolve the version and compute the next one @@ -65,7 +66,7 @@ ARG="patch" # ← replace with the text after /bump-plugin-version: patch | mi extract_ver() { grep -oE '"version" *: *"[0-9]+\.[0-9]+\.[0-9]+"' "$1" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+'; } -LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null) +LATEST_TAG=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1) if [ -n "$LATEST_TAG" ]; then BASE="${LATEST_TAG#v}" else @@ -102,15 +103,16 @@ esac echo "OLD versions:"; extract_ver plugins/archcore/.claude-plugin/plugin.json extract_ver plugins/archcore/.cursor-plugin/plugin.json extract_ver plugins/archcore/.codex-plugin/plugin.json +extract_ver plugins/archcore/.plugin/plugin.json echo "$LATEST_TAG (base $BASE) -> NEXT $NEXT" ``` If the manifests already disagree (drift), note it — the bump reconciles them all to `NEXT`. State `NEXT` explicitly before editing anything. -### Step 3 — Write NEXT to all three manifests +### Step 3 — Write NEXT to all four manifests -For **each** of the three files, use `Edit` to replace that file's current version +For **each** of the four files, use `Edit` to replace that file's current version line — `"version": ""` → `"version": ""`. Only the version substring changes; formatting, key order, `name`, and `description` stay untouched. If a file's `` differs from the others (drift), still set it to `NEXT` — that is the @@ -118,22 +120,23 @@ reconciliation. ### Step 4 — Verify parity -All three MUST now read the same `NEXT` — check by equality, not by eye: +All four MUST now read the same `NEXT` — check by equality, not by eye: ```bash extract_ver() { grep -oE '"version" *: *"[0-9]+\.[0-9]+\.[0-9]+"' "$1" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+'; } V1=$(extract_ver plugins/archcore/.claude-plugin/plugin.json) V2=$(extract_ver plugins/archcore/.cursor-plugin/plugin.json) V3=$(extract_ver plugins/archcore/.codex-plugin/plugin.json) -if [ "$V1" = "$V2" ] && [ "$V2" = "$V3" ]; then echo "PARITY OK: $V1" -else echo "DRIFT: claude=$V1 cursor=$V2 codex=$V3" >&2; exit 1; fi +V4=$(extract_ver plugins/archcore/.plugin/plugin.json) +if [ "$V1" = "$V2" ] && [ "$V2" = "$V3" ] && [ "$V3" = "$V4" ]; then echo "PARITY OK: $V1" +else echo "DRIFT: claude=$V1 cursor=$V2 codex=$V3 copilot=$V4" >&2; exit 1; fi ``` Then run the test-enforced invariant (this is the CI check the skill exists to keep green; run it, don't skip it — only note "bats unavailable" if the binary is missing): ```bash -bats test/structure/json-configs.bats test/structure/codex-plugin.bats +bats test/structure/json-configs.bats test/structure/codex-plugin.bats test/structure/copilot-plugin.bats ``` ## Result @@ -146,8 +149,8 @@ Report in this shape: - Latest tag: - Step: - Version: -> -- Drift reconciled: cursor= codex= → unified to > -- Parity: all three manifests now read "" (bats: ) +- Drift reconciled: cursor= codex= copilot= → unified to > +- Parity: all four manifests now read "" (bats: ) Manual next steps (not run): 1. Review & commit the bump on dev. diff --git a/.claude/skills/verify-plugin-integrity/SKILL.md b/.claude/skills/verify-plugin-integrity/SKILL.md index a962867..a105052 100644 --- a/.claude/skills/verify-plugin-integrity/SKILL.md +++ b/.claude/skills/verify-plugin-integrity/SKILL.md @@ -1,12 +1,12 @@ --- name: verify-plugin-integrity -description: Validate plugin format conformance for Claude Code, Cursor, and Codex CLI — statically audits the plugin manifests under plugins/archcore/ (.claude-plugin/, .cursor-plugin/, .codex-plugin/), the repo-root marketplace catalogs (incl. .agents/plugins/marketplace.json), SKILL.md frontmatter, MD + TOML agent files, Codex commands/*.md wrappers, hooks JSON for all three hosts, plugin-shipped MCP wiring (.mcp.json + .codex.mcp.json), Cursor rules, and bin/ scripts against the official Claude Code plugin spec, Cursor plugin spec, OpenAI Codex CLI plugin docs, and Agent Skills specification. No test execution. Use after structural changes to manifests, skills, agents, hooks, or rules; before opening a PR touching plugin structure; or when multi-host (Claude + Cursor + Codex) consistency is in doubt. For the bats test suite, use /archcore:verify instead. +description: Validate plugin format conformance for Claude Code, Cursor, Codex CLI, and GitHub Copilot CLI — statically audits the plugin manifests under plugins/archcore/ (.claude-plugin/, .cursor-plugin/, .codex-plugin/, .plugin/), the repo-root marketplace catalogs (incl. .agents/plugins/marketplace.json), SKILL.md frontmatter, MD + TOML agent files, Codex commands/*.md wrappers, hooks JSON for all four hosts, plugin-shipped MCP wiring (.mcp.json + .codex.mcp.json), Cursor rules, and bin/ scripts against the official host plugin specifications and Agent Skills specification. No test execution. Use after structural changes to manifests, skills, agents, hooks, or rules; before opening a PR touching plugin structure; or when multi-host consistency is in doubt. For the bats test suite, use /archcore:verify instead. disable-model-invocation: true --- # /verify-plugin-integrity -Static format-conformance audit for the archcore multi-host plugin. Validates that every plugin artifact matches the official Claude Code, Cursor, OpenAI Codex CLI, and Agent Skills specifications, plus the normative Archcore specs in this repo. +Static format-conformance audit for the archcore multi-host plugin. Validates that every plugin artifact matches the official Claude Code, Cursor, OpenAI Codex CLI, GitHub Copilot CLI, and Agent Skills specifications, plus the normative Archcore specs in this repo. **Does not** execute tests, lint, scripts, or hooks — for that, use `/archcore:verify` (which runs the bats test suite). @@ -14,7 +14,7 @@ Static format-conformance audit for the archcore multi-host plugin. Validates th ## Layout (post-relocation — read this first) -The **plugin root is the `plugins/archcore/` subdirectory**, not the repo root. All host-runtime-loaded content lives there: `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`, `skills/`, `agents/`, `commands/`, `hooks/`, `bin/`, `rules/`, `assets/`, `.mcp.json`, `.codex.mcp.json`. +The **plugin root is the `plugins/archcore/` subdirectory**, not the repo root. All host-runtime-loaded content lives there: `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.plugin/plugin.json`, `skills/`, `agents/`, `commands/`, `hooks/`, `bin/`, `rules/`, `assets/`, `.mcp.json`, `.codex.mcp.json`. The three marketplace catalogs stay at the **repo root** (`.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/marketplace.json`), each pointing `source`/`path` at `./plugins/archcore`. Rationale: Codex never scans a marketplace root for a plugin — a catalog `source.path` of `./` is silently undiscoverable (issue #2). See `.archcore/plugin/subdirectory-plugin-layout.adr.md`. @@ -47,6 +47,11 @@ The three marketplace catalogs stay at the **repo root** (`.claude-plugin/market - Codex CLI repo — https://github.com/openai/codex - When upstream docs are unreachable or in flux, the internal Codex ground truth is `.archcore/plugin/codex-local-plugin-testing.guide.md` (Step 3 invariants), enforced by `test/structure/codex-plugin.bats`. +### GitHub Copilot CLI +- Plugin structure — https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-plugins +- Hooks reference — https://docs.github.com/en/copilot/reference/hooks-reference +- Plugin manifest reference — https://docs.github.com/en/copilot/reference/copilot-cli-reference/plugin-manifest + ### Archcore internal conformance (normative for this repo) - `.archcore/plugin/plugin-architecture.spec.md` — seven-skill intent surface, invocation flags - `.archcore/plugin/subdirectory-plugin-layout.adr.md` — `plugins/archcore/` layout, catalog `source` rules (issue #2) @@ -66,11 +71,11 @@ If a local spec conflicts with an external official doc, the external doc is gro ## When to use -- After editing any file under `plugins/archcore/` (`.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `skills/`, `agents/`, `commands/`, `hooks/`, `rules/`, `bin/`) +- After editing any file under `plugins/archcore/` (`.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`, `.plugin/`, `skills/`, `agents/`, `commands/`, `hooks/`, `rules/`, `bin/`) - After changing the repo-root marketplace catalogs (`.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/marketplace.json`) - After changing `.mcp.json` / `.codex.mcp.json` at the plugin root, or `docs/cursor.mcp.example.json` at the repo root - Before opening a PR that touches plugin structure -- When cross-host (Claude + Cursor + Codex) manifest consistency is in doubt +- When cross-host manifest consistency is in doubt - **Not** for test runs — use `/archcore:verify` - **Not** for Archcore document freshness vs code — use `/archcore:audit --drift` @@ -127,15 +132,27 @@ Per `.archcore/plugin/codex-host-support.prd.md` F1 and `.archcore/plugin/codex- - **Forbidden**: an inline `mcpServers` object inside the manifest. Codex requires the pointer-to-file shape; an inline object is non-conformant - Sibling files: `.codex-plugin/` MUST contain ONLY `plugin.json`. Any additional file (notably a stray `marketplace.json`) is a hard FAIL — Codex marketplace metadata lives at the repo root (see Section 5) +### Section 3a — GitHub Copilot CLI plugin manifest + +File: `.plugin/plugin.json` (plugin root) + +Per the GitHub Copilot CLI plugin manifest reference, enforced by `test/structure/copilot-plugin.bats`: + +- JSON parses and `name`, `description`, and semver `version` are present +- `hooks` equals `./hooks/copilot.hooks.json` +- `mcpServers` equals `./.mcp.json` +- `skills` and `agents` equal `./skills/` and `./agents/` +- Every explicit relative pointer starts with `./` and resolves inside the plugin root + ### Section 4 — Cross-host consistency -Compare `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, and `.codex-plugin/plugin.json` (all at the plugin root). The following fields MUST be byte-identical across all three: +Compare `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`, and `.plugin/plugin.json` (all at the plugin root). The following fields MUST be byte-identical across all four: - `name` - `description` - `version` -Flag any drift — this is the most common regression when bumping versions on one host but forgetting another. Enforced by the cross-host parity assertions in `test/structure/codex-plugin.bats`. +Flag any drift — this is the most common regression when bumping versions on one host but forgetting another. Enforced by `test/structure/codex-plugin.bats` and `test/structure/copilot-plugin.bats`. ### Section 5 — Marketplace catalogs (repo root) @@ -284,6 +301,18 @@ Expected PostToolUse shape: **Runtime caveat** (informational, not a FAIL): live plugin-hook execution is gated behind a Codex feature flag (`codex features enable plugin_hooks`; `under development, false` by default in Codex 0.130.0-era builds) and user trust. Upstream hook docs are in flux — see the guide's "Hook guardrails do not fire" entry. This skill validates static packaging only; live execution is verified by the bats integration suite. +### Section 11a — GitHub Copilot CLI hooks + +File: `hooks/copilot.hooks.json` (plugin root) + +Per the GitHub Copilot hooks reference, enforced by `test/structure/copilot-plugin.bats`: + +- JSON parses, `version: 1` is present, and event keys use native camelCase: `sessionStart`, `preToolUse`, `postToolUse` +- Every hook sets `cwd: "."`, `ARCHCORE_HOST: "copilot"`, and an appropriate timeout +- Commands resolve from `${COPILOT_PLUGIN_ROOT}/bin/*` +- `preToolUse` matchers cover `create`, `edit`, `str_replace_editor`, and `apply_patch` +- `postToolUse` entries omit matchers so all tool calls reach the shared scripts, which self-filter after normalizing Copilot tool names + ### Section 12 — MCP wiring Two plugin-shipped MCP configs at the plugin root (`plugins/archcore/`). Both must point at `archcore` resolved via PATH — the plugin does not bundle a launcher (see `remove-bundled-launcher-global-cli.idea.md`). Users install the CLI globally per https://docs.archcore.ai/cli/install/. @@ -293,6 +322,7 @@ Two plugin-shipped MCP configs at the plugin root (`plugins/archcore/`). Both mu - `mcpServers.archcore.command` equals `archcore` - `mcpServers.archcore.args` equals `["mcp"]` - File MUST NOT contain `${CLAUDE_PLUGIN_ROOT}` or any `bin/archcore` reference (grep — either is a hard FAIL: the launcher was removed) +- GitHub Copilot CLI shares this config through `.plugin/plugin.json`; `mcpServers.archcore` must remain directly compatible with both hosts **Codex CLI — `.codex.mcp.json`** (plugin root): - File exists at the plugin root (NOT inside `.codex-plugin/`) @@ -328,7 +358,7 @@ Required `bin/` shape (plugin root): - `bin/lib/normalize-stdin.sh` exists (sourced by all hook scripts except `check-staleness`) - `bin/lib/empty-state.sh` exists (sourced by `session-start` for the empty-state nudge) -- All hook scripts referenced by any of the three hooks configs exist under `bin/` and are executable: `session-start`, `check-archcore-write`, `check-code-alignment`, `validate-archcore`, `check-cascade`, `check-precision`, `check-staleness` +- All hook scripts referenced by any of the four hooks configs exist under `bin/` and are executable: `session-start`, `check-archcore-write`, `check-code-alignment`, `validate-archcore`, `check-cascade`, `check-precision`, `check-staleness` - `bin/git-scope` exists and is executable — it is a **skill helper** for `/archcore:context --git-changes` (invoked by the context skill via Bash), NOT referenced by any hooks config; do not expect it in the hooks JSON - All scripts start with `#!/bin/sh` (POSIX) and pass `shellcheck -s sh -x` when available (`make lint` is the canonical invocation) - `bin/session-start` falls back to an install-instructions message pointing at https://docs.archcore.ai/cli/install/ when `archcore` is not on PATH @@ -343,7 +373,7 @@ Light staleness check against `.archcore/plugin/component-registry.doc.md`: - `ls plugins/archcore/agents/*.toml | wc -l` matches the TOML agent total (Codex parity — currently 2) - `ls plugins/archcore/commands/*.md | wc -l` equals 7 — one Codex slash-command wrapper per skill; each carries `description:` frontmatter and references the matching `skills//SKILL.md` (parity per the registry's Codex Slash Command Wrappers table, enforced by `codex-plugin.bats`) - Scripts in `bin/` match the registry's Bin Scripts table (hook scripts, `bin/lib/` libraries, and the `git-scope` skill helper) -- Per-host config table all exists: at the plugin root — `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.mcp.json`, `.codex.mcp.json`, `hooks/hooks.json`, `hooks/cursor.hooks.json`, `hooks/codex.hooks.json`; at the repo root — `.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/marketplace.json`, `docs/cursor.mcp.example.json` +- Per-host config table all exists: at the plugin root — `.claude-plugin/plugin.json`, `.cursor-plugin/plugin.json`, `.codex-plugin/plugin.json`, `.plugin/plugin.json`, `.mcp.json`, `.codex.mcp.json`, `hooks/hooks.json`, `hooks/cursor.hooks.json`, `hooks/codex.hooks.json`, `hooks/copilot.hooks.json`; at the repo root — `.claude-plugin/marketplace.json`, `.cursor-plugin/marketplace.json`, `.agents/plugins/marketplace.json`, `docs/cursor.mcp.example.json` This is a spot-check, not a full audit — for full staleness detection use `/archcore:audit --drift`. diff --git a/Makefile b/Makefile index ad272c7..9c9c01f 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,9 @@ ALL_SCRIPTS := $(BIN_SCRIPTS) $(LIB_SCRIPTS) # Marketplace catalogs stay at repo root; plugin manifests/hooks/mcp live under plugins/archcore/. JSON_FILES := .agents/plugins/marketplace.json .claude-plugin/marketplace.json .cursor-plugin/marketplace.json \ $(PLUGIN_REL)/.claude-plugin/plugin.json $(PLUGIN_REL)/.cursor-plugin/plugin.json \ - $(PLUGIN_REL)/.codex-plugin/plugin.json $(PLUGIN_REL)/.codex.mcp.json \ + $(PLUGIN_REL)/.codex-plugin/plugin.json $(PLUGIN_REL)/.plugin/plugin.json $(PLUGIN_REL)/.codex.mcp.json \ $(PLUGIN_REL)/hooks/hooks.json $(PLUGIN_REL)/hooks/cursor.hooks.json $(PLUGIN_REL)/hooks/codex.hooks.json \ + $(PLUGIN_REL)/hooks/copilot.hooks.json \ $(PLUGIN_REL)/.mcp.json docs/cursor.mcp.example.json .PHONY: test test-codex-smoke lint check-json check-perms verify all diff --git a/docs/release.md b/docs/release.md index cadcdef..742973c 100644 --- a/docs/release.md +++ b/docs/release.md @@ -28,7 +28,7 @@ This complements two other defenses: - **CLI guard** in `archcore mcp` — refuses to serve when the working directory looks like a plugin install dir (sibling `.cursor-plugin/`, - `.claude-plugin/`, or `.codex-plugin/` manifests). See `archcore-cli` + `.claude-plugin/`, `.codex-plugin/`, or `.plugin/` manifests). See `archcore-cli` repo. - **Hook guard** in `bin/session-start` — same detection, silent exit. @@ -61,7 +61,8 @@ multi-host layout ADR and issue #2). That directory carries `skills/`, for marketplace surfaces), the per-host manifests (`plugins/archcore/.claude-plugin/plugin.json`, `plugins/archcore/.cursor-plugin/plugin.json`, -`plugins/archcore/.codex-plugin/plugin.json`), and the MCP configs +`plugins/archcore/.codex-plugin/plugin.json`, +`plugins/archcore/.plugin/plugin.json`), and the MCP configs (`plugins/archcore/.mcp.json`, `plugins/archcore/.codex.mcp.json`). At the **repo root** the marketplace catalogs ship and point at the @@ -79,10 +80,11 @@ subdirectory: `.agents/plugins/marketplace.json` (Codex), ## Cutting a release -1. Bump `version` in all three manifests +1. Bump `version` in all four manifests (`plugins/archcore/.claude-plugin/plugin.json`, `plugins/archcore/.cursor-plugin/plugin.json`, - `plugins/archcore/.codex-plugin/plugin.json`). + `plugins/archcore/.codex-plugin/plugin.json`, + `plugins/archcore/.plugin/plugin.json`). 2. Merge the bump PR to `dev`. 3. Tag the merge commit: `git tag v0.4.1 && git push origin v0.4.1`. 4. The `release.yml` workflow runs: diff --git a/plugins/archcore/.claude-plugin/plugin.json b/plugins/archcore/.claude-plugin/plugin.json index fb12914..637e766 100644 --- a/plugins/archcore/.claude-plugin/plugin.json +++ b/plugins/archcore/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "archcore", "description": "Make your AI agent code with your project's architecture, rules, and decisions.", - "version": "0.4.20", + "version": "0.4.23", "author": { "name": "Archcore" }, diff --git a/plugins/archcore/.codex-plugin/plugin.json b/plugins/archcore/.codex-plugin/plugin.json index 04d419a..835eb9c 100644 --- a/plugins/archcore/.codex-plugin/plugin.json +++ b/plugins/archcore/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "archcore", "description": "Make your AI agent code with your project's architecture, rules, and decisions.", - "version": "0.4.20", + "version": "0.4.23", "author": { "name": "Archcore", "url": "https://archcore.ai" diff --git a/plugins/archcore/.cursor-plugin/plugin.json b/plugins/archcore/.cursor-plugin/plugin.json index 2b49b2a..c6da129 100644 --- a/plugins/archcore/.cursor-plugin/plugin.json +++ b/plugins/archcore/.cursor-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "archcore", "displayName": "Archcore", "description": "Make your AI agent code with your project's architecture, rules, and decisions.", - "version": "0.4.20", + "version": "0.4.23", "author": { "name": "Archcore" }, diff --git a/plugins/archcore/.plugin/plugin.json b/plugins/archcore/.plugin/plugin.json new file mode 100644 index 0000000..634c494 --- /dev/null +++ b/plugins/archcore/.plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "archcore", + "description": "Make your AI agent code with your project's architecture, rules, and decisions.", + "version": "0.4.23", + "author": { + "name": "Archcore" + }, + "license": "Apache-2.0", + "repository": "https://github.com/archcore-ai/plugin", + "hooks": "./hooks/copilot.hooks.json", + "mcpServers": "./.mcp.json", + "skills": "./skills/", + "agents": "./agents/" +} diff --git a/plugins/archcore/bin/lib/normalize-stdin.sh b/plugins/archcore/bin/lib/normalize-stdin.sh index c60f8a2..c1a3066 100755 --- a/plugins/archcore/bin/lib/normalize-stdin.sh +++ b/plugins/archcore/bin/lib/normalize-stdin.sh @@ -99,17 +99,30 @@ case "$ARCHCORE_HOST" in # Native camelCase payload: toolName + toolArgs (an ESCAPED JSON string, # so field extraction goes through the unescaped helper, which also # handles plain objects). Legacy Claude-compat payloads (hookEventName + - # snake_case tool_input) extract via the tool_name fallback. The toolArgs - # key candidates (file_path/filePath/path) are provisional until real - # payload captures land — see copilot-host-support.rnd Next Action. + # snake_case tool_input) extract via the tool_name fallback. Native file + # tools use an absolute path key (verified against Copilot CLI 1.0.73). ARCHCORE_TOOL_NAME=$(_archcore_json_val "toolName") if [ -z "$ARCHCORE_TOOL_NAME" ]; then ARCHCORE_TOOL_NAME=$(_archcore_json_val "tool_name") fi + case "$ARCHCORE_TOOL_NAME" in + archcore-*) + # Copilot prefixes MCP tools with "-"; normalize to the + # canonical name consumed by the shared post-tool scripts. + ARCHCORE_TOOL_NAME="mcp__archcore__${ARCHCORE_TOOL_NAME#archcore-}" + ;; + esac ARCHCORE_FILE_PATH=$(_archcore_json_val_unescaped "file_path") if [ -z "$ARCHCORE_FILE_PATH" ]; then ARCHCORE_FILE_PATH=$(_archcore_json_val_unescaped "filePath") fi + if [ -z "$ARCHCORE_FILE_PATH" ]; then + case "$ARCHCORE_TOOL_NAME" in + create|edit|str_replace_editor|apply_patch) + ARCHCORE_FILE_PATH=$(_archcore_json_val_unescaped "path") + ;; + esac + fi ARCHCORE_DOC_PATH=$(_archcore_json_val "path") if [ -z "$ARCHCORE_DOC_PATH" ]; then ARCHCORE_DOC_PATH=$(_archcore_json_val_unescaped "path") diff --git a/plugins/archcore/hooks/copilot.hooks.json b/plugins/archcore/hooks/copilot.hooks.json new file mode 100644 index 0000000..ee35e70 --- /dev/null +++ b/plugins/archcore/hooks/copilot.hooks.json @@ -0,0 +1,66 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/session-start", + "env": { + "ARCHCORE_HOST": "copilot" + } + } + ], + "preToolUse": [ + { + "type": "command", + "matcher": "create|edit|str_replace_editor|apply_patch", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/check-archcore-write", + "env": { + "ARCHCORE_HOST": "copilot" + }, + "timeoutSec": 1 + }, + { + "type": "command", + "matcher": "create|edit|str_replace_editor|apply_patch", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/check-code-alignment", + "env": { + "ARCHCORE_HOST": "copilot" + }, + "timeoutSec": 1 + } + ], + "postToolUse": [ + { + "type": "command", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/validate-archcore", + "env": { + "ARCHCORE_HOST": "copilot" + }, + "timeoutSec": 3 + }, + { + "type": "command", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/check-cascade", + "env": { + "ARCHCORE_HOST": "copilot" + }, + "timeoutSec": 3 + }, + { + "type": "command", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/check-precision", + "env": { + "ARCHCORE_HOST": "copilot" + }, + "timeoutSec": 3 + } + ] + } +} diff --git a/test/fixtures/stdin/copilot/README.md b/test/fixtures/stdin/copilot/README.md index e372101..f6a4e22 100644 --- a/test/fixtures/stdin/copilot/README.md +++ b/test/fixtures/stdin/copilot/README.md @@ -5,12 +5,13 @@ Two payload generations: - `legacy-hybrid-write.json` — the Claude-compat shape (`hookEventName` + snake_case `tool_input`). Kept as the fallback-heuristic fixture: detection must still route it to `copilot`. -- `pretooluse-*.json`, `sessionstart.json`, `posttooluse-mcp-update.json` — - the native camelCase shape (`toolName` + `toolArgs` as an escaped JSON - string), per docs.github.com hooks-reference. +- `pretooluse-*.json` and `sessionstart.json` — redacted native captures from + GitHub Copilot CLI 1.0.73 (`toolName` + `toolArgs` as an escaped JSON + string). Native file tools use an absolute `path`; `create` uses + `file_text`, while `edit` uses `old_str` and `new_str`. +- `posttooluse-mcp-update.json` — a redacted native update capture. Copilot + prefixes MCP tools with the server name and a hyphen + (`archcore-update_document`). -**PROVISIONAL:** the exact keys inside `toolArgs` per native tool -(`create`/`edit`/`str_replace_editor`/`apply_patch`) and the MCP `toolName` -format are not documented. These fixtures encode the current best guess and -MUST be replaced with real redacted captures during the Copilot smoke test -(see `copilot-host-support.rnd` Next Action) before the adapter ships. +Session IDs, working directories, prompts, and file contents are redacted; +field names and nesting are preserved from the captured payloads. diff --git a/test/fixtures/stdin/copilot/posttooluse-mcp-update.json b/test/fixtures/stdin/copilot/posttooluse-mcp-update.json index 8311d57..ef611a8 100644 --- a/test/fixtures/stdin/copilot/posttooluse-mcp-update.json +++ b/test/fixtures/stdin/copilot/posttooluse-mcp-update.json @@ -1 +1 @@ -{"sessionId":"s1","timestamp":1751700000000,"cwd":"/work","toolName":"mcp__archcore__update_document","toolArgs":"{\"path\":\"auth/jwt-strategy.adr.md\",\"content\":\"## Context\"}","toolResult":{"resultType":"success","textResultForLlm":"OK"}} +{"sessionId":"","timestamp":1784817896763,"cwd":"/work","toolName":"archcore-update_document","toolArgs":"{\"path\":\".archcore/copilot-hook-probe.doc.md\",\"content\":\"## Purpose\\nCaptured native Copilot post-tool payload.\"}","toolResult":{"resultType":"success","textResultForLlm":"{\"category\":\"knowledge\",\"path\":\".archcore/copilot-hook-probe.doc.md\",\"status\":\"draft\",\"title\":\"Copilot Hook Probe\",\"type\":\"doc\"}"}} diff --git a/test/fixtures/stdin/copilot/pretooluse-create-archcore.json b/test/fixtures/stdin/copilot/pretooluse-create-archcore.json index c7d0b94..0abfbe8 100644 --- a/test/fixtures/stdin/copilot/pretooluse-create-archcore.json +++ b/test/fixtures/stdin/copilot/pretooluse-create-archcore.json @@ -1 +1 @@ -{"sessionId":"s1","timestamp":1751700000000,"cwd":"/work","toolName":"create","toolArgs":"{\"file_path\":\".archcore/my.rule.md\",\"content\":\"# Rule\"}"} +{"sessionId":"","timestamp":1784816794176,"cwd":"/work","toolName":"create","toolArgs":"{\"path\":\"/work/.archcore/my.rule.md\",\"file_text\":\"# Rule\"}"} diff --git a/test/fixtures/stdin/copilot/pretooluse-edit-regular.json b/test/fixtures/stdin/copilot/pretooluse-edit-regular.json index d61f168..5d7a2c0 100644 --- a/test/fixtures/stdin/copilot/pretooluse-edit-regular.json +++ b/test/fixtures/stdin/copilot/pretooluse-edit-regular.json @@ -1 +1 @@ -{"sessionId":"s1","timestamp":1751700000000,"cwd":"/work","toolName":"edit","toolArgs":"{\"file_path\":\"src/app.py\",\"old_str\":\"a\",\"new_str\":\"b\"}"} +{"sessionId":"","timestamp":1784816974910,"cwd":"/work","toolName":"edit","toolArgs":"{\"path\":\"/work/src/app.py\",\"old_str\":\"a\",\"new_str\":\"b\"}"} diff --git a/test/fixtures/stdin/copilot/sessionstart.json b/test/fixtures/stdin/copilot/sessionstart.json index b0d281b..32fc916 100644 --- a/test/fixtures/stdin/copilot/sessionstart.json +++ b/test/fixtures/stdin/copilot/sessionstart.json @@ -1 +1 @@ -{"sessionId":"s1","timestamp":1751700000000,"cwd":"/work","source":"startup"} +{"sessionId":"","timestamp":1784816768887,"cwd":"/work","source":"new","initialPrompt":""} diff --git a/test/structure/copilot-plugin.bats b/test/structure/copilot-plugin.bats new file mode 100644 index 0000000..e821a87 --- /dev/null +++ b/test/structure/copilot-plugin.bats @@ -0,0 +1,105 @@ +#!/usr/bin/env bats +# Structure tests: validate the native GitHub Copilot CLI adapter. + +setup() { + load '../helpers/common' + common_setup +} + +MANIFEST_REL=".plugin/plugin.json" +HOOKS_REL="hooks/copilot.hooks.json" + +@test "Copilot manifest exists and is valid JSON" { + local manifest="$PLUGIN_ROOT/$MANIFEST_REL" + [ -f "$manifest" ] + jq . < "$manifest" > /dev/null +} + +@test "Copilot manifest points explicitly at every shared component" { + local manifest="$PLUGIN_ROOT/$MANIFEST_REL" + jq -e ' + .name == "archcore" and + .hooks == "./hooks/copilot.hooks.json" and + .mcpServers == "./.mcp.json" and + .skills == "./skills/" and + .agents == "./agents/" + ' "$manifest" > /dev/null +} + +@test "Copilot manifest version matches the Claude manifest" { + local copilot="$PLUGIN_ROOT/$MANIFEST_REL" + local claude="$PLUGIN_ROOT/.claude-plugin/plugin.json" + [ "$(jq -r '.version' "$copilot")" = "$(jq -r '.version' "$claude")" ] +} + +@test "Copilot hooks config exists and is valid version 1 JSON" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + [ -f "$hooks" ] + jq -e '.version == 1 and (.hooks | type == "object")' "$hooks" > /dev/null +} + +@test "Copilot hooks use only native camelCase lifecycle events" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + local events + events=$(jq -r '.hooks | keys[]' "$hooks" | sort | tr '\n' ',') + [ "$events" = "postToolUse,preToolUse,sessionStart," ] \ + || fail "unexpected Copilot hook event set: $events" +} + +@test "every Copilot hook entry sets deterministic host detection" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + jq -e ' + ([.hooks[][]] | length) == 6 and + all(.hooks[][]; .type == "command" and .env.ARCHCORE_HOST == "copilot") + ' "$hooks" > /dev/null +} + +@test "every Copilot hook runs from the user project root" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + jq -e ' + all(.hooks[][]; .cwd == ".") + ' "$hooks" > /dev/null +} + +@test "Copilot hook commands use COPILOT_PLUGIN_ROOT and the shared scripts" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + local actual expected + actual=$(jq -r '.hooks[][] | .bash' "$hooks" | sort) + expected=$(printf '%s\n' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/check-archcore-write' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/check-cascade' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/check-code-alignment' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/check-precision' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/session-start' \ + '"${COPILOT_PLUGIN_ROOT}"/bin/validate-archcore' | sort) + [ "$actual" = "$expected" ] || { + echo "expected: $expected" + echo "actual: $actual" + fail "Copilot hook commands must route to the shared bin scripts" + } +} + +@test "Copilot preToolUse covers every native mutation tool" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + local matcher="create|edit|str_replace_editor|apply_patch" + jq -e --arg matcher "$matcher" ' + (.hooks.preToolUse | length) == 2 and + all(.hooks.preToolUse[]; + .matcher == $matcher and + .timeoutSec == 1 and + (.bash | test("/bin/check-(archcore-write|code-alignment)$")) + ) + ' "$hooks" > /dev/null +} + +@test "Copilot postToolUse self-filters through all shared validation scripts" { + local hooks="$PLUGIN_ROOT/$HOOKS_REL" + jq -e ' + (.hooks.postToolUse | length) == 3 and + all(.hooks.postToolUse[]; + (has("matcher") | not) and + .timeoutSec == 3 and + (.bash | test("/bin/(validate-archcore|check-cascade|check-precision)$")) + ) + ' "$hooks" > /dev/null +} diff --git a/test/structure/host-coverage-matrix.bats b/test/structure/host-coverage-matrix.bats index 30f39ac..27b4e7c 100644 --- a/test/structure/host-coverage-matrix.bats +++ b/test/structure/host-coverage-matrix.bats @@ -18,12 +18,13 @@ matrix_rows() { hooks/hooks.json|SessionStart|PostToolUse,PreToolUse,SessionStart,|PreToolUse|Write Edit hooks/cursor.hooks.json|sessionStart|afterMCPExecution,preToolUse,sessionStart,|preToolUse|Write hooks/codex.hooks.json|SessionStart|PostToolUse,PreToolUse,SessionStart,|PreToolUse|Write Edit apply_patch +hooks/copilot.hooks.json|sessionStart|postToolUse,preToolUse,sessionStart,|preToolUse|create edit str_replace_editor apply_patch ROWS } # Normalize a hooks config's command list to sorted unique script basenames. script_basenames() { - jq -r '.. | .command? // empty' "$1" | sed 's|"||g' | awk -F/ '{print $NF}' | sort -u + jq -r '.. | .command? // .bash? // empty' "$1" | sed 's|"||g' | awk -F/ '{print $NF}' | sort -u } @test "host matrix: every host hooks file has the expected event set" { @@ -40,7 +41,9 @@ script_basenames() { local file session_key cmds while IFS='|' read -r file session_key _ _ _; do [ -z "$file" ] && continue - cmds=$(jq -r --arg e "$session_key" '.hooks[$e][]?.hooks[]?.command // empty' "$PLUGIN_ROOT/$file") + cmds=$(jq -r --arg e "$session_key" \ + '.hooks[$e][]? | (.hooks[]?.command // .command? // .bash? // empty)' \ + "$PLUGIN_ROOT/$file") echo "$cmds" | grep -q 'bin/session-start' \ || fail "$file: '$session_key' must invoke bin/session-start; got: $cmds" done < <(matrix_rows) @@ -51,14 +54,18 @@ script_basenames() { while IFS='|' read -r file _ _ guard_event tools; do [ -z "$file" ] && continue # The write-guard event must run both check-archcore-write and check-code-alignment. - entry_cmds=$(jq -r --arg e "$guard_event" '.hooks[$e][]?.hooks[]?.command // empty' "$PLUGIN_ROOT/$file") + entry_cmds=$(jq -r --arg e "$guard_event" \ + '.hooks[$e][]? | (.hooks[]?.command // .command? // .bash? // empty)' \ + "$PLUGIN_ROOT/$file") echo "$entry_cmds" | grep -q 'bin/check-archcore-write' \ || fail "$file: '$guard_event' must invoke bin/check-archcore-write; got: $entry_cmds" echo "$entry_cmds" | grep -q 'bin/check-code-alignment' \ || fail "$file: '$guard_event' must invoke bin/check-code-alignment; got: $entry_cmds" # The guard entry's matcher must cover every mutation tool of that host. guard_matcher=$(jq -r --arg e "$guard_event" \ - '.hooks[$e][] | select(.hooks[]?.command | test("check-archcore-write")) | .matcher' \ + '.hooks[$e][] | + select((.hooks[]?.command // .command? // .bash? // "") | test("check-archcore-write")) | + .matcher' \ "$PLUGIN_ROOT/$file" | head -1) for tok in $tools; do case "$guard_matcher" in diff --git a/test/structure/release-blocklist.bats b/test/structure/release-blocklist.bats index 02e4ee6..847b93c 100644 --- a/test/structure/release-blocklist.bats +++ b/test/structure/release-blocklist.bats @@ -44,6 +44,11 @@ is_stripped() { || fail ".codex-plugin/ is in release.yml blocklist — Codex install will break" } +@test "release.yml does not strip .plugin/" { + ! is_stripped '\.plugin' \ + || fail ".plugin/ is in release.yml blocklist — Copilot install will break" +} + @test "release.yml does not strip .agents/" { ! is_stripped '\.agents' \ || fail ".agents/ is in release.yml blocklist — Codex marketplace registry will be missing" diff --git a/test/unit/check-cascade.bats b/test/unit/check-cascade.bats index 77be3ba..bd0ee5f 100644 --- a/test/unit/check-cascade.bats +++ b/test/unit/check-cascade.bats @@ -127,6 +127,14 @@ run_cascade_stdin() { assert_output --partial "impl.plan.md" } +@test "copilot: native MCP update emits cascade info as Copilot JSON" { + create_sync_state '{"source":"impl.plan.md","target":"copilot-hook-probe.doc.md","type":"implements"}' + run_cascade copilot/posttooluse-mcp-update.json + assert_success + assert_output --partial '"additionalContext"' + assert_output --partial "impl.plan.md" +} + # --- Pretty-printed sync-state --- @test "handles pretty-printed sync-state.json" { diff --git a/test/unit/normalize-stdin.bats b/test/unit/normalize-stdin.bats index c575e82..5f8f3aa 100644 --- a/test/unit/normalize-stdin.bats +++ b/test/unit/normalize-stdin.bats @@ -27,7 +27,7 @@ setup() { } @test "detects copilot host from native camelCase toolName (no hookEventName)" { - run_normalizer '{"sessionId":"s1","timestamp":1751700000000,"cwd":"/work","toolName":"create","toolArgs":"{\"file_path\":\"x.md\"}"}' + run_normalizer '{"sessionId":"s1","timestamp":1784816794176,"cwd":"/work","toolName":"create","toolArgs":"{\"path\":\"/work/x.md\"}"}' assert_success assert_line "HOST=copilot" } @@ -147,21 +147,28 @@ setup() { # --- Copilot field extraction --- @test "copilot: extracts toolName from native payload" { - run_normalizer '{"sessionId":"s1","toolName":"create","toolArgs":"{\"file_path\":\".archcore/my.rule.md\"}"}' + run_normalizer '{"sessionId":"s1","toolName":"create","toolArgs":"{\"path\":\"/work/.archcore/my.rule.md\"}"}' assert_success assert_line "TOOL=create" } -@test "copilot: extracts file path from escaped toolArgs" { - run_normalizer '{"sessionId":"s1","toolName":"create","toolArgs":"{\"file_path\":\".archcore/my.rule.md\",\"content\":\"x\"}"}' +@test "copilot: extracts native create path from escaped toolArgs" { + run_normalizer '{"sessionId":"s1","toolName":"create","toolArgs":"{\"path\":\"/work/.archcore/my.rule.md\",\"file_text\":\"x\"}"}' assert_success - assert_line "FILE=.archcore/my.rule.md" + assert_line "FILE=/work/.archcore/my.rule.md" } -@test "copilot: extracts doc path from escaped toolArgs (MCP)" { - run_normalizer '{"sessionId":"s1","toolName":"mcp__archcore__update_document","toolArgs":"{\"path\":\"auth/jwt.adr.md\"}"}' +@test "copilot: extracts native edit path from escaped toolArgs" { + run_normalizer '{"sessionId":"s1","toolName":"edit","toolArgs":"{\"path\":\"/work/src/app.py\",\"old_str\":\"a\",\"new_str\":\"b\"}"}' assert_success - assert_line "DOC=auth/jwt.adr.md" + assert_line "FILE=/work/src/app.py" +} + +@test "copilot: normalizes native MCP tool name and extracts doc path" { + run_normalizer '{"sessionId":"s1","toolName":"archcore-update_document","toolArgs":"{\"path\":\".archcore/copilot-hook-probe.doc.md\"}"}' + assert_success + assert_line "TOOL=mcp__archcore__update_document" + assert_line "DOC=.archcore/copilot-hook-probe.doc.md" } @test "copilot: legacy hybrid payload still extracts tool_name" { diff --git a/test/unit/validate-archcore.bats b/test/unit/validate-archcore.bats index d567fec..9d94cf9 100644 --- a/test/unit/validate-archcore.bats +++ b/test/unit/validate-archcore.bats @@ -80,6 +80,14 @@ setup() { assert_output --partial "additional_context" } +@test "copilot native MCP update triggers validation with Copilot JSON" { + mock_archcore "✗ broken relation" + run_with_fixture validate-archcore copilot/posttooluse-mcp-update.json + assert_success + assert_output --partial '"additionalContext"' + assert_output --partial "validation found issues" +} + # --- Invocation contract: which subcommand actually ran? --- @test "validate-archcore calls archcore doctor (not validate)" {