diff --git a/.archcore/.sync-state.json b/.archcore/.sync-state.json index 3599f4c..7287c06 100644 --- a/.archcore/.sync-state.json +++ b/.archcore/.sync-state.json @@ -3371,6 +3371,46 @@ "source": "plugin/component-registry.doc.md", "target": "plugin/host-wiring-parity.adr.md", "type": "related" + }, + { + "source": "plugin/copilot-mcp-architecture.adr.md", + "target": "plugin/copilot-adapter-design.adr.md", + "type": "extends" + }, + { + "source": "plugin/copilot-mcp-architecture.adr.md", + "target": "plugin/cursor-mcp-architecture.adr.md", + "type": "related" + }, + { + "source": "plugin/copilot-mcp-architecture.adr.md", + "target": "plugin/host-wiring-parity.adr.md", + "type": "related" + }, + { + "source": "plugin/copilot-mcp-architecture.adr.md", + "target": "plugin/component-registry.doc.md", + "type": "related" + }, + { + "source": "plugin/host-probe-protocol.spec.md", + "target": "plugin/host-adapter-contract.spec.md", + "type": "implements" + }, + { + "source": "plugin/host-probe-protocol.spec.md", + "target": "plugin/hooks-validation-system.spec.md", + "type": "related" + }, + { + "source": "plugin/host-probe-protocol.spec.md", + "target": "plugin/jtbd1-phase2-hardening-delegated.plan.md", + "type": "related" + }, + { + "source": "plugin/host-probe-protocol.spec.md", + "target": "plugin/plugin-testing.guide.md", + "type": "related" } ] } diff --git a/.archcore/plugin/actualize-system.spec.md b/.archcore/plugin/actualize-system.spec.md index 2aa2461..5b23dca 100644 --- a/.archcore/plugin/actualize-system.spec.md +++ b/.archcore/plugin/actualize-system.spec.md @@ -8,6 +8,8 @@ tags: - "validation" --- +--- + > **Outcome (2026-05-15):** The Actualize system shipped, but as the `--drift` mode of the unified `/archcore:audit` skill rather than as a standalone `/archcore:actualize` skill. Layer 1 and Layer 2 (the `bin/check-staleness` SessionStart hook and the `bin/check-cascade` PostToolUse hook) shipped as designed. Layer 3 was folded into `audit` per `skill-surface-collapse.adr.md`. The detection protocol now lives at `skills/audit/lib/drift-detection.md`. This spec is preserved for historical context; the active contract is the `--drift` mode in `commands-system.spec.md` and `plugin-architecture.spec.md`. ## Purpose @@ -106,7 +108,7 @@ PostToolUse hook, fires after `mcp__archcore__update_document` succeeds. Does NO #### Handler -`bin/check-cascade`, registered as a PostToolUse hook entry across all hosts (`hooks/hooks.json`, `hooks/cursor.hooks.json`, `hooks/codex.hooks.json`). +`bin/check-cascade`, registered as a PostToolUse hook entry in all four host hook configs (`hooks/hooks.json`, `hooks/cursor.hooks.json`, `hooks/codex.hooks.json`, `hooks/copilot.hooks.json`). #### Detection Logic @@ -130,6 +132,8 @@ PostToolUse hook, fires after `mcp__archcore__update_document` succeeds. Does NO } ``` +The wrapper above is Claude Code's and Codex's. Cursor takes `additional_context`, Copilot a bare top-level `additionalContext`, and OpenCode the plain message — the script never builds these by hand; `bin/lib/normalize-stdin.sh` output helpers pick the shape from `ARCHCORE_HOST`. + #### Relation Direction Table | Relation in graph | Updated doc role | Potentially stale doc | Why | @@ -183,7 +187,7 @@ Drift mode loads `skills/audit/lib/drift-detection.md` for the detailed protocol ### hooks configuration -The cascade PostToolUse entry is shipped in all three host hook configs: +The cascade PostToolUse entry is shipped in all four host hook configs. Claude Code, Cursor and Codex share this shape (the plugin-root variable differs per host): ```json { @@ -192,6 +196,18 @@ The cascade PostToolUse entry is shipped in all three host hook configs: } ``` +Copilot's entry is structurally different and cannot be produced by search-and-replacing the variable — it is a flat object using `bash` instead of `command`, `timeoutSec` instead of `timeout`, and **no matcher at all**, because Copilot's `postToolUse` does not take one. The script self-filters there on the normalized tool name: + +```json +{ + "type": "command", + "cwd": ".", + "bash": "\"${COPILOT_PLUGIN_ROOT}\"/bin/check-cascade", + "env": { "ARCHCORE_HOST": "copilot" }, + "timeoutSec": 3 +} +``` + SessionStart hook calls `bin/check-staleness` internally as part of `bin/session-start`. ### bin/ Scripts @@ -206,7 +222,7 @@ Requirements: executable, exits 0, completes within 3 seconds, POSIX shell, degr PostToolUse handler for cascade detection after `update_document`. -Requirements: executable, exits 0, reads JSON from stdin, outputs JSON with `hookSpecificOutput` when cascade detected, POSIX shell. +Requirements: executable, exits 0, reads JSON from stdin, outputs the host-shaped context envelope when cascade detected, POSIX shell. ## Normative Behavior @@ -216,6 +232,7 @@ Requirements: executable, exits 0, reads JSON from stdin, outputs JSON with `hoo - Layer 2 MUST fire only after `update_document`, not after `create_document` or `remove_document`. - Layer 2 MUST only flag documents connected via `implements`, `depends_on`, or `extends` (not `related`). - Layer 2 MUST NOT block the update operation. +- WHERE a host's PostToolUse event accepts no matcher, `bin/check-cascade` MUST reach the same decision by filtering on the normalized tool name — the set of updates that trigger a cascade warning MUST NOT differ by host. - Layer 3 (`/archcore:audit --drift`) MUST verify MCP availability before analysis. - Layer 3 MUST NOT modify documents without explicit user confirmation per document. - Layer 3 MUST present findings grouped by severity (critical, cascade, temporal). @@ -252,7 +269,7 @@ The Actualize system conforms to this specification if: 1. `bin/check-staleness` runs at SessionStart and produces code-drift warnings when applicable. 2. `bin/check-cascade` runs after `update_document` and produces cascade warnings when applicable. -3. Every host hook config (`hooks.json`, `cursor.hooks.json`, `codex.hooks.json`) registers `check-cascade` on `update_document`. +3. Every host hook config (`hooks.json`, `cursor.hooks.json`, `codex.hooks.json`, `copilot.hooks.json`) registers `check-cascade` on `update_document` — by matcher where the host has one, and by the script's own filtering on Copilot, which has none. 4. `/archcore:audit --drift` exists as a mode of the `audit` intent skill, with routing-table support and the 3-dimension analysis. 5. The drift protocol lives at `skills/audit/lib/drift-detection.md`. 6. All hooks complete within their timeout budgets. diff --git a/.archcore/plugin/agent-system.spec.md b/.archcore/plugin/agent-system.spec.md index f5556a7..1057511 100644 --- a/.archcore/plugin/agent-system.spec.md +++ b/.archcore/plugin/agent-system.spec.md @@ -8,11 +8,11 @@ tags: ## Purpose -Define the contract for the Archcore Claude Plugin's subagents — `archcore-assistant` (read/write) and `archcore-auditor` (read-only). +Define the contract for the Archcore plugin's subagents — `archcore-assistant` (read/write) and `archcore-auditor` (read-only) — across every host that loads them. ## Scope -This specification covers both agent definitions in `agents/`, their system prompts, tool restrictions, invocation triggers, and domain expertise. +This specification covers both agent definitions, their per-host file formats, their system prompts, tool restrictions, invocation triggers, and domain expertise. ## Authority @@ -20,13 +20,29 @@ This specification is the authoritative reference for both agents. The Single Un ## Subject +### Per-host file formats + +The two agents are one definition each, shipped in the format every host's loader accepts. Content is identical across formats; only the container differs. + +| Format | Location | Read by | Notes | +|---|---|---|---| +| `.md` | `agents/` | Claude Code, Cursor | The canonical source; agent id comes from frontmatter `name:` | +| `.toml` | `agents/` | Codex CLI | Adds `sandbox_mode` and `disabled_tools[]`; body kept in parity with the MD | +| `.agent.md` | `copilot-agents/` | GitHub Copilot CLI | Byte-identical copy of the MD; agent id comes from the filename | + +Copilot's copies sit in a directory of their own rather than beside the originals. Its loader accepts only the `*.agent.md` extension, and that extension still matches the `*.md` glob Claude Code and Cursor use — a sibling copy would give both hosts two files declaring the same `name:`. The TOML variants never had this problem because their extension is foreign to every md-globbing host. `test/structure/agents.bats` holds the copies byte-identical (`cmp`) and fails when an agent in `agents/` has no counterpart. + +### MCP tool naming + +A tool list is only as good as the names in it. The same MCP server appears under three names depending on how it was registered: `mcp__archcore__*` (project `.mcp.json`), `mcp__plugin_archcore_archcore__*` (plugin-bundled on Claude Code), and the flat `archcore-` (Copilot, which joins server and tool with a hyphen). Every allow-list and deny-list in the agent files carries all three. This matters asymmetrically: an allow-list missing a name loses a capability, while the auditor's TOML **deny**-list missing a name silently grants the read-only agent the power to mutate. `test/structure/agents.bats` pins the twins in both directions. + ### Agent 1: archcore-assistant (Read/Write) Handles complex, multi-step documentation tasks requiring write access to MCP tools. #### Definition File -Location: `agents/archcore-assistant.md` +Location: `agents/archcore-assistant.md` (plus the TOML and `*.agent.md` variants above) ```yaml --- @@ -54,9 +70,11 @@ tools: --- ``` +(Abbreviated: the shipped file lists each MCP tool under all three namings.) + #### Invocation Triggers -Claude should invoke `archcore-assistant` when: +The host should invoke `archcore-assistant` when: - User requests creation of multiple related documents - Task involves requirements decomposition (e.g., "break this PRD into specifications") @@ -69,7 +87,7 @@ Performs documentation health checks without any mutation capability. #### Definition File -Location: `agents/archcore-auditor.md` +Location: `agents/archcore-auditor.md` (plus the TOML and `*.agent.md` variants above) ```yaml --- @@ -94,7 +112,7 @@ tools: #### Invocation Triggers -Claude should invoke `archcore-auditor` when: +The host should invoke `archcore-auditor` when: - User asks for a documentation audit, health check, or review - User asks "what's missing?" or "what needs attention?" about documentation @@ -186,6 +204,7 @@ Three tracks that can coexist: - MUST, immediately after the bootstrap calls return, note the categories present, the most common tags, recent accepted decisions, and any draft plans before proceeding with the user's task. This synthesis is a read-only transformation over data already in hand; it adds no new tool calls. - MUST use MCP tools for all `.archcore/` operations (no Write/Edit/Bash). - MUST call `list_documents` before creating any document to prevent duplicates (subsumed by the bootstrap requirement above, retained for emphasis). +- MUST list every MCP tool they use under all three namings, so the definition works whether the server was registered by the project, by the plugin, or by Copilot's flattening. - Should explain reasoning when choosing document types or relation types. ### archcore-assistant Only @@ -209,6 +228,7 @@ Three tracks that can coexist: - System prompts must not exceed 2000 lines. - Neither agent may modify files outside `.archcore/` via any means. - Both agents respect existing document statuses. +- Format variants exist only where a host's loader requires one. They are copies, not forks: no host-specific instruction may enter any variant (`host-adapter-contract.spec`). ## Invariants @@ -217,6 +237,8 @@ Three tracks that can coexist: - Both agents check for existing documents before suggesting creation. - Every sub-agent invocation's first tool calls are `list_documents` and `list_relations` (bootstrap requirement per `subagent-knowledge-tree-bootstrap.adr`). `archcore-auditor` has no exception to this invariant; `archcore-assistant` has a narrow exception only for strictly single-document reads with explicit paths. - Both agent system prompts carry a `# First Step — Bootstrap Knowledge Tree` section as the first content section after the YAML frontmatter, with cross-references to `remove-skill-verify-mcp-preamble.cpat` and `subagent-knowledge-tree-bootstrap.adr`, and with the synthesis directive anchor literal `recent accepted decisions` present. +- Every agent present in `agents/*.md` has a byte-identical `copilot-agents/.agent.md` counterpart. +- No Copilot-only tool entry exists without its canonical twin. ## Error Handling @@ -228,10 +250,10 @@ Three tracks that can coexist: An agent conforms to this specification if: -1. It resides at `agents/.md` with the correct frontmatter -2. Its tool list matches the allowed tools exactly (per tool access matrix) +1. It resides at `agents/.md` with the correct frontmatter, and every format variant its hosts require exists: `agents/.toml` for Codex, `copilot-agents/.agent.md` for Copilot +2. Its tool list matches the allowed tools exactly (per tool access matrix), under all three MCP namings 3. Its system prompt covers the shared domain knowledge 4. It follows the normative behavior for its role 5. archcore-auditor produces no mutations; archcore-assistant produces structured output 6. Its system prompt carries the `# First Step — Bootstrap Knowledge Tree` section per `subagent-knowledge-tree-bootstrap.adr`, including cross-references to that ADR and to `remove-skill-verify-mcp-preamble.cpat`, plus the synthesis directive whose anchor literal `recent accepted decisions` is grep-able in both files -7. `test/structure/agents.bats` asserts the bootstrap preamble and the synthesis directive anchor are present in both agent files +7. `test/structure/agents.bats` asserts the bootstrap preamble, the synthesis directive anchor, the three-way tool naming, and byte-identity of the Copilot copies 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/commands-system.spec.md b/.archcore/plugin/commands-system.spec.md index 35c17e2..0585500 100644 --- a/.archcore/plugin/commands-system.spec.md +++ b/.archcore/plugin/commands-system.spec.md @@ -10,7 +10,7 @@ tags: Define the contract for user-invoked skills: their discoverability, naming, argument handling, and behavior when users invoke them via slash commands. The current surface is **7 commands**, all auto-invocable, governed by `skill-surface-collapse.adr.md`. The prior tiered structure (intent/track/utility) is superseded — every remaining skill is intent-class. -Note: Claude Code and Cursor surface user-invoked workflows directly from skills. Codex CLI requires `commands/*.md` wrappers — thin host-adapter shims that delegate to `skills//SKILL.md`. The skill file remains the single behavioral source of truth. +Note: Claude Code and Cursor surface user-invoked workflows directly from skills. Codex CLI requires `commands/*.md` wrappers — thin host-adapter shims that delegate to `skills//SKILL.md`. GitHub Copilot CLI surfaces skills directly too, but its manifest gives the `commands` field no default path, so `.plugin/plugin.json` names `./commands/` explicitly; without that pointer the wrappers simply do not load there. On Copilot a skill takes precedence over a command of the same name, so the wrappers act as a fallback surface rather than the primary one. The skill file remains the single behavioral source of truth on every host. ## Scope @@ -96,7 +96,7 @@ The `/archcore:init` command accepts `--mode=small|medium|large` (force the dete ### Discoverability -Claude Code, Cursor, and Codex CLI all show the 7 skills in a flat list. Discoverability is supported by: +All four hosts — Claude Code, Cursor, Codex CLI, GitHub Copilot CLI — show the 7 skills in a flat list. Discoverability is supported by: 1. **`/archcore:help`** — explains the active surface and routes users to the right command. 2. **SessionStart empty-state nudge** — on fresh repos, the session-start hook points users at `/archcore:init` so onboarding is self-routing. @@ -139,6 +139,7 @@ The right skill auto-invokes from the phrasing. - The visible palette MUST be exactly 7 commands. Adding an eighth skill requires a new ADR. - Commands ask at most one scope-confirmation question before starting execution (`/archcore:init` is the exception: it presents one preview manifest and proceeds on a single `confirm` / `edit` / `cancel`). - Flow steps within `plan` ask at most 1–2 content questions per document step. +- Every host that needs the `commands/` wrappers MUST point at them from its manifest where the host provides no default (Copilot); a missing pointer removes the entire `/archcore:*` surface on that host and is pinned by `test/structure/copilot-plugin.bats`. ## Invariants @@ -148,6 +149,7 @@ The right skill auto-invokes from the phrasing. - Every analysis command gathers data via MCP read tools before producing output. - The `help` command accurately reflects the current 7-command surface and notes direct-MCP access for any document type. - Every Archcore document type has at least one intent path that can create it. +- The set of `commands/*.md` wrappers matches the set of `skills//` directories exactly — a wrapper naming a removed skill would surface a `/archcore:` entry that dead-ends. ## Error Handling @@ -158,11 +160,11 @@ The right skill auto-invokes from the phrasing. ## Conformance -A user-invoked skill or Codex command wrapper conforms to this specification if: +A user-invoked skill or its command wrapper conforms to this specification if: -1. Its behavior resides at `skills//SKILL.md` and `` is one of: `init`, `capture`, `decide`, `plan`, `audit`, `context`, `help`. Codex may also expose a matching `commands/.md` wrapper. +1. Its behavior resides at `skills//SKILL.md` and `` is one of: `init`, `capture`, `decide`, `plan`, `audit`, `context`, `help`. Codex and Copilot may also expose a matching `commands/.md` wrapper — required on Codex, which does not surface skills directly, and a fallback on Copilot, which does. 2. Its description uses the "Activate when X. Do NOT activate for Y." trigger format. 3. It uses MCP tools exclusively for document operations. 4. Creation commands check for duplicates before creation and suggest relations after. 5. Analysis commands gather data via MCP read tools. -6. Its argument handling matches its `argument-hint:` frontmatter. \ No newline at end of file +6. Its argument handling matches its `argument-hint:` frontmatter. diff --git a/.archcore/plugin/component-registry.doc.md b/.archcore/plugin/component-registry.doc.md index 7e5ec1d..f58aa9f 100644 --- a/.archcore/plugin/component-registry.doc.md +++ b/.archcore/plugin/component-registry.doc.md @@ -8,13 +8,13 @@ tags: ## Overview -Reference document listing all components of the Archcore Plugin (multi-host: Claude Code, Cursor, Codex CLI). +Reference document listing all components of the Archcore Plugin (multi-host: Claude Code, Cursor, Codex CLI, GitHub Copilot 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, Cursor and Copilot CLI surface user-invoked workflows directly from skills (`skills//SKILL.md`). Codex CLI discovers slash commands from `commands/*.md` wrappers — thin host-adapter shims that delegate to the matching skill — and Copilot loads the same wrappers as a secondary surface (a skill outranks a command of the same name there). 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`. -**Layout (post-relocation).** All component paths in this document are **plugin-root-relative** — the plugin root is the `plugins/archcore/` subdirectory (e.g. `skills/audit/` means `plugins/archcore/skills/audit/`, `bin/session-start` means `plugins/archcore/bin/session-start`). The three marketplace catalogs are the exception: they live at the **repo root** and point their `source`/`path` at `./plugins/archcore`. This root-catalog / subdirectory-manifest split is required for Codex marketplace discovery (a catalog `source.path` of `./` is never scanned); see `subdirectory-plugin-layout.adr.md` and issue #2. The reference template `docs/cursor.mcp.example.json` also stays at the repo root. +**Layout (post-relocation).** All component paths in this document are **plugin-root-relative** — the plugin root is the `plugins/archcore/` subdirectory (e.g. `skills/audit/` means `plugins/archcore/skills/audit/`, `bin/session-start` means `plugins/archcore/bin/session-start`). The three marketplace catalogs are the exception: they live at the **repo root** and point their `source`/`path` at `./plugins/archcore`. This root-catalog / subdirectory-manifest split is required for Codex marketplace discovery (a catalog `source.path` of `./` is never scanned); see `subdirectory-plugin-layout.adr.md` and issue #2. There are three catalogs and four hosts because Copilot CLI has no marketplace and installs by subdir spec instead. The reference template `docs/cursor.mcp.example.json` also stays at the repo root. ## Content @@ -58,15 +58,15 @@ Plain-markdown assets loaded at runtime by skills before composing documents. Th Companion ADR: `precision-over-coverage.adr.md` documents the design rationale. -### Codex Slash Command Wrappers (`commands/`) +### Slash Command Wrappers (`commands/`) Codex CLI requires a thin wrapper per user-facing skill so that `/archcore:` appears in its `/` menu. Each wrapper is a host-adapter shim — `description:` frontmatter plus a one-line delegate instruction pointing at `skills//SKILL.md`. No workflow logic lives here. | Wrapper Set | Count | Purpose | | --- | --- | --- | -| `commands/.md` (Codex CLI) | 7 | One per skill (`init`, `capture`, `decide`, `plan`, `audit`, `context`, `help`) — surfaces `/archcore:` in Codex | +| `commands/.md` | 7 | One per skill (`init`, `capture`, `decide`, `plan`, `audit`, `context`, `help`) — surfaces `/archcore:` on Codex CLI, and on Copilot CLI as a fallback behind the skill of the same name | -Conformance is enforced by `test/structure/codex-plugin.bats`: every entry must exist, carry `description:`, and reference the matching `skills//SKILL.md`. Claude Code and Cursor do not need wrappers — they surface skills directly. +Conformance is enforced by `test/structure/codex-plugin.bats` (every entry exists, carries `description:`, references the matching `skills//SKILL.md`) and by `test/structure/copilot-plugin.bats` (the wrapper set matches the skill set, and the manifest actually points at `commands/` — Copilot gives that field no default path, so without the pointer the entire `/archcore:*` surface is missing there). Claude Code and Cursor need no wrappers — they surface skills directly. ### Document-type coverage @@ -74,7 +74,7 @@ Every Archcore document type is reachable via an intent skill or direct MCP (`mc ### Visible `/` menu surface -**7 visible commands.** All 7 skills on disk are visible — no hidden or flagged-out skills. Codex CLI exposes the same 7 entries via the matching `commands/*.md` wrappers. +**7 visible commands.** All 7 skills on disk are visible — no hidden or flagged-out skills. Codex CLI exposes the same 7 entries via the matching `commands/*.md` wrappers; Copilot exposes them from the skills themselves, with the wrappers as backup. ### Agents (2) @@ -87,7 +87,17 @@ Every Archcore document type is reachable via an intent skill or direct MCP (`mc **archcore-auditor** — documentation health checks: coverage gaps, orphaned docs, stale statuses, code-document correlation (cross-references document path mentions with git history to flag drift). Background, yellow, max 15 turns. -For Codex CLI, both subagents also ship as TOML variants (`agents/archcore-assistant.toml`, `agents/archcore-auditor.toml`) — same `developer_instructions` content, plus Codex-specific `sandbox_mode` and `disabled_tools[]` fields. Agent tool lists — the `.md` allow-lists and the auditor TOML deny-list — carry BOTH MCP tool namings (`mcp__archcore__*` + `mcp__plugin_archcore_archcore__*`); twin pairing is guarded by `test/structure/agents.bats` (deny-lists fail open under naming drift, so the guard matters most there). +Both subagents ship in three formats, one per loader: + +| Format | Location | Read by | +| --- | --- | --- | +| `.md` | `agents/` | Claude Code, Cursor | +| `.toml` | `agents/` | Codex CLI (adds `sandbox_mode`, `disabled_tools[]`) | +| `.agent.md` | `copilot-agents/` | GitHub Copilot CLI | + +The Copilot copies live in a **separate directory** rather than beside the originals: Copilot loads plugin agents only from the `*.agent.md` extension, and `.agent.md` still matches the `*.md` glob Claude Code and Cursor use — a sibling copy would hand both hosts two files declaring the same frontmatter `name:`. The TOML variants never had this problem because their extension is foreign to every md-globbing host. `test/structure/agents.bats` holds the copies byte-identical (`cmp`) and fails if an agent in `agents/` has no counterpart. + +Agent tool lists — the `.md` allow-lists and the auditor TOML deny-list — carry ALL THREE MCP tool namings (`mcp__archcore__*`, `mcp__plugin_archcore_archcore__*`, and Copilot's flat `archcore-`); twin pairing is guarded by `test/structure/agents.bats` (deny-lists fail open under naming drift, so the guard matters most there). ### Hooks (6 entries across 3 events) @@ -100,14 +110,16 @@ For Codex CLI, both subagents also ship as TOML variants (`agents/archcore-assis | 5 | PostToolUse | `mcp__archcore__update_document` (+ `mcp__plugin_archcore_archcore__*` twin) | `bin/check-cascade` | 3s | | 6 | PostToolUse | `mcp__archcore__create_document\|mcp__archcore__update_document` (+ `mcp__plugin_archcore_archcore__*` twins) | `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 under a `bash` key rather than `command`, `timeoutSec` rather than `timeout`, deterministic `ARCHCORE_HOST=copilot`, and `cwd: "."` so hooks run from the user project). Copilot's `postToolUse` entries carry **no matcher** — the scripts self-filter there. -Every PostToolUse matcher lists each tool under BOTH namings — a project-level `.mcp.json` yields `mcp__archcore__*`, a plugin-bundled server yields `mcp__plugin_archcore_archcore__*`, and Claude Code matchers without regex metacharacters are exact matches (guarded by `test/structure/hooks.bats`; see `host-wiring-parity.adr.md`). +Every PostToolUse matcher lists each tool under BOTH namings — a project-level `.mcp.json` yields `mcp__archcore__*`, a plugin-bundled server yields `mcp__plugin_archcore_archcore__*`, and Claude Code matchers without regex metacharacters are exact matches (guarded by `test/structure/hooks.bats`; see `host-wiring-parity.adr.md`). Independently of matchers, `bin/lib/normalize-stdin.sh` folds all three namings to the canonical `mcp__archcore__*` before any guard inspects a tool name, so the scripts themselves match exactly one string. -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. +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. They act on disjoint path sets by construction — no conflict. Hook 6 (`check-precision`) is the Phase 1 implementation of the Precision Initiative (see `precision-over-coverage.adr.md`). It emits soft warnings via `additionalContext` for forbidden vague words, missing mandatory sections, frontmatter gaps, and stub-length bodies. It never blocks (always exits 0). +Both PreToolUse guards run on a 1-second budget on every host, and on Copilot a `preToolUse` timeout fails **open**. `test/unit/hook-latency.bats` keeps them far inside that budget; it exists because the injection guard once cost ~6 ms per matching document, so a knowledge base where ~170 documents mentioned a common source root blew the budget outright — and since injection is additive, the only symptom was context silently ceasing to arrive on exactly the repositories with the most of it. + 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`, extended per `host-wiring-parity.adr.md`): it exits silently — **before the CLI availability check, so a misrouted cwd never even sees the install nudge** — when `$PWD` contains an install-cache fragment (`.cursor/plugins/`, `.claude/plugins/`, `.codex/plugins/`, `plugins/cache/`) or when a bounded (depth-12) upward walk finds a `.cursor-plugin/`, `.claude-plugin/`, `.codex-plugin/`, or `.plugin/` manifest — so a cwd misrouted into a subdirectory of an install is caught too. It also emits a rate-limited (24h) outdated-CLI advisory backed by `archcore update --check`. @@ -118,28 +130,30 @@ The `bin/` tree contains hook scripts, shared shell libraries under `bin/lib/`, | Script | Hook Event | Purpose | | --- | --- | --- | -| `bin/lib/normalize-stdin.sh` | (library) | Multi-host stdin normalization. Detects host (Claude Code/Cursor/Copilot/Codex), extracts fields (tool_name, file_path, path), normalizes MCP tool names, provides output helpers (archcore_hook_block, archcore_hook_info, archcore_hook_pretool_info, archcore_hook_allow). Sourced by all hook scripts except check-staleness. | +| `bin/lib/normalize-stdin.sh` | (library) | Multi-host stdin normalization. Detects host (Claude Code/Cursor/Copilot/Codex/OpenCode), extracts fields (tool_name, file_path, path; Copilot's native payload carries the path inside an escaped JSON string under `toolArgs`), folds all three MCP tool namings to the canonical one, provides output helpers (archcore_hook_block, archcore_hook_info, archcore_hook_pretool_info, archcore_hook_allow). Sourced by all hook scripts except check-staleness. | | `bin/lib/empty-state.sh` | (library) | Defines `archcore_is_functionally_empty [dir]`: `.archcore/` counts as functionally empty when it contains no `.md` file larger than 200 bytes (filters stubs, `.gitkeep` placeholders, scaffolds). Pure POSIX, no jq/awk. Sourced by `bin/session-start` to decide the empty-state nudge. | | `bin/session-start` | SessionStart | Sources the normalizer; refuses FIRST to run from inside a plugin install (cache-path fragments + bounded depth-12 upward manifest walk — silent exit before any other output). Then, if `archcore` is not on PATH, prints an install message pointing at https://docs.archcore.ai/cli/install/ and exits 0. Otherwise detects missing `.archcore/` and emits init guidance (instructs the agent to call `mcp__archcore__init_project`), or invokes `archcore hooks session-start` directly, then calls `bin/check-staleness` and emits the rate-limited outdated-CLI advisory (`archcore update --check`). Always exits 0. | -| `bin/check-archcore-write` | PreToolUse | Blocks direct Write/Edit to `.archcore/**/*.md` with exit 2 + stderr message redirecting to MCP tools. Allows `.archcore/settings.json` and `.archcore/.sync-state.json`. Allows all paths outside `.archcore/`. | -| `bin/check-code-alignment` | PreToolUse | Injects relevant `.archcore/` context for source-file Write/Edit. Greps `.archcore/**/*.md` for documents referencing the edit path (directory prefixes, longest-first). Ranks by specificity → type priority (rule > cpat > adr > spec > guide). Emits top-3 via `hookSpecificOutput.additionalContext` (Claude Code/Codex/Copilot) or `additional_context` (Cursor). Never blocks; always exits 0. Honors `ARCHCORE_DISABLE_INJECTION=1` escape hatch and `.archcore/settings.json → codeAlignment.sourceRoots` override. | -| `bin/validate-archcore` | PostToolUse | Runs `archcore doctor` directly (timeout 2s) after MCP document operations (by tool_name prefix). The legacy Write/Edit branch in the script is retained as defensive code but is never reached from the current hooks config. Outputs JSON `hookSpecificOutput` when issues found, empty otherwise. Silently exits 0 if the CLI is unavailable. Always exits 0. | +| `bin/check-archcore-write` | PreToolUse | Blocks direct Write/Edit to `.archcore/**/*.md` — exit 2 + stderr on Claude/Codex/Cursor, stdout `{"permissionDecision":"deny",…}` on Copilot, where exit 2 is only a warning. Allows `.archcore/settings.json` and `.archcore/.sync-state.json`. Allows all paths outside `.archcore/`. | +| `bin/check-code-alignment` | PreToolUse | Injects relevant `.archcore/` context for source-file Write/Edit. Greps `.archcore/**/*.md` for documents referencing the edit path (directory prefixes, longest-first). Ranks by specificity → type priority (rule > cpat > adr > spec > guide). Emits top-3 via `hookSpecificOutput.additionalContext` (Claude Code/Codex), `additional_context` (Cursor), top-level `additionalContext` (Copilot), or the bare message (OpenCode). Cost is independent of how many documents match. Never blocks; always exits 0. Honors `ARCHCORE_DISABLE_INJECTION=1` escape hatch and `.archcore/settings.json → codeAlignment.sourceRoots` override. | +| `bin/validate-archcore` | PostToolUse | Runs `archcore doctor` directly (timeout 2s) after MCP document operations (by tool_name prefix, post-normalization so all three namings reach it). The legacy Write/Edit branch in the script is retained as defensive code but is never reached from the current hooks config. Outputs JSON `hookSpecificOutput` when issues found, empty otherwise. Silently exits 0 if the CLI is unavailable. Always exits 0. | | `bin/check-staleness` | SessionStart (called by `bin/session-start`) | Detects code-document drift via git: finds source files changed since the last `.archcore/` commit, cross-references with documents that mention affected directories. Rate-limited to once per 24h via a timestamp file. Emits only when matching documents exist. Outputs plain text warning (max 2KB) or empty. Always exits 0. | | `bin/check-cascade` | PostToolUse | After `update_document`, queries `.sync-state.json` relation graph for documents connected via `implements`, `depends_on`, or `extends` to the updated document. Outputs JSON `hookSpecificOutput` listing potentially stale dependents, or empty if no cascade. Always exits 0. | | `bin/check-precision` | PostToolUse | Phase 1 of the Precision Initiative. After `create_document` and `update_document`, reads the resulting file from disk and runs four checks: forbidden vagueness lexicon, mandatory sections by type, frontmatter title+status presence, body length ≥200 chars. Emits soft warnings via `additionalContext`. Always exits 0 (never blocks). See `precision-over-coverage.adr.md`. | | `bin/git-scope` | (skill helper — not wired to any hooks config) | Resolves a capped, ranked directory set from uncommitted working-tree changes (tracked diff vs HEAD + untracked files, `.archcore/` excluded) for `/archcore:context --git-changes`. Invoked by the context skill via Bash. Emits ≤20 directories ranked by changed-file count plus a `__TOTAL__ ` trailer, or a single sentinel (`__USAGE__`, `__NO_GIT__`, `__NOT_REPO__`, `__CLEAN__`). Always exits 0. Covered by the Makefile `BIN_SCRIPTS` lint/permissions set. | -| `bin/detect-host` | (skill helper — not wired to any hooks config) | Resolves the current AI host from environment only (`CLAUDECODE`/`CLAUDE_SKILL_DIR` → claude-code, `CURSOR_TRACE_ID` → cursor, `CODEX_HOME` → codex-cli; precedence claude > cursor > codex), printing exactly one token or `__UNKNOWN__`; never reads stdin or cwd. Invoked by the init skill to pick the `--agent` id for host wiring (`host-wiring-parity.adr.md`). Always exits 0. Covered by the Makefile `BIN_SCRIPTS` lint/permissions set. | +| `bin/detect-host` | (skill helper — not wired to any hooks config) | Resolves the current AI host from environment only (`CLAUDECODE`/`CLAUDE_SKILL_DIR` → claude-code, `CURSOR_TRACE_ID` → cursor, `CODEX_HOME` → codex-cli; precedence claude > cursor > codex), printing exactly one token or `__UNKNOWN__`; never reads stdin or cwd. **There is deliberately no Copilot branch**: every documented `COPILOT_*` variable is read *from* the user rather than set by the host, and `COPILOT_PLUGIN_ROOT` exists only inside hook processes — so a Copilot session resolves to `__UNKNOWN__` and reaches wiring through the init skill's ask path, which offers all four hosts. Invoked by the init skill to pick the `--agent` id for host wiring (`host-wiring-parity.adr.md`). Always exits 0. Covered by the Makefile `BIN_SCRIPTS` lint/permissions set. | | `bin/cli-gte` | (skill helper — not wired to any hooks config) | Deterministic semver gate: `cli-gte ` prints exactly one token — `yes` (installed `archcore --version` ≥ min), `no`, or `__NO_CLI__` (CLI missing/unparsable); always exits 0. Numeric field-by-field compare so `0.10.0 ≥ 0.6.0` resolves correctly — the init skill's pre-flight host-wiring gate calls it instead of comparing versions in prose (`host-wiring-parity.adr.md`). Contract in `test/unit/cli-gte.bats`. Covered by the Makefile `BIN_SCRIPTS` lint/permissions set. | ### Test Suite | 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). | -| Fixtures | `test/fixtures/stdin/` | — | Mock stdin JSON for Claude Code, Cursor, Copilot, Codex CLI, and malformed inputs | +| Unit tests | `test/unit/` | — | Test each bin script: stdin parsing, host detection, exit codes, output format, edge cases. `hook-latency.bats` pins both PreToolUse guards inside the 1s budget and asserts their cost does not scale with the number of matching documents. `probe-wrapper.bats` proves the probe harness wrapper is transparent across every stdin fixture. | +| Structure tests | `test/structure/` | — | Validate JSON configs, skill frontmatter, agent frontmatter, hook references, script permissions, rules. `hooks.bats` resolves every host's hook scripts from one table (Copilot's live under `bash`, not `command`, so a per-host copy would silently iterate an empty set) and carries the anti-regression invariants. `json-configs.bats` compares name/description/version across all four manifests. `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 wrapper parity. `copilot-plugin.bats` enforces Copilot manifest pointers, the absence of `mcpServers`, `./`-path resolution, hooks shape, project-root execution, and metadata parity. `probe-hygiene.bats` and `probe-records.bats` guard the probe protocol. `marketplace-discovery.bats` pins all three catalogs' `source`/`path` to the `plugins/archcore` subdirectory (issue #2 regression). | +| Integration tests | `test/integration/` | — | Host-install smoke tests (`codex-plugin-smoke.bats`, `copilot-plugin-smoke.bats`). Each skips when its host binary is absent, so they ship and stay quiet in CI while running for free on a contributor's machine. | +| Probe harness | `test/probe/` | — | `mkprobe` builds a disposable wrapped copy of the plugin for live-session verification. See `host-probe-protocol.spec.md`. | +| Fixtures | `test/fixtures/stdin/` | — | Mock stdin JSON for Claude Code, Cursor, Copilot, Codex CLI, OpenCode, 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. | +| Makefile | `Makefile` | — | Targets: `test`, `test-unit`, `test-structure`, `test-codex-smoke`, `test-copilot-smoke`, `lint`, `check-json`, `check-perms`, `verify`. Dev-only — stripped from `main` distribution. | | CI | `.github/workflows/test.yml` | — | GitHub Actions on push/PR to `dev`: macOS + Linux matrix, bats + shellcheck | | Release | `.github/workflows/release.yml` | — | GitHub Actions on tag push: strips dev-only artifacts, force-pushes the clean tree to `main`. See `docs/release.md` for the blocklist. | @@ -147,7 +161,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 MCP registration for **Claude Code only**, via `.mcp.json` at the plugin root (`plugins/archcore/.mcp.json`): ```json { @@ -164,9 +178,13 @@ The `command` resolves through PATH — users must have the Archcore CLI install 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"]`. -Cursor is the exception. The plugin **does not** ship a plugin-MCP for Cursor — no `mcp.json` at the plugin root. Cursor users instead copy `docs/cursor.mcp.example.json` into `~/.cursor/mcp.json` (user-scoped) or `.cursor/mcp.json` (project-scoped); the template passes `--project ${workspaceFolder}` in `args` to make the workspace explicit. Since `host-wiring-parity.adr.md`, the project-scoped copy is written automatically by `/archcore:init` (and `archcore init --agent cursor`); the manual copy remains the fallback for sessions where wiring has not run yet. Full rationale and three-layer defense in `cursor-mcp-architecture.adr.md`. +**Cursor and GitHub Copilot CLI are the two exceptions, for the same reason: each launches a plugin's MCP child outside the user's project.** + +Cursor spawns plugin MCPs from the plugin install directory and its stdio schema has no `cwd` field, so the plugin ships no `mcp.json` at the plugin root. Cursor users get `.cursor/mcp.json` written by `/archcore:init` (or `archcore init --agent cursor`), carrying `--project ${workspaceFolder}`; copying `docs/cursor.mcp.example.json` by hand remains the fallback before wiring has run. See `cursor-mcp-architecture.adr.md`. + +Copilot launches a plugin's MCP child with cwd set to the plugin install root and passes it no project path at all — not even the `COPILOT_PROJECT_DIR` its hooks receive (github/copilot-cli#4234). Documents would be written into `~/.copilot/installed-plugins/` while every tool reported success, so `.plugin/plugin.json` declares no `mcpServers`; Copilot users get a workspace-root `.mcp.json` from `archcore init --agent copilot` (CLI ≥ v0.6.4). The Cursor remedy of moving the file out of the plugin root was unavailable — `.mcp.json` must stay where Claude Code finds it. See `copilot-mcp-architecture.adr.md`. -Rationale: see `remove-bundled-launcher-global-cli.idea.md`. The previous bundled launcher (download-on-first-use, checksum-verified, cached per host) is removed; CLI lifecycle now decouples cleanly from plugin releases. +Rationale for the no-bundled-CLI stance: see `remove-bundled-launcher-global-cli.idea.md`. The previous bundled launcher (download-on-first-use, checksum-verified, cached per host) is removed; CLI lifecycle now decouples cleanly from plugin releases. ### Plugin Configs @@ -177,18 +195,22 @@ 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` (→ `copilot-agents/`), `commands`, and `hooks` pointers; **no `mcpServers` field** — deliberately disabled, see `copilot-mcp-architecture.adr.md` | | `.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` | +| (none) | GitHub Copilot CLI | No catalog by design — installs by subdir spec `archcore-ai/plugin:plugins/archcore` | | `.mcp.json` | Claude Code | 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) | -| `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[]` under both MCP namings); MD original used by Claude Code/Cursor | +| `hooks/copilot.hooks.json` | GitHub Copilot CLI | Hook event config (camelCase + native mutation matchers + project-root cwd) | +| `commands/*.md` | Codex CLI, GitHub Copilot 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"`) | +| `agents/archcore-auditor.toml` | Codex CLI | Codex TOML subagent (`sandbox_mode = "read-only"` + `disabled_tools[]` under all MCP namings) | +| `copilot-agents/*.agent.md` | GitHub Copilot CLI | Byte-identical copies of the MD agents under the extension Copilot's loader requires | | `rules/archcore-context.mdc` | Cursor | Always-apply context rule | | `rules/archcore-files.mdc` | Cursor | .archcore/ glob-triggered MCP-only rule | @@ -206,4 +228,4 @@ Component manifests, hooks, and MCP configs are plugin-root-relative (under `plu /archcore:help — system guide ``` -Total visible in `/` menu: **7 commands**. Every Archcore document type is reachable via these skills or directly through `mcp__archcore__create_document(type=)`. Codex CLI surfaces the same 7 entries via the matching `commands/*.md` wrappers. \ No newline at end of file +Total visible in `/` menu: **7 commands**. Every Archcore document type is reachable via these skills or directly through `mcp__archcore__create_document(type=)`. Codex CLI surfaces the same 7 entries via the matching `commands/*.md` wrappers; Copilot CLI surfaces them from the skills, with the wrappers loaded behind them. diff --git a/.archcore/plugin/copilot-adapter-design.adr.md b/.archcore/plugin/copilot-adapter-design.adr.md index 086ca6e..fc4fefb 100644 --- a/.archcore/plugin/copilot-adapter-design.adr.md +++ b/.archcore/plugin/copilot-adapter-design.adr.md @@ -25,15 +25,20 @@ Ship the GitHub Copilot host adapter as **native adapter files** — a dedicated ## Consequences -- Every contract the adapter uses is in the official reference (hooks config schema, stdin JSON payload, `permissionDecision` output, `timeoutSec`, subdir install spec) — release verification is a smoke test plus the standard three-probe protocol, not a research spike. [expected] -- Adapter cost stays within the per-host budget of `multi-host-plugin-architecture.adr`: one ~15-line manifest, one ~40-line hooks config, one output branch in `bin/` guard scripts. -- A fourth hooks config joins the sync surface: the host-coverage-matrix structure test MUST gain a `copilot` row so a mutation-tool gap fails CI. -- Deny semantics diverge per host: Copilot ignores the Claude "exit 2 = block" idiom (exit 2 is a warning there), so guard scripts branch on `ARCHCORE_HOST`; non-2 non-zero exits deny fail-closed, but a hook timeout (30 s default) fails **open** — a documented residual risk on this host. +- Adapter cost stays within the per-host budget of `multi-host-plugin-architecture.adr`: one ~14-line manifest, one ~40-line hooks config, one output branch in `bin/` guard scripts. +- A fourth hooks config joins the sync surface: the host-coverage-matrix structure test gained its `copilot` row, so a mutation-tool gap fails CI. [done] +- Deny semantics diverge per host: Copilot ignores the Claude "exit 2 = block" idiom (exit 2 is a warning there), so guard scripts branch on `ARCHCORE_HOST`; non-2 non-zero exits deny fail-closed, but a hook timeout fails **open**. `test/unit/hook-latency.bats` keeps both PreToolUse guards far enough inside the 1 s budget that the timeout path stays out of reach on realistic knowledge bases. - VS Code and cloud-agent users get no plugin in this release; the limitation is documented rather than papered over. A repo-level `.github/hooks/*.json` template for cloud agents is a separable follow-up. -- One naming detail rides on the smoke test: whether plain `NAME.md` agents load from a plugin `agents/` dir (plugin docs show only `*.agent.md`); fallback is shipping `*.agent.md` copies of the two agents. + +**Two premises did not survive contact with the release.** Both were recorded above as things the smoke test would confirm; documentation settled them first, in the other direction. + +- **Agent file naming.** Plain `NAME.md` does not load. Copilot's plugin reference defines the `agents` field as "Path(s) to agent directories (`.agent.md` files)", and the how-to and concepts pages agree. The fallback named here — shipping `*.agent.md` copies — is now the design, with one addition: the copies live in `copilot-agents/`, not beside the originals, because `.agent.md` still matches the `*.md` glob Claude Code and Cursor use and a sibling copy would give both hosts two files declaring the same `name:`. Parity is held by `cmp` in `test/structure/agents.bats`. +- **Plugin-shipped MCP.** The Context above lists it among the documented contracts. It is documented and it does not work: Copilot launches a plugin's MCP child in the plugin install directory with no project path (github/copilot-cli#4234), so writes land in `~/.copilot/installed-plugins/` while every tool reports success. `copilot-mcp-architecture.adr` supersedes that part of this decision — the manifest ships no `mcpServers`, and Copilot users register the server per project. + +A third premise was confirmed rather than broken: `commands` is a real manifest field with no default path, so the explicit pointer this adapter ships is what makes the seven `/archcore:*` wrappers exist at all. ## Superseded when - Copilot documents self-serve plugin install for VS Code agent mode — revisit the CLI-only scope. - Copilot publishes a complete Claude-compatibility reference for plugin manifests and hooks — revisit design A to drop the two Copilot-specific config files. -- Cloud agent documents that `enabledPlugins` plugins contribute hooks/MCP inside the sandbox — revisit cloud-agent support. \ No newline at end of file +- Cloud agent documents that `enabledPlugins` plugins contribute hooks inside the sandbox — revisit cloud-agent support. (The MCP half of that question is now governed by `copilot-mcp-architecture.adr` and github/copilot-cli#4234.) diff --git a/.archcore/plugin/copilot-mcp-architecture.adr.md b/.archcore/plugin/copilot-mcp-architecture.adr.md new file mode 100644 index 0000000..44249cf --- /dev/null +++ b/.archcore/plugin/copilot-mcp-architecture.adr.md @@ -0,0 +1,67 @@ +--- +title: "Copilot MCP Architecture — Project-Level Install Only, No Plugin-Shipped MCP" +status: accepted +tags: + - "architecture" + - "copilot" + - "multi-host" + - "plugin" +--- + +## Context + +While preparing the GitHub Copilot CLI adapter for release in July 2026, plugin-shipped MCP turned out to be unusable on that host — for the same class of reason that produced `cursor-mcp-architecture.adr`, but with a sharper failure mode. + +Two facts produced the decision: + +1. **Copilot launches a plugin's MCP children in the plugin install directory and tells them nothing about the project.** [github/copilot-cli#4234](https://github.com/github/copilot-cli/issues/4234) (opened 2026-07-23, open, no maintainer response): "an MCP server loaded from an installed plugin is launched with its working directory set to the plugin installation root, and the child process receives no project/workspace path." The child does not even receive `COPILOT_PROJECT_DIR`, which plugin *hooks* do get — so there is no recovery path from inside the process. + + For Archcore this is worse than a wrong answer. `archcore mcp` resolves the project root from cwd; launched from `~/.copilot/installed-plugins/…` it finds no `.archcore/` there and reports an uninitialized project, at which point `init_project` creates `.archcore/` **inside the plugin cache**. Every tool returns success. The documents land where no git repository will ever see them, and the user's next session — served by a different cache entry or a reinstalled plugin — finds an empty knowledge base. + +2. **GitHub's own documentation does not agree on whether removing the manifest key is even sufficient.** The [plugin reference](https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-plugin-reference) gives a default path only to `agents/` and `skills/`; `mcpServers` has none, which makes the manifest key the only source. The [concepts page](https://docs.github.com/en/copilot/concepts/agents/about-plugins) instead describes "a `.mcp.json` file in the plugin root" as where MCP configurations live, phrased as a discovery rule. Documentation cannot settle this; a live probe can. The decision below is deliberately robust to either answer. + +The Cursor remedy does not transfer. There we moved the offending file out of the plugin root (`docs/cursor.mcp.example.json`). Here we cannot: `.mcp.json` must stay at the plugin root because Claude Code discovers it there with no manifest key at all. + +## Decision + +The plugin does **not** ship MCP for Copilot CLI. `.plugin/plugin.json` omits `mcpServers`. Copilot users get the MCP server from their own project's `.mcp.json` — written by `archcore init --agent copilot` (CLI >= v0.6.4) or `/archcore:init` — or from `~/.copilot/mcp-config.json` at the user level. + +This is the correct surface regardless of the defect: Copilot CLI discovers `.mcp.json` by walking from the working directory up to the git root, so a repo-root file covers monorepo layouts, and a project-registered server is launched from the project, which makes cwd right by construction rather than by luck. + +**Layer 1 — the manifest omits the key**, pinned by `test/structure/copilot-plugin.bats` with the issue number in the assertion message. Without a test the key reads like an oversight and comes back on the next cleanup. + +**Layer 2 — host wiring is the documented path.** `archcore init --agent copilot` writes `.mcp.json`, `.github/hooks/archcore.json` and the `AGENTS.md` managed block; `/archcore:init` writes the same set (`host-wiring-parity.adr`). On Copilot this stops being an optimization and becomes the only route to MCP tools. + +**Layer 3 — the runtime guard, once it covers this host.** `resolveProjectRoot` in the CLI already refuses to treat a directory inside a plugin install cache as a project root, and `bin/session-start` applies the same rule to hooks. The CLI's fragment list currently names `.cursor/plugins/`, `.claude/plugins/`, `.codex/plugins/` and `plugins/cache/` — **not** `~/.copilot/installed-plugins/`. Until that fragment is added, Layer 3 does not protect Copilot, and Layers 1-2 carry the decision alone. + +## Alternatives + +**A. Ship `mcpServers` and pass the project path in `args`.** +The canonical way to make a plugin MCP project-aware. Rejected: #4234 states the child receives no project path, and Copilot documents no interpolation token usable inside plugin-MCP `args` — there is nothing to put after `--project`. + +**B. Move `.mcp.json` out of the plugin root, as we did for Cursor.** +Rejected: impossible. Claude Code discovers plugin-root `.mcp.json` with no manifest key; moving it would break the primary host to fix the newest one. + +**C. Ship a wrapper that re-resolves the working directory before exec'ing `archcore mcp`.** +Rejected twice over. It re-creates `bundled-cli-launcher.adr`, removed in v0.4.0 for eight categories of bugs; and the input such a wrapper needs — the project path — is precisely what #4234 says the host does not pass. + +**D. Wait for upstream to fix #4234.** +Rejected as the whole plan. The issue is open with no maintainer response, and the adapter ships now. It is retained as the revisit trigger below. + +## Consequences + +**Positive** + +- Copilot cannot write a user's documents into its own plugin cache through our MCP. +- The install story is the one Cursor users already have: one precedent, one explanation, one place in the docs. +- Claude Code and Codex are untouched — `.mcp.json` and `.codex.mcp.json` stay exactly where they are. + +**Negative** + +- **The plugin stops being self-contained on Copilot.** Installing it yields skills, commands, agents and hooks, but no MCP tools until host wiring runs. The init skill must therefore always emit the Host wiring line for copilot, which pins that path to CLI >= v0.6.4. +- Layer 3 does not cover Copilot until `.copilot/installed-plugins/` joins the CLI's plugin-cache fragments. Until it does, a user who hand-registers the server from inside the plugin directory gets the silent-write behavior back. +- If upstream closes #4234 — by passing `COPILOT_PROJECT_DIR` to MCP children or launching them from the project directory — the manifest key can be revisited, but the runtime guard stays as belt-and-suspenders. + +**Neutral** + +- Whether omitting the key is *sufficient* is unverified: if the concepts page is right and a plugin-root `.mcp.json` is auto-discovered, the key was never the only source. Probe P4 (`copilot mcp list` against an installed plugin) settles it, and Layer 3 is what makes the answer non-critical either way. diff --git a/.archcore/plugin/hooks-validation-system.spec.md b/.archcore/plugin/hooks-validation-system.spec.md index 1f187ae..f163c77 100644 --- a/.archcore/plugin/hooks-validation-system.spec.md +++ b/.archcore/plugin/hooks-validation-system.spec.md @@ -13,15 +13,19 @@ Define the contract for the hook-based validation, freshness detection, and cont ## Scope -This specification covers all hook entries in `hooks/hooks.json`: the SessionStart hook (via `bin/session-start` wrapper with staleness check), two PreToolUse hooks on `Write|Edit` (blocking direct writes to `.archcore/*.md` and injecting context for source edits), the PostToolUse hook for validation after MCP document operations, the PostToolUse hook for cascade detection after document updates, and the PostToolUse hook for precision checks. It does not cover the MCP server itself, the Archcore CLI lifecycle (the CLI is installed by the user per https://docs.archcore.ai/cli/install/ and resolved via PATH), or the agent's tool restrictions. +This specification covers all hook entries the plugin ships, across every host config (`hooks/hooks.json`, `hooks/cursor.hooks.json`, `hooks/codex.hooks.json`, `hooks/copilot.hooks.json`): the SessionStart hook (via `bin/session-start` wrapper with staleness check), two PreToolUse hooks on source mutations (blocking direct writes to `.archcore/*.md` and injecting context for source edits), the PostToolUse hook for validation after MCP document operations, the PostToolUse hook for cascade detection after document updates, and the PostToolUse hook for precision checks. + +Claude Code's `hooks/hooks.json` is used below as the reference shape because it is the most explicit; per-host divergences are called out where they change behavior rather than only syntax. The canonical event/matcher matrix lives in `plugin-architecture.spec.md`; blocking-semantics translation per host lives in `host-adapter-contract.spec.md`. + +It does not cover the MCP server itself, the Archcore CLI lifecycle (the CLI is installed by the user per https://docs.archcore.ai/cli/install/ and resolved via PATH), or the agent's tool restrictions. ## Authority -This specification is the authoritative reference for the plugin's hook configuration. The Always Use MCP Tools ADR provides the architectural rationale for the blocking behavior. The Actualize System ADR and Specification provide the rationale and contract for staleness detection (Layers 1 and 2). The Pre-Code Context Injection idea and its implementation plan provide the rationale for the source-edit context-injection hook. The Host-Wiring Parity ADR governs the dual-naming matcher requirement and the SessionStart dedup/advisory additions. +This specification is the authoritative reference for the plugin's hook configuration. The Always Use MCP Tools ADR provides the architectural rationale for the blocking behavior. The Actualize System ADR and Specification provide the rationale and contract for staleness detection (Layers 1 and 2). The Pre-Code Context Injection idea and its implementation plan provide the rationale for the source-edit context-injection hook. The Host-Wiring Parity ADR governs the dual-naming matcher requirement and the SessionStart dedup/advisory additions. `host-probe-protocol.spec.md` governs how the behaviors specified here are verified on a live host. ## Subject -The hooks system consists of event handlers registered in `hooks/hooks.json` that respond to Claude Code lifecycle events. Three event types with six hook entries enforce quality, the MCP-only principle, documentation freshness, source-edit context alignment, and precision after document mutations. +The hooks system consists of event handlers registered in each host's hooks config that respond to that host's lifecycle events. Three event types with six hook entries enforce quality, the MCP-only principle, documentation freshness, source-edit context alignment, and precision after document mutations. The event *names* differ per host (PascalCase on Claude Code and Codex, camelCase on Cursor and Copilot) and so do the entry shapes; the six behaviors do not. ## Contract Surface @@ -71,48 +75,52 @@ The hooks system consists of event handlers registered in `hooks/hooks.json` tha } ``` +**Per-host shape divergence.** Cursor and Codex differ from the above only in event-name casing, plugin-root variable, and matcher contents. Copilot differs structurally: entries are flat objects (no nested `hooks[]` group), the command lives under `bash` rather than `command`, the budget under `timeoutSec` rather than `timeout`, each entry sets `cwd: "."` so the hook runs from the user's project and `env.ARCHCORE_HOST=copilot` so detection is deterministic, and its `postToolUse` entries carry **no matcher at all**. A config or a test written by copying another host's and swapping names will load without error and do nothing — which is why `test/structure/hooks.bats` extracts commands through a `.command // .bash` union and fails loudly when the extraction is empty. + **Dual tool naming (mandatory).** Every archcore tool in a PostToolUse matcher is listed under BOTH namings: `mcp__archcore__X` (the name a project-level `.mcp.json` server yields) and `mcp__plugin_archcore_archcore__X` (the name Claude Code gives tools from a plugin-bundled MCP server — `mcp__plugin____*`). Claude Code matchers without regex metacharacters are exact matches, so a single-naming matcher silently never fires in one of the two setups. Guarded by `test/structure/hooks.bats`; rationale in `host-wiring-parity.adr.md`. +Matchers and scripts solve different halves of the same problem, and the split matters. A matcher decides *whether the script runs at all*; the script decides *what to do*. `bin/lib/normalize-stdin.sh` therefore folds all three namings — including Copilot's flat `archcore-`, where the host joins server and tool with a hyphen — into the canonical `mcp__archcore__*` before any guard inspects a tool name. Without that fold a guard fires and then silently falls through its own filter, which is the worst of both: cost paid, protection absent. On Copilot, where `postToolUse` takes no matcher, that filtering *is* the whole selection mechanism. + Historical note: a prior revision included a PostToolUse `Write|Edit` matcher invoking `validate-archcore` as defense-in-depth. The hook was dead in practice — PreToolUse blocks all Write/Edit to `.archcore/*.md` before they reach PostToolUse (PostToolUse fires only on success per Claude Code hooks semantics), and `.archcore/settings.json` / `.archcore/.sync-state.json` are allowlisted, so `validate-archcore` never had an edge case to handle through that path. It was removed to eliminate a per-Write/Edit shell fork across the entire repository. The MCP matcher below remains the single validation entry point. -The two PreToolUse entries on `Write|Edit` are deliberately coupled: `check-archcore-write` short-circuits on `.archcore/*.md` with exit 2 (blocks the write); `check-code-alignment` short-circuits on everything INSIDE `.archcore/` with exit 0 (silent). On any source path only the alignment hook does real work. The order matters for fast exit on blocks but does not affect correctness — exit codes from different hooks are combined per Claude Code semantics (any exit 2 blocks). +The two PreToolUse entries on `Write|Edit` are deliberately coupled: `check-archcore-write` short-circuits on `.archcore/*.md` and denies; `check-code-alignment` short-circuits on everything INSIDE `.archcore/` with exit 0 (silent). On any source path only the alignment hook does real work. The order matters for fast exit on blocks but does not affect correctness — the two run as independent entries with independent budgets on every host. ### Hook 1: SessionStart (Context Loading + Staleness Check) -**Event**: SessionStart (fires when a session begins or resumes) +**Event**: SessionStart / `sessionStart` (fires when a session begins or resumes) **Matcher**: empty (matches all session sources: startup, resume, clear, compact) -**Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/session-start` +**Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/session-start` (per-host plugin-root variable) **Behavior**: pipeline of phases: 0. **Plugin-install-dir guard.** Exit 0 silently when `$PWD` contains an install-cache path fragment (`.cursor/plugins/`, `.claude/plugins/`, `.codex/plugins/`, `plugins/cache/`) or when a bounded upward walk finds a `.cursor-plugin/`, `.claude-plugin/`, `.codex-plugin/`, or `.plugin/` manifest — a cwd misrouted into a plugin install (at any depth) must never surface the plugin's own bundled files as the user's knowledge base (`cursor-mcp-architecture.adr.md`, extended per `host-wiring-parity.adr.md`). -1. **CLI availability check.** If `archcore` is not on PATH, emit an `additionalContext` install message pointing at https://docs.archcore.ai/cli/install/ and exit 0. No further phases run. Installing the CLI mid-session does NOT reconnect a Claude Code MCP server that failed to register at session start — users must restart the host after a fresh install. -2. **Project check.** If `.archcore/` does not exist, emit `additionalContext` instructing the agent to call `mcp__archcore__init_project` on first Archcore operation, then exit 0. -3. **Context loading + staleness.** If `.archcore/` exists, pipe stdin into `archcore hooks session-start`; swallow any non-zero exit so SessionStart remains non-blocking. The CLI-side handler dedupes duplicate SessionStart emissions per `session_id`+`source` (Cursor: `conversation_id`) via short-window XDG-state stamps, fail-open — so a project-level hook installed by `archcore init --agent` coexisting with this plugin hook emits context once, for any plugin/CLI version combination. Then call `bin/check-staleness` to detect code-doc drift via git, emit findings via the info helper. +1. **CLI availability check.** If `archcore` is not on PATH, emit an install message pointing at https://docs.archcore.ai/cli/install/ and exit 0. No further phases run. Installing the CLI mid-session does NOT reconnect a Claude Code MCP server that failed to register at session start — users must restart the host after a fresh install. +2. **Project check.** If `.archcore/` does not exist, emit context instructing the agent to call `mcp__archcore__init_project` on first Archcore operation, then exit 0. +3. **Context loading + staleness.** If `.archcore/` exists, pipe stdin into `archcore hooks session-start`; swallow any non-zero exit so SessionStart remains non-blocking. The CLI-side handler dedupes duplicate SessionStart emissions per `session_id`+`source` (Cursor: `conversation_id`) via short-window XDG-state stamps, fail-open — so a project-level hook installed by `archcore init --agent` coexisting with this plugin hook emits context once, for any plugin/CLI version combination. It also emits the response in that host's shape (CLI ≥ v0.6.4 knows Copilot's bare `additionalContext`). Then call `bin/check-staleness` to detect code-doc drift via git, emit findings via the info helper. 4. **Outdated-CLI advisory.** Run `archcore update --check` (24h-cached, ~500ms-bounded, silent on any failure, exit 0 always; an older CLI without the flag degrades silently). When it reports a newer version and the advisory's own 24h rate-limit stamp is due, emit a one-line plain-text nudge naming `archcore update`. Exit 0. Staleness and the advisory are additive — if either fails or produces no output, the preceding phases are unaffected. -**Input**: JSON on stdin with `session_id`, `cwd`, `hook_event_name` -**Output**: Structured `hookSpecificOutput.additionalContext` (Claude Code / Copilot) or plain text (other hosts) +**Input**: JSON on stdin with `session_id`, `cwd`, `hook_event_name` (host-specific field names, normalized by `bin/lib/normalize-stdin.sh`) +**Output**: the host's context envelope — `hookSpecificOutput.additionalContext` (Claude Code, Codex), top-level `additionalContext` (Copilot), `additional_context` (Cursor), plain text (OpenCode) ### Hook 2: PreToolUse — Block Direct Writes -**Event**: PreToolUse (fires before a tool call executes) -**Matcher**: `Write|Edit` (only intercepts Write and Edit tool calls) +**Event**: PreToolUse / `preToolUse` (fires before a tool call executes) +**Matcher**: the host's file-mutation tools — `Write|Edit` (Claude Code), `Write` (Cursor, which exposes no Edit tool), `Write|Edit|apply_patch` (Codex), `create|edit|str_replace_editor|apply_patch` (Copilot) **Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/check-archcore-write` **Timeout**: 1 second -**Input**: JSON on stdin containing the tool call details including `tool_input.file_path` +**Input**: JSON on stdin containing the tool call details including the target path **Behavior**: -1. Extract `file_path` from the tool input (stdin JSON) +1. Extract the target path from the tool input (stdin JSON, via the normalizer — on Copilot the path lives inside an escaped JSON string under `toolArgs`) 2. Check if the path matches `.archcore/**/*.md` (document files) 3. If NO match: exit 0 with empty output (allow the operation) -4. If MATCH: write blocking reason to **stderr** and **exit 2** +4. If MATCH: deny, using the mechanism the host honors -Per Claude Code documentation, exit code 2 is a blocking error — stderr is sent directly to the model as feedback, and the tool call is blocked. +**Deny mechanism is host-specific, and getting it wrong fails open.** On Claude Code, Codex and Cursor, exit code 2 is a blocking error — stderr goes to the model as feedback and the tool call is blocked. **On Copilot exit 2 is only a warning**: the write proceeds. There a deny must be written to stdout as `{"permissionDecision":"deny","permissionDecisionReason":"…"}` with exit 0. (Copilot does treat other non-zero exits as fail-closed denials, but with no reason text reaching the model, so the JSON form is the contract.) The branch lives in the shared script keyed on `ARCHCORE_HOST`; see `host-adapter-contract.spec.md` for the full translation table. -**Stderr message when blocking**: +**Reason message when blocking**: ``` Direct writes to .archcore/ documents are not allowed. Use Archcore MCP tools instead: @@ -129,11 +137,11 @@ This ensures validation, templates, and the sync manifest stay consistent. ### Hook 3: PreToolUse — Inject Context for Source Edits -**Event**: PreToolUse (fires before a tool call executes) -**Matcher**: `Write|Edit` (shared with Hook 2) +**Event**: PreToolUse / `preToolUse` +**Matcher**: same as Hook 2, per host **Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/check-code-alignment` **Timeout**: 1 second -**Input**: JSON on stdin containing the tool call details including `tool_input.file_path` +**Input**: JSON on stdin containing the tool call details including the target path **Behavior**: @@ -142,7 +150,7 @@ This ensures validation, templates, and the sync manifest stay consistent. 3. Normalize to cwd-relative; exit 0 if path is absolute outside `$CWD`. 4. Enforce source-root filter: path must start with a configured source root. Default set: `src lib app pkg cmd internal apps packages modules components`. Override via `.archcore/settings.json` → `codeAlignment.sourceRoots` (JSON array). Exit 0 if not matched. 5. Generate candidate tokens — directory prefixes of the file path, longest first (capped at 5 levels). -6. Scan `.archcore/**/*.md` with `grep -rlF ` per token in longest-first order. Score each matched document by specificity (length of the longest matching token) combined with type priority: `rule=5, cpat=4, adr=3, spec=2, guide=1`. Only these five types are eligible — other types (prd, idea, plan, rfc, doc, task-type, etc.) are ignored as not enforceable or too high-level for line-of-code context. +6. Scan `.archcore/**/*.md` with `grep -rlF ` per token in longest-first order — one grep per token, not per match. Score each matched document by specificity (length of the longest matching token) combined with type priority: `rule=5, cpat=4, adr=3, spec=2, guide=1`. Only these five types are eligible — other types (prd, idea, plan, rfc, doc, task-type, etc.) are ignored as not enforceable or too high-level for line-of-code context. 7. Rank desc, take top 3. 8. Render a compact block: ``` @@ -151,47 +159,53 @@ This ensures validation, templates, and the sync manifest stay consistent. ... ``` Output capped at 2 KB. -9. Emit as PreToolUse `additionalContext`: - - Claude Code / Copilot: `{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"..."}}` +9. Emit the host's context envelope: + - Claude Code / Codex: `{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"..."}}` + - Copilot: `{"additionalContext":"..."}` — top-level, no wrapper; Claude's shape is ignored wholesale there - Cursor: `{"additional_context":"..."}` (may be ignored by current Cursor — graceful degradation, documented limitation) **Non-blocking by design**: exit code is always 0. Any error in the pipeline (missing tools, malformed JSON, empty matches) results in a silent pass. Injection is strictly additive and must never prevent a write. +**That additivity is exactly why its cost is a correctness concern.** A timeout here raises nothing: the write proceeds and no context arrives, with nothing in any log. The hook once cost roughly 6 ms per *matching* document — two process spawns each, per token — so a knowledge base where ~170 documents mentioned a common source root exceeded the 1 s budget outright, and push-mode stopped working on precisely the repositories with the most context to give. Deduplication now happens in a single pass, so process spawns scale with the number of tokens rather than the number of matches. `test/unit/hook-latency.bats` pins both the absolute budget and the independence from match count. + **Escape hatch**: set environment variable `ARCHCORE_DISABLE_INJECTION=1` to disable injection globally for a session. -**Relationship to Hook 2**: both hooks fire on the same matcher. Hook 2 handles `.archcore/*.md` paths (blocks). Hook 3 handles source paths (injects). Their active path sets are disjoint by construction. +**Relationship to Hook 2**: both hooks fire on the same matcher. Hook 2 handles `.archcore/*.md` paths (blocks). Hook 3 handles source paths (injects). Their active path sets are disjoint by construction. They are separate hook entries with separate budgets, so a slow Hook 3 cannot consume Hook 2's. -#### Sub-agent tool invocations (Task-dispatched) +#### Sub-agent tool invocations (delegated) -PreToolUse hooks in Claude Code fire at the tool-execution boundary, not at the session boundary. Any Write or Edit tool call matches the `Write|Edit` matcher regardless of whether the call originates from the main conversation or from a sub-agent dispatched via the Task tool. Hook 2 and Hook 3 therefore cover Task-dispatched Write/Edit identically to main-session Write/Edit. Input stdin carries `tool_name` and `tool_input.file_path` in the same shape; the host does not annotate sub-agent origin in a way that the hooks need to consume or branch on. +PreToolUse hooks fire at the tool-execution boundary, not at the session boundary. Any file-mutation tool call matches the host's matcher regardless of whether the call originates from the main conversation or from a delegated sub-agent. Hook 2 and Hook 3 therefore cover delegated Write/Edit identically to main-session Write/Edit. Input stdin carries the tool name and path in the same shape; the host does not annotate sub-agent origin in a way that the hooks need to consume or branch on. Scope clarifications: -- **Archcore's own sub-agents** (`archcore-assistant`, `archcore-auditor`) do NOT have `Write` or `Edit` in their tools allowlist (see `agent-system.spec.md` Tool Access Matrix). They cannot trigger Hooks 2 or 3 by definition. The sub-agent coverage discussion concerns general-purpose and third-party Task agents dispatched by the user for code work. -- **Claude Code**: hook coverage for Task-dispatched Write/Edit holds by the host's PreToolUse contract. An empirical probe is recommended on major host releases but not required for the specification to stand. +- **Archcore's own sub-agents** (`archcore-assistant`, `archcore-auditor`) do NOT have `Write` or `Edit` in their tools allowlist (see `agent-system.spec.md` Tool Access Matrix). They cannot trigger Hooks 2 or 3 by definition. The sub-agent coverage discussion concerns general-purpose and third-party agents dispatched by the user for code work. +- **Claude Code**: hook coverage for delegated Write/Edit holds by the host's PreToolUse contract. +- **Copilot**: has a delegation surface (`subagentStart` / `subagentStop`), so the same question is answerable there. - **Cursor**: the PreToolUse matcher in `cursor.hooks.json` is `Write` only, not `Write|Edit` — a pre-existing multi-host asymmetry, independent of the sub-agent question. Sub-agent-originated Edit calls on Cursor go unhooked for the same reason main-session Edit calls do. +This is probe A-d in `host-probe-protocol.spec.md`, which is where the per-host result is recorded. A specification claim about delegated coverage is not evidence; the record is. + ### Hook 4: PostToolUse — Validate After MCP Document Operations -**Event**: PostToolUse (fires after a tool call succeeds) -**Matcher**: the five document-mutation tools, each under both namings (`mcp__archcore__X|mcp__plugin_archcore_archcore__X` for `create_document`, `update_document`, `remove_document`, `add_relation`, `remove_relation`) +**Event**: PostToolUse / `postToolUse` (fires after a tool call succeeds) +**Matcher**: the five document-mutation tools, each under both namings (`mcp__archcore__X|mcp__plugin_archcore_archcore__X` for `create_document`, `update_document`, `remove_document`, `add_relation`, `remove_relation`). Copilot registers no matcher; the script filters on the normalized tool name instead. **Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/validate-archcore` **Timeout**: 3 seconds **Input**: JSON on stdin containing the completed MCP tool call details **Behavior**: -1. Extract `tool_name` from stdin JSON +1. Extract `tool_name` from stdin JSON (normalized to the canonical naming) 2. Detect the archcore MCP prefix — run `archcore doctor` directly (resolved via PATH, wrapped in `timeout 2` and `|| true`) 3. If validation passes: exit 0 with empty output -4. If validation fails: exit 0 with JSON output containing validation context +4. If validation fails: exit 0 with the host's context envelope containing validation context This is the sole validation hook. Because PreToolUse blocks all direct Write/Edit to `.archcore/*.md` and MCP tools are the supported interface for document operations, this single matcher fires after every document mutation that can actually touch the knowledge base. ### Hook 5: PostToolUse — Cascade Detection After Document Updates -**Event**: PostToolUse (fires after a tool call succeeds) -**Matcher**: `mcp__archcore__update_document|mcp__plugin_archcore_archcore__update_document` +**Event**: PostToolUse / `postToolUse` +**Matcher**: `mcp__archcore__update_document|mcp__plugin_archcore_archcore__update_document` (Copilot: none — script-side filtering) **Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/check-cascade` **Timeout**: 3 seconds **Input**: JSON on stdin containing the completed `update_document` tool call details @@ -201,7 +215,7 @@ This is the sole validation hook. Because PreToolUse blocks all direct Write/Edi 1. Extract updated document path from `tool_input.path` in stdin JSON 2. Query relation graph for documents where the updated document is the **target** of `implements`, `depends_on`, or `extends` relations 3. If no such relations found: exit 0 with empty output (no cascade) -4. If cascade found: exit 0 with JSON output containing affected document list +4. If cascade found: exit 0 with the host's context envelope containing the affected document list This hook fires **in addition to** Hook 4 (validation). Both hooks fire independently on `update_document` — Hook 4 validates structural integrity, Hook 5 detects cascade staleness. Neither depends on the other. @@ -211,12 +225,12 @@ This hook fires **in addition to** Hook 4 (validation). Both hooks fire independ ### Hook 6: PostToolUse — Precision Check -**Event**: PostToolUse -**Matcher**: `create_document` and `update_document`, each under both namings +**Event**: PostToolUse / `postToolUse` +**Matcher**: `create_document` and `update_document`, each under both namings (Copilot: none — script-side filtering) **Handler**: `${CLAUDE_PLUGIN_ROOT}/bin/check-precision` **Timeout**: 3 seconds -Phase 1 of the Precision Initiative (see `precision-over-coverage.adr`). Reads the resulting file from disk and runs four checks: forbidden vagueness lexicon, mandatory sections by type (adr/rule/spec/guide/rfc), frontmatter title+status presence, body length ≥ 200 chars. Emits soft warnings via `additionalContext`. Always exits 0; never blocks. +Phase 1 of the Precision Initiative (see `precision-over-coverage.adr`). Reads the resulting file from disk and runs four checks: forbidden vagueness lexicon, mandatory sections by type (adr/rule/spec/guide/rfc), frontmatter title+status presence, body length ≥ 200 chars. Emits soft warnings as additional context. Always exits 0; never blocks. ### PostToolUse Output Formats @@ -253,7 +267,17 @@ Phase 1 of the Precision Initiative (see `precision-over-coverage.adr`). Reads t } ``` -Cursor host uses the flat `{"additional_context": "..."}` shape. +### Output envelope per host + +The scripts never build these by hand; the output helpers in `bin/lib/normalize-stdin.sh` select the shape from `ARCHCORE_HOST`, which is why a new host costs one branch in one file. + +| Host | Context envelope | Pre-mutation deny | +|---|---|---| +| Claude Code | `hookSpecificOutput.additionalContext` | exit 2 + stderr | +| Codex CLI | `hookSpecificOutput.additionalContext` | exit 2 + stderr | +| Cursor | `additional_context` (flat) | exit 2 + stderr | +| GitHub Copilot CLI | `additionalContext` (flat, top level) | stdout `{"permissionDecision":"deny",…}`, exit 0 | +| OpenCode | plain message, no JSON | bridge throws `Error(reason)` | ### Exit Code Semantics @@ -261,11 +285,13 @@ Cursor host uses the flat `{"additional_context": "..."}` shape. |------|--------|--------| | SessionStart | Always (output = install msg / init msg / context + staleness + advisory) | N/A | | PreToolUse block (Hook 2, allow) | Empty output, operation proceeds | N/A | -| PreToolUse block (Hook 2, block) | N/A | stderr → model feedback, operation blocked | -| PreToolUse inject (Hook 3) | Always (no match → empty; match → `additionalContext`) | N/A | -| PostToolUse validation (Hook 4) | Always (clean → empty; issues → `additionalContext`) | N/A | -| PostToolUse cascade (Hook 5) | Always (no cascade → empty; cascade → `additionalContext`) | N/A | -| PostToolUse precision (Hook 6) | Always (clean → empty; warnings → `additionalContext`) | N/A | +| PreToolUse block (Hook 2, block) | Copilot only: deny JSON on stdout | Claude Code / Codex / Cursor: stderr → model feedback, operation blocked | +| PreToolUse inject (Hook 3) | Always (no match → empty; match → context) | N/A | +| PostToolUse validation (Hook 4) | Always (clean → empty; issues → context) | N/A | +| PostToolUse cascade (Hook 5) | Always (no cascade → empty; cascade → context) | N/A | +| PostToolUse precision (Hook 6) | Always (clean → empty; warnings → context) | N/A | + +The exit-2 column is Claude Code's convention, adopted by Codex and Cursor. Copilot is the one host where a non-zero exit is not the deny channel — see Hook 2. ### bin/ Scripts @@ -282,16 +308,16 @@ Requirements: - Exits 0 in all cases - Exits silently when run from inside a plugin install — cache path fragments in `$PWD` or a manifest found by the bounded upward walk - When `archcore` is not on PATH: emits an install message pointing at https://docs.archcore.ai/cli/install/ and exits 0 -- When `.archcore/` is absent: emits `additionalContext` pointing at `mcp__archcore__init_project` +- When `.archcore/` is absent: emits context pointing at `mcp__archcore__init_project` - Otherwise: invokes `archcore hooks session-start` and discards any non-zero exit, then calls `bin/check-staleness`, then runs the `archcore update --check`-backed advisory (own 24h rate-limit stamp; silent when the CLI is current, the flag is unsupported, or the network is unavailable) - Invokes only allowlisted CLI subcommands: `hooks`, `update` (as `update --check` only), `--version` - Degrades gracefully — never errors, just warns #### `bin/check-archcore-write` -Shell script that reads stdin JSON, extracts `tool_input.file_path`, and decides whether to block. +Shell script that reads stdin JSON, extracts the target path, and decides whether to block. -Requirements: executable; `#!/bin/sh`; reads JSON from stdin; exit 0 when allowing, exit 2 when blocking; writes blocking reason to stderr; completes within 1 second. +Requirements: executable; `#!/bin/sh`; reads JSON from stdin; exit 0 when allowing; denies through the mechanism the host honors — exit 2 + stderr on Claude Code / Codex / Cursor, stdout `{"permissionDecision":"deny","permissionDecisionReason":…}` with exit 0 on Copilot; completes within 1 second. #### `bin/check-code-alignment` @@ -307,8 +333,8 @@ Requirements: - Ranks by specificity first, type priority (`rule > cpat > adr > spec > guide`) second - Considers only `rule`, `cpat`, `adr`, `spec`, `guide` document types - Emits at most 3 matches, capped at 2 KB total output -- Completes within 1 second on a corpus of ≤ 50 `.archcore/*.md` documents -- Outputs host-normalized JSON — `hookSpecificOutput` for Claude Code / Copilot, flat `additional_context` for Cursor +- Completes within 1 second, and its cost MUST NOT scale with the number of matching documents — process spawns are bounded by the token count, not the match count +- Emits the host's context envelope (see the table above) #### `bin/validate-archcore` @@ -317,10 +343,10 @@ Shell script that reads stdin JSON, determines if validation is needed (by tool_ Requirements: - Executable; `#!/bin/sh`; reads JSON from stdin -- Fires unconditionally for archcore MCP tools (both namings); the legacy Write/Edit branch is retained as defensive code but is never reached from the current hooks config +- Fires unconditionally for archcore MCP tools under every naming, which the normalizer has already folded to one; the legacy Write/Edit branch is retained as defensive code but is never reached from the current hooks config - Invokes `archcore doctor` directly (`timeout 2 archcore doctor 2>&1`), no wrapper script - Exits 0 in all cases — silent skip when `archcore` is unavailable -- Outputs valid JSON with `hookSpecificOutput` when reporting issues, empty output when clean +- Outputs the host's context envelope when reporting issues, empty output when clean - Completes within 3 seconds ##### Test Contract @@ -342,7 +368,7 @@ Requirements: executable; `#!/bin/sh`; exit 0 in all cases; output ≤ 2 KB plai PostToolUse handler for cascade detection after `update_document`. -Requirements: executable; `#!/bin/sh`; reads JSON from stdin; exit 0 in all cases; outputs JSON `hookSpecificOutput` when cascade detected, empty otherwise; invokes `archcore` directly via PATH; completes within 3 seconds; skips gracefully if `archcore` is unavailable. +Requirements: executable; `#!/bin/sh`; reads JSON from stdin; exit 0 in all cases; outputs the host's context envelope when cascade detected, empty otherwise; invokes `archcore` directly via PATH; completes within 3 seconds; skips gracefully if `archcore` is unavailable. #### `bin/check-precision` @@ -350,7 +376,8 @@ PostToolUse handler running the precision lexicon, mandatory-sections, frontmatt ## Normative Behavior -- The PreToolUse block hook (Hook 2) MUST block all Write/Edit calls targeting `.archcore/**/*.md` files via exit code 2 with stderr message. +- The PreToolUse block hook (Hook 2) MUST block all Write/Edit calls targeting `.archcore/**/*.md` files on every host, using that host's honored deny mechanism. +- WHERE a host does not treat exit 2 as blocking, the block hook MUST emit that host's deny payload instead; a guard that reports a block the host ignores is a guard that does not block. - The PreToolUse block hook MUST NOT block writes to `.archcore/settings.json` or `.archcore/.sync-state.json`. - The PreToolUse block hook MUST NOT block writes to files outside `.archcore/`. - The PreToolUse injection hook (Hook 3) MUST exit 0 on every code path and MUST NEVER block or fail an edit. @@ -358,9 +385,12 @@ PostToolUse handler running the precision lexicon, mandatory-sections, frontmatt - The PreToolUse injection hook MUST rank matches by specificity first (longest matching directory prefix wins), type priority second, and MUST restrict eligible types to `rule`, `cpat`, `adr`, `spec`, `guide`. - The PreToolUse injection hook MUST cap output at 3 documents and 2 KB. - The PreToolUse injection hook MUST honor the `ARCHCORE_DISABLE_INJECTION=1` environment variable as an unconditional off-switch. -- The PreToolUse hooks MUST treat Task-dispatched Write/Edit tool calls identically to main-session calls — no special-casing, no skipping. +- Both PreToolUse hooks MUST complete far enough inside their 1 s budget that a timeout is unreachable on realistic knowledge bases. On Copilot a `preToolUse` timeout fails OPEN, which turns latency into a correctness property rather than a comfort one. +- The PreToolUse hooks MUST treat delegated Write/Edit tool calls identically to main-session calls — no special-casing, no skipping. - Every PostToolUse matcher MUST list each archcore tool under both namings (`mcp__archcore__X|mcp__plugin_archcore_archcore__X`) — Claude Code matchers are exact-match, and the two MCP registration paths (project `.mcp.json` vs plugin-bundled server) yield different tool names. -- The PostToolUse validation hook reports validation issues via `hookSpecificOutput.additionalContext` but does not block or revert operations. +- `bin/lib/normalize-stdin.sh` MUST fold every host's MCP tool naming to the canonical `mcp__archcore__*` before any guard script inspects a tool name, so that a guard which fires always also acts. +- WHERE a host's post-mutation event accepts no matcher, the handling script MUST filter to the same tool set the matcher would have selected. +- The PostToolUse validation hook reports validation issues as additional context but does not block or revert operations. - The PostToolUse MCP validation matcher MUST fire after all document mutation MCP tools. - The hooks config MUST NOT register a Write/Edit matcher on PostToolUse. - The PostToolUse cascade hook MUST fire only after `update_document`, not after `create_document` or `remove_document`. @@ -382,21 +412,22 @@ PostToolUse handler running the precision lexicon, mandatory-sections, frontmatt - SessionStart staleness check must complete within 3 seconds. - Hooks must work without network access in steady state. The plugin never downloads anything — CLI lifecycle is the user's responsibility via the official installer. (The update advisory's `update --check` probe is bounded to ~500ms and silent offline; it checks freshness, it never downloads a binary.) - Hooks must degrade gracefully if the Archcore CLI is missing (skip validation/cascade silently; SessionStart prints install guidance and exits 0). -- The injection hook MUST degrade gracefully for corpora larger than the Phase 1 baseline — either by completing in time at lower fidelity or by short-circuiting cleanly; it MUST NOT time out in a way that blocks Write/Edit. +- The injection hook MUST degrade gracefully for large corpora — either by completing in time at lower fidelity or by short-circuiting cleanly; it MUST NOT time out in a way that costs a write its context. - Bin scripts must be POSIX-compatible shell (no bash-specific features). ## Invariants -- The PreToolUse block hook blocks 100% of direct Write/Edit to `.archcore/**/*.md` files. +- The PreToolUse block hook blocks 100% of direct Write/Edit to `.archcore/**/*.md` files, on every host that supports pre-mutation hooks. - The PreToolUse block hook never blocks writes outside `.archcore/`. - The PreToolUse injection hook never blocks any edit, regardless of result or error mode. - The PreToolUse injection hook and the PreToolUse block hook act on disjoint path sets — the injection hook is silent for every path the block hook acts on. -- Task-dispatched Write/Edit tool calls are subject to the same PreToolUse behavior as main-session calls; there is no dispatcher-based bypass. +- Delegated Write/Edit tool calls are subject to the same PreToolUse behavior as main-session calls; there is no dispatcher-based bypass. - The PostToolUse hooks never modify files — they only report. - Every PostToolUse matcher covers both archcore tool namings — no hook silently dies when the MCP registration path changes. +- Every guard that fires also acts: no naming reaches a script that the script cannot recognize. - Hook 4 (validation) and Hook 5 (cascade) fire independently on `update_document` — neither depends on the other. - SessionStart and PostToolUse hooks exit 0 regardless of outcome. -- The PreToolUse block hook exits 0 (allow) or 2 (block) — never other codes. +- The PreToolUse block hook exits 0 (allow, or Copilot deny-JSON) or 2 (block) — never other codes. - The PreToolUse injection hook exits 0 — never other codes. - SessionStart never initiates a binary download (the plugin no longer has download logic; CLI lifecycle is the user's responsibility). - SessionStart emits the staleness warning at most once per 24h per project, and the update advisory at most once per 24h. @@ -405,29 +436,29 @@ PostToolUse handler running the precision lexicon, mandatory-sections, frontmatt - If `archcore` is not on PATH: SessionStart emits the install message and exits 0; PostToolUse hooks skip validation/cascade silently. PreToolUse hooks (Hooks 2 and 3) do not depend on the CLI — Hook 2 only inspects file paths; Hook 3 scans `.archcore/` via shell grep. - If stdin JSON is malformed: exit 0 with empty output (fail open, don't break the session). -- If `archcore doctor` hangs: enforced by `timeout 2` inside the script plus the hook's `timeout: 3` envelope. +- If `archcore doctor` hangs: enforced by `timeout 2` inside the script plus the hook's 3-second envelope. - If git is unavailable for staleness check: skip silently, context loading continues. - If relation graph is empty for cascade check: produce no output (no cascade possible). - If the staleness timestamp file is missing, empty, or contains non-numeric data: treat as "never emitted" and run the check normally. - If `archcore update --check` fails, is unsupported, or the network is down: the advisory stays silent; no retry, no error surface. - If the injection hook encounters any error (grep failure, malformed frontmatter, I/O error): exit 0 with empty output. +- If a host's pre-mutation hook times out: on Copilot the write proceeds (fail-open) — mitigated by keeping both guards far inside budget rather than by relying on the host. Elsewhere the host's own timeout semantics apply. Observed per host as probe D in `host-probe-protocol.spec.md`. ## Conformance The hooks system conforms to this specification if: -1. `hooks/hooks.json` contains all six hook entries (SessionStart, two PreToolUse on `Write|Edit`, three PostToolUse on MCP matchers with dual tool naming). +1. Every host hooks config (`hooks.json`, `cursor.hooks.json`, `codex.hooks.json`, `copilot.hooks.json`) registers the six behaviors its host's event set supports, with dual tool naming wherever the host uses matchers. Documented per-host gaps (Cursor's `Write`-only matcher and absent postToolUse) are declared, not silent. 2. `bin/session-start` guards against plugin-install cwd at any depth, emits an install message when `archcore` is missing, emits init guidance when `.archcore/` is missing, otherwise delegates to `archcore hooks`, then calls `bin/check-staleness` and the rate-limited update advisory. -3. `bin/check-archcore-write` blocks `.archcore/**/*.md` writes via exit 2 + stderr and allows everything else. -4. `bin/check-code-alignment` injects top-ranked `.archcore/` context for source-file edits inside configured source roots, exits 0 on every code path, and honors the `ARCHCORE_DISABLE_INJECTION=1` escape hatch. +3. `bin/check-archcore-write` blocks `.archcore/**/*.md` writes through each host's honored deny mechanism and allows everything else. +4. `bin/check-code-alignment` injects top-ranked `.archcore/` context for source-file edits inside configured source roots, exits 0 on every code path, honors the `ARCHCORE_DISABLE_INJECTION=1` escape hatch, and keeps its cost independent of the match count. 5. `bin/validate-archcore` runs `archcore doctor` directly (no launcher wrapper) for archcore MCP tool calls and is covered by the Test Contract above. 6. `bin/check-staleness` detects code-doc drift via git, emits only when matching documents are found, and is rate-limited to once per 24h. 7. `bin/check-cascade` detects relation cascade after `update_document` and outputs warnings. 8. `bin/check-precision` runs the precision checks after `create_document` and `update_document`. -9. Both PreToolUse hooks complete within 1 second. +9. Both PreToolUse hooks complete within 1 second, pinned by `test/unit/hook-latency.bats`. 10. PostToolUse hooks complete within 3 seconds. 11. SessionStart never initiates a binary download — the plugin contains no fetcher. -12. Output formats follow Claude Code hooks documentation (exit codes, hookSpecificOutput object) with host-normalized Cursor shape where applicable. -13. Sub-agent tool invocations (Task-dispatched Write/Edit) are covered by Hooks 2 and 3 identically to main-session calls; no committed code contains a probe line. +12. Output formats follow each host's documented shape — `hookSpecificOutput` for Claude Code and Codex, flat `additional_context` for Cursor, top-level `additionalContext` and `permissionDecision` deny JSON for Copilot, plain text for OpenCode — all produced by the shared output helpers rather than by per-script string building. +13. Delegated tool invocations are covered by Hooks 2 and 3 identically to main-session calls; no committed code contains a probe line (the harness in `test/probe/` wraps a copy — see `host-probe-protocol.spec.md`). 14. Every script that invokes `archcore` passes only allowlisted subcommands; the contract is enforced by `test/structure/readme-cli-references.bats` and per-script invocation-log assertions. -15. No `bin/archcore`, `bin/archcore.cmd`, `bin/archcore.ps1`, `bin/CLI_VERSION`, or any download/cache logic exists in the repo. \ No newline at end of file diff --git a/.archcore/plugin/host-adapter-contract.spec.md b/.archcore/plugin/host-adapter-contract.spec.md index 829061b..4655c2e 100644 --- a/.archcore/plugin/host-adapter-contract.spec.md +++ b/.archcore/plugin/host-adapter-contract.spec.md @@ -10,20 +10,20 @@ tags: ## Purpose -Formalize the boundary between the **portable core** of the Archcore plugin and the **per-host adapters**, so host expansion (GitHub Copilot and OpenCode next) reuses the core unchanged instead of refactoring it. This spec is the contract every new host adapter is built against — and the gate contributor PRs are reviewed against. +Formalize the boundary between the **portable core** of the Archcore plugin and the **per-host adapters**, so host expansion (OpenCode next) reuses the core unchanged instead of refactoring it. This spec is the contract every new host adapter is built against — and the gate contributor PRs are reviewed against. **Ownership:** the portable core and this contract are maintainer-owned. Key design decisions (boundary changes, new adapter targets, hook-semantics mappings) are recorded by the maintainer before implementation; contributor work is limited to adapter implementation against an accepted decision. See `stack-and-tooling.rule` for the language/tooling side of the same policy. ## Scope -Covers the three implemented hosts (Claude Code, Cursor, Codex CLI), the two planned adapters (GitHub Copilot, OpenCode), and any future host. Applies to everything under `plugins/archcore/` and to adapter code that lives outside this repo (e.g. an npm wrapper package for OpenCode): external adapters are bound by the same MUST NOT rules even where the stack rule's shell-only requirement (which is scoped to this repo) does not reach them. +Covers the four implemented hosts (Claude Code, Cursor, Codex CLI, GitHub Copilot CLI), the one planned adapter (OpenCode), and any future host. Applies to everything under `plugins/archcore/` and to adapter code that lives outside this repo (e.g. an npm wrapper package for OpenCode): external adapters are bound by the same MUST NOT rules even where the stack rule's shell-only requirement (which is scoped to this repo) does not reach them. ## Portable core (normative) The core is maintained once and is host-agnostic: - `plugins/archcore/skills/` — Agent Skills (SKILL.md). MUST NOT contain host-conditional instructions. -- `plugins/archcore/agents/` — markdown agent definitions plus per-host format variants (e.g. Codex TOML), kept in parity by structure tests. +- `plugins/archcore/agents/` — markdown agent definitions plus per-host format variants (Codex TOML in the same directory; Copilot `*.agent.md` in `copilot-agents/`, which must stay outside `agents/` because `.agent.md` matches the `*.md` glob Claude Code and Cursor use), kept in parity by structure tests. - `plugins/archcore/commands/` — slash-command wrappers. - `plugins/archcore/bin/` — hook scripts + `bin/lib/normalize-stdin.sh`. POSIX sh only (stack rule). Host awareness is allowed **only** here: stdin detection and field extraction in `normalize-stdin.sh`, and host-conditional *output/exit conventions* in hook scripts (e.g. a Copilot deny-JSON branch), keyed on `ARCHCORE_HOST`. @@ -31,19 +31,19 @@ Everything else an adapter ships is **configuration, not logic**. ## Normative Behavior — adapter MUST provide -1. **Manifest** the host discovers. A shared manifest is preferred where the host reads it (Copilot's loader checks `.claude-plugin/plugin.json`). +1. **Manifest** the host discovers, in the location that host searches first. Copilot CLI checks `.plugin/plugin.json`, then `plugin.json`, then `.github/plugin/plugin.json`, then `.claude-plugin/plugin.json` — so a dedicated manifest wins over the shared one, and `copilot-adapter-design.adr` takes the dedicated path deliberately rather than relying on the Claude fallback. 2. **Hook wiring** for the three canonical lifecycle events, each routed to the shared `bin/` scripts: - session-start → `bin/session-start` - pre-mutation guard → `bin/check-archcore-write` (+ `bin/check-code-alignment` for source edits) - post-mutation validation → `bin/validate-archcore` (+ cascade/precision checks) -3. **MCP registration** launching `archcore mcp` resolved from PATH — or a documented user-side fallback where plugin-shipped MCP is unsafe (the Cursor precedent, `cursor-mcp-architecture.adr`). +3. **MCP registration** launching `archcore mcp` resolved from PATH — or a documented user-side fallback where plugin-shipped MCP is unsafe. Two hosts now take that fallback for the same reason (the host launches the MCP child outside the user's project): `cursor-mcp-architecture.adr` and `copilot-mcp-architecture.adr`. 4. **A host case in `bin/lib/normalize-stdin.sh`** — detection plus the canonical env schema (`ARCHCORE_HOST`, `ARCHCORE_TOOL_NAME`, `ARCHCORE_FILE_PATH`, …). 5. **A mutation-tool coverage-matrix row**: the full set of the host's filesystem-mutation tools, asserted by a structure test so a coverage gap fails CI instead of surfacing when a user trips it. -6. **A dated three-probe verification** (main-session source write, delegated write, `.archcore/` write) recorded in `hooks-validation-system.spec`. +6. **A dated probe record** in `host-probe-protocol.spec` — one probe per shipped guard (`P0` gate, then `A` main-session source write, `A-d` delegated write, `B` `.archcore/` write, `C` MCP validation), with unavailable probes recorded as `n/a` or `deferred` rather than omitted. ## Normative Behavior — adapter MUST NOT -- Fork or copy skills/agents per host, or patch skill text for one host. +- Fork or copy skills/agents per host, or patch skill text for one host. Format variants that a host's loader requires (Codex TOML, Copilot `*.agent.md`) are exempt and parity-tested byte-for-byte. - Reimplement guard/validation logic outside `bin/` — **including in adapter code written in other languages**. Hosts with programmatic hook systems (OpenCode JS/TS plugins) implement hooks as thin bridges: build the canonical stdin JSON, spawn the `bin/` script, translate its exit/output into host semantics. No decision logic lives in the bridge. - Introduce runtimes or languages into `plugins/archcore/` without an accepted ADR (`stack-and-tooling.rule`). - Ship logic in manifests or hook configs (configuration only). @@ -70,10 +70,10 @@ A given translation lives in exactly one place: either the `bin/` script's host- ## Error Handling - Guard scripts stay authoritative for *what* is blocked and *why* (the reason text); adapters only decide *how* the host is told. -- Fail-open/fail-closed differences per host (e.g. Copilot's preToolUse timeout fails open) are documented in the host's support doc and covered by the probe protocol — not papered over. +- Fail-open/fail-closed differences per host are documented in the host's support doc and observed by probe D, never papered over. Copilot's `preToolUse` timeout fails open, which makes guard latency a correctness concern there rather than a comfort one — `test/unit/hook-latency.bats` keeps both PreToolUse scripts far enough inside the 1 s budget that the timeout path stays unreachable. ## Conformance - Structure tests pin: the per-host coverage matrix; no host-conditional text in `skills/`; `bin/*` are `#!/bin/sh`; agent format parity. -- A new host counts as "supported" only when all six MUST-provide items exist and dated probe results are recorded. -- PRs that change anything in the portable core, or add adapter logic outside this contract, require maintainer review and a link to an accepted decision document. \ No newline at end of file +- A new host counts as "supported" only when all six MUST-provide items exist and its row in `host-probe-protocol.spec` records `pass` or a justified `n/a` for P0, A, B and C. +- PRs that change anything in the portable core, or add adapter logic outside this contract, require maintainer review and a link to an accepted decision document. diff --git a/.archcore/plugin/host-probe-protocol.spec.md b/.archcore/plugin/host-probe-protocol.spec.md new file mode 100644 index 0000000..d6308d3 --- /dev/null +++ b/.archcore/plugin/host-probe-protocol.spec.md @@ -0,0 +1,86 @@ +--- +title: "Host Probe Protocol — Live-Session Verification and Dated Records" +status: accepted +tags: + - "hooks" + - "multi-host" + - "plugin" + - "testing" + - "validation" +--- + +## Purpose & Scope + +Defines the live-session verification a host adapter MUST pass before it counts as supported, and holds the dated records of those runs. Covers every implemented host (Claude Code, Cursor, Codex CLI, GitHub Copilot CLI) and every future one. This document satisfies item 6 of `host-adapter-contract.spec`. + +Static tests prove the wiring is *shaped* right. Only a live session proves the host *runs* it: that it loads the config at all, that its matcher fires on the tool name the model actually chose, and that a deny is honored rather than merely displayed. Those questions are what this protocol asks, and nothing else — everything mechanically checkable belongs in bats. + +## Surface + +- **Probe ids** — `P0` (gate), `A`, `A-d`, `B`, `C`, `D`. One probe per shipped guard, not one guard observed three ways. +- **`test/probe/mkprobe`** — builds a disposable probe tree; never writes inside `plugins/archcore/`. +- **Records table** — append-only, between the HTML `PROBE-RECORDS` markers below. +- **Structure tests** — `probe-hygiene.bats` (no probe residue under `plugins/`), `probe-records.bats` (every enrolled host has a well-formed row), `probe-wrapper.bats` (the harness wrapper is transparent). + +| Probe | Guard exercised | Action in a live session | +|---|---|---| +| P0 | `bin/session-start` | start a session; its output must prove the **probe tree** was loaded | +| A | `bin/check-code-alignment` | main session writes `src/probe/alpha.ts` | +| A-d | same, via delegation | a sub-agent writes `src/probe/beta.ts` | +| B | `bin/check-archcore-write` | write `.archcore/probe/p.adr.md`; the host must refuse and show the reason | +| C | `bin/validate-archcore` | `update_document` through MCP | +| D | host timeout path | repeat B with a slowed guard | + +## Normative Behavior + +1. WHEN a probe run begins, the operator MUST establish P0 before issuing any other prompt. +2. IF P0 does not prove the probe tree was loaded, THEN the operator MUST NOT record a row for that run. +3. WHEN probe A runs, the operator MUST observe injected context naming the edited path. +4. WHEN probe A-d runs, the operator MUST observe the same guard firing on the delegated call. +5. WHEN probe B runs, the host MUST refuse the write and surface the guard's reason text. +6. WHEN probe C runs, the operator MUST observe validation output for the MCP call. +7. WHEN a probe run completes, the operator MUST append one row to the records table and MUST paste the captured evidence verbatim into the commit body. +8. WHEN a probe run captures host stdin, the operator MUST add it to `test/fixtures/stdin//` so the payload shape becomes a CI assertion. +9. The operator MUST NOT record `pass` for a probe by analogy with another host. + +## Constraints & Invariants + +- The harness MUST live under `test/`, which `release.yml` strips from `main`. +- `mkprobe` MUST copy the working tree to a temporary directory and wrap scripts **in the copy**; no probe line ever reaches a shipped file (`hooks-validation-system.spec` conformance 13). +- `mkprobe` MUST stamp a sentinel version into the copied manifests: a version equal to a cached install lets the host serve the cache instead of the probe tree, which is exactly how the first attempt failed (`jtbd1-phase2-hardening-delegated.plan`). +- The probe project MUST be a **sibling** of the plugin copy, never nested inside it — `bin/session-start` walks upward for plugin manifests and would silence P0. +- A table cell MUST NOT contain `|`; a pipe shifts every column after it, and the outcome checks would then read the wrong fields and pass on garbage. Write the method as `+`, e.g. `install+log`. +- A row, once written, MUST NOT be edited; a re-run appends. + +## Failure Behavior + +- IF the host offers no delegation surface, THEN A-d MUST be recorded `n/a:no-delegation-surface ( )`. +- IF a probe cannot be run yet, THEN it MUST be recorded `deferred:` rather than omitted. +- IF probe D shows the guard bypassed on timeout, THEN it MUST be recorded `fail-open-confirmed`; if the host still denies, `fail-closed-observed`. +- IF the host's self-report is the only evidence for a probe, THEN the grade MUST be `report`, not `log` — on Cursor `additional_context` may be dropped, so an agent's claim that it saw context proves nothing. + +Outcome vocabulary: `pass` · `fail` · `n/a:` · `deferred:` · D only: `fail-open-confirmed` | `fail-closed-observed`. Method notation: how the plugin reached the session (`install` or `wired`) `+` evidence grade (`log`, `report` or `ui`). + +## Records + +Evidence pointer is `:`; the log itself lives in that commit's body. + + + +| Date | Host | Host ver | Plugin ver | Method | P0 | A | A-d | B | C | D | Evidence | +|---|---|---|---|---|---|---|---|---|---|---|---| +| — | claude-code | — | — | — | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | — | +| — | cursor | — | — | — | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | — | +| — | codex | — | — | — | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | — | +| — | copilot | — | — | — | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | deferred:not-yet-run | — | + + + +Every row starts `deferred:not-yet-run` on purpose. Before this document existed, the contract demanded dated probe results for every host and none were recorded anywhere — a silent gap. Rows make it visible and testable, and they are replaced as runs happen. + +## Conformance + +1. `test/structure/probe-records.bats` parses the table and fails if a host enrolled in `host-coverage-matrix.bats` has no row, if an outcome falls outside the vocabulary, or if a cell smuggles a pipe. +2. `test/structure/probe-hygiene.bats` fails if any probe marker or harness artifact appears under `plugins/`. +3. `test/unit/probe-wrapper.bats` fails if the wrapper alters stdout, stderr, or exit status for any fixture. +4. A host counts as supported only when its row records `pass` or a justified `n/a` for P0, A, B and C. diff --git a/.archcore/plugin/host-wiring-parity.adr.md b/.archcore/plugin/host-wiring-parity.adr.md index a5da071..e42d4d5 100644 --- a/.archcore/plugin/host-wiring-parity.adr.md +++ b/.archcore/plugin/host-wiring-parity.adr.md @@ -19,24 +19,26 @@ Verified facts that shaped the design: - Cursor guarantees cwd neither for agent shell commands nor for stdio MCP spawns (docs silent, community-confirmed; historical dev-docs leak). Any wiring path must carry the project root explicitly. - The CLI hook installers' idempotency probes matched exact command strings — a changed hook command would append a duplicate entry instead of updating (fixed as a precondition). +GitHub Copilot CLI joined later and made wiring **mandatory** rather than merely nice: `copilot-mcp-architecture.adr` removes plugin-shipped MCP on that host too, so `archcore init --agent copilot` is the only route to MCP tools there. + ## Decision One Go core, two thin frontends, a deterministic cascade in the skill: -- **Core** — the existing installers in `cli` (`installAgents`, instructions writers, hook installers) with two hardening fixes: marker-based hook probes (`archcore hooks` substring) that update a stale archcore entry in place and heal past duplicates, and a plugin-cache guard in `resolveProjectRoot` (rejects roots containing `.cursor/plugins/`, `.claude/plugins/`, `.codex/plugins/`, `plugins/cache/`; a plugin *developer* repo with manifests at its root stays valid). -- **Instruction-nudge targets (CLI ≥ v0.6.1)** — claude-code: one fenced managed block (`` … ``) upserted into BOTH `CLAUDE.md` and `AGENTS.md` — CLAUDE.md is what Claude Code natively reads, AGENTS.md is the shared standard block the other hosts converge on; one write refreshes both, and the CLI deletes the legacy owned `.claude/rules/archcore.md` whenever claude-code is (re)wired. cursor / codex-cli: the `AGENTS.md` managed block. The `install_host_config` JSON report names the primary file in `instructions_path` and any additional files in the additive `instructions_extra_paths` field (CLI ≥ v0.6.2); the skill's preview and closing message name both claude-code files themselves, independent of the report shape. +- **Core** — the existing installers in `cli` (`installAgents`, instructions writers, hook installers) with two hardening fixes: marker-based hook probes (`archcore hooks` substring) that update a stale archcore entry in place and heal past duplicates, and a plugin-cache guard in `resolveProjectRoot` (rejects roots containing `.cursor/plugins/`, `.claude/plugins/`, `.codex/plugins/`, `plugins/cache/`; a plugin *developer* repo with manifests at its root stays valid). The fragment list does **not** yet name `~/.copilot/installed-plugins/` — a tracked follow-up recorded in `copilot-mcp-architecture.adr`, which is why that host currently rests on the manifest omission alone. +- **Instruction-nudge targets (CLI ≥ v0.6.1)** — claude-code: one fenced managed block (`` … ``) upserted into BOTH `CLAUDE.md` and `AGENTS.md` — CLAUDE.md is what Claude Code natively reads, AGENTS.md is the shared standard block the other hosts converge on; one write refreshes both, and the CLI deletes the legacy owned `.claude/rules/archcore.md` whenever claude-code is (re)wired. cursor / codex-cli / copilot: the `AGENTS.md` managed block. The `install_host_config` JSON report names the primary file in `instructions_path` and any additional files in the additive `instructions_extra_paths` field (CLI ≥ v0.6.2); the skill's preview and closing message name both claude-code files themselves, independent of the report shape. - **B, primary** — MCP tool `install_host_config(host, all_detected)`: executes in the MCP server process whose baseDir is correct by construction, returns a JSON report. Injected from `cmd` into the server (no import cycle); the server shields the JSON-RPC stream by pointing `os.Stdout` at stderr for tool-executor prints. -- **A, fallback** — `archcore init --agent ... --project `: non-interactive (no picker, no confirms), validates ids before any write, writes under the resolved root regardless of process cwd. Serves Cursor day-one (no archcore MCP connected yet), CI, and scripts. `--project` also threaded into `hooks install` / `instructions install`. +- **A, fallback** — `archcore init --agent ... --project `: non-interactive (no picker, no confirms), validates ids before any write, writes under the resolved root regardless of process cwd. Serves Cursor day-one (no archcore MCP connected yet), Copilot always, CI, and scripts. `--project` also threaded into `hooks install` / `instructions install`. - **C, last resort** — a ready-to-run terminal command for the user (`archcore update && archcore init --agent --project ""`) when the CLI turns out too old for `--agent` at execution time. -- **Skill** — `/archcore:init` detects the host via `bin/detect-host` (env-only: `CLAUDECODE`/`CLAUDE_SKILL_DIR` → claude-code, `CURSOR_TRACE_ID` → cursor, `CODEX_HOME` → codex-cli, else `__UNKNOWN__` → ask; precedence claude > cursor > codex because sessions inherit stray companion vars), shows the wiring files AND the resolved project root in the preview, and executes the cascade only after `confirm`. Scope: current host by default, `edit → hosts: all` for team repos. The empty route (no source yet) offers wiring behind its own mini-confirm — an empty repo is exactly where a CLI-only teammate needs the configs. `--refresh` retrofits wiring on repos seeded before this ADR. A user who declines the version-gate update never enters the cascade — their channel is the closing-message note carrying the same manual command. -- **Deterministic version gate** — the skill's pre-flight compares the installed CLI against v0.6.1 via `bin/cli-gte` (numeric field-by-field semver compare, prints `yes|no|__NO_CLI__`, always exit 0), never in prose: an LLM comparing "0.10.0" against "0.6.0" lexically gets it wrong. Same right-sized-helper mold as `detect-host`; contract in `test/unit/cli-gte.bats`. The gate was bumped from v0.6.0 when CLI v0.6.1 moved the claude-code nudge to the CLAUDE.md + AGENTS.md managed blocks: a v0.6.0 CLI still writes the legacy `.claude/rules/archcore.md` layout, which the skill's preview no longer promises — the gate keeps preview and reality in lockstep. -- **Dual naming everywhere a tool list gates behavior** — PostToolUse matchers (both hooks configs), agent allow-lists (`agents/*.md` `tools:`), and the Codex auditor's deny-list (`agents/archcore-auditor.toml` `disabled_tools`) each carry both namings. Deny-lists fail OPEN under naming drift (an unlisted twin would let the read-only auditor mutate), so the twins are mandatory there even if Codex ultimately yields only one naming. Twin pairing is guarded by structure tests in `test/structure/hooks.bats` (matchers) and `test/structure/agents.bats` (agent lists). -- **Cursor MCP config** gets a dedicated writer emitting `args: ["mcp", "--project", "${workspaceFolder}"]` (project-level interpolation is documented), aligning the installed config with `docs/cursor.mcp.example.json` and the cwd-independence invariant. The shared standard writer keeps plain `["mcp"]` for Claude. -- **SessionStart dedup in the binary** — both the plugin hook script and a repo-committed hook delegate to `archcore hooks session-start`; the handler now stamps `session_id`+`source` (Cursor: `conversation_id`) in XDG state with a short window, so a double-registered hook emits context once. Living in the binary, it protects every plugin/CLI version combination. Fail-open on missing id or unwritable state. -- **Freshness loop** — `archcore update --check` (24h-cached, ~500ms-bounded, always exit 0, silent on failure) + a rate-limited session-start advisory; the skill's pre-flight gates wiring on CLI ≥ v0.6.1 with a consent-based `archcore update` offer. Auto-update from hooks was rejected (offline/latency/enterprise/pinning failure modes — the bundled-launcher lesson). +- **Skill** — `/archcore:init` detects the host via `bin/detect-host` (env-only: `CLAUDECODE`/`CLAUDE_SKILL_DIR` → claude-code, `CURSOR_TRACE_ID` → cursor, `CODEX_HOME` → codex-cli, else `__UNKNOWN__` → ask; precedence claude > cursor > codex because sessions inherit stray companion vars). Copilot has **no detection branch by design** — every documented `COPILOT_*` variable is read *from* the user rather than set by the host — so a Copilot session lands on `__UNKNOWN__` and reaches wiring through the ask path, which is why that path offers all four hosts. The skill shows the wiring files AND the resolved project root in the preview, and executes the cascade only after `confirm`. Scope: current host by default, `edit → hosts: all` for team repos. The empty route (no source yet) offers wiring behind its own mini-confirm — an empty repo is exactly where a CLI-only teammate needs the configs. `--refresh` retrofits wiring on repos seeded before this ADR. A user who declines the version-gate update never enters the cascade — their channel is the closing-message note carrying the same manual command. +- **Deterministic version gate** — the skill's pre-flight compares the installed CLI against the pinned minimum via `bin/cli-gte` (numeric field-by-field semver compare, prints `yes|no|__NO_CLI__`, always exit 0), never in prose: an LLM comparing "0.10.0" against "0.6.0" lexically gets it wrong. Same right-sized-helper mold as `detect-host`; contract in `test/unit/cli-gte.bats`. The gate moved from v0.6.0 to v0.6.1 when the claude-code nudge became the CLAUDE.md + AGENTS.md managed blocks, and to v0.6.4 when the Copilot writer stopped targeting `.vscode/mcp.json` (a surface Copilot CLI dropped in v1.0.37) in favour of the workspace-root `.mcp.json` it actually reads. Each bump keeps the preview and the writes in lockstep. +- **Dual naming everywhere a tool list gates behavior** — PostToolUse matchers (Claude and Codex configs; Cursor has no postToolUse event, and Copilot's carries no matcher because the scripts self-filter there), agent allow-lists (`agents/*.md` `tools:`, which also carry Copilot's flat `archcore-` form), and the Codex auditor's deny-list (`agents/archcore-auditor.toml` `disabled_tools`) each carry every naming the host can produce. Deny-lists fail OPEN under naming drift (an unlisted twin would let the read-only auditor mutate), so the twins are mandatory there even if Codex ultimately yields only one naming. `bin/lib/normalize-stdin.sh` additionally folds all three namings to the canonical one before any guard script inspects a tool name, so downstream scripts match exactly one string. Twin pairing is guarded by structure tests in `test/structure/hooks.bats` (matchers) and `test/structure/agents.bats` (agent lists). +- **Cursor MCP config** gets a dedicated writer emitting `args: ["mcp", "--project", "${workspaceFolder}"]` (project-level interpolation is documented), aligning the installed config with `docs/cursor.mcp.example.json` and the cwd-independence invariant. The shared standard writer keeps plain `["mcp"]` for Claude and Copilot, which both read a workspace-root `.mcp.json` from the project process. +- **SessionStart dedup in the binary** — both the plugin hook script and a repo-committed hook delegate to `archcore hooks session-start`; the handler now stamps `session_id`+`source` (Cursor: `conversation_id`) in XDG state with a short window, so a double-registered hook emits context once. Living in the binary, it protects every plugin/CLI version combination. Fail-open on missing id or unwritable state. The response shape is per host: Copilot's native `sessionStart` takes a bare `{"additionalContext":…}`, not Claude's `hookSpecificOutput` wrapper (CLI ≥ v0.6.4). +- **Freshness loop** — `archcore update --check` (24h-cached, ~500ms-bounded, always exit 0, silent on failure) + a rate-limited session-start advisory; the skill's pre-flight gates wiring on the pinned CLI minimum with a consent-based `archcore update` offer. Auto-update from hooks was rejected (offline/latency/enterprise/pinning failure modes — the bundled-launcher lesson). - **doctor --fix convergence** — `--agent`/`--project` flags; re-runs installers (now update-capable) and converges drifted MCP entries (e.g. a pre-`--project` Cursor config) via semantic-diff rewrite that never touches foreign servers. -Rollout order is part of the decision: CLI v0.6.0 first (dedup protects old plugins immediately), plugin second (version-gated skill), advisories last. +Rollout order is part of the decision: CLI first (dedup protects old plugins immediately), plugin second (version-gated skill), advisories last. ## Alternatives @@ -55,6 +57,7 @@ Rollout order is part of the decision: CLI v0.6.0 first (dedup protects old plug - Codex Desktop ignores project-level MCP config (upstream #13025) — CLI unaffected. - The "everything old + newly wired repo" combination (old CLI, old plugin, teammate pulls wiring) still double-fires SessionStart until that teammate updates the CLI; the version-gate narrows the window, rollout order narrows it further. - Runtime `--project` in the Cursor config requires teammates' CLI ≥ v0.3.6 (flag introduction); all other committed commands exist since v0.0.1. +- **On Copilot, wiring is not an improvement but a dependency.** Without it the plugin has no MCP tools at all, so a Copilot user on a CLI older than v0.6.4 gets skills, commands, agents and hooks and nothing to write documents with. The version gate makes that visible instead of silent. - **Shell-guard asymmetry with the CLI.** `resolveProjectRoot` (CLI) accepts a plugin *developer* repo whose root carries the manifests; the plugin's `bin/session-start` guard does not — its bounded upward walk silences the hook in any directory nested (≤ 12 levels) under a manifest-carrying ancestor, developer repos included. This extends the pre-existing behavior (manifest-at-cwd already silenced the hook) and is accepted: a false silent-exit costs one session's context, a false emit surfaces the plugin's bundled files as the user's knowledge base. Legitimate projects nested deeper than 12 levels below such an ancestor run normally (bound tested both ways). - **`plugins/cache/` path-fragment false positive.** A user project whose absolute path contains a literal `plugins/cache/` segment is silently silenced by guard layer 1. Accepted deliberately — the fragment set mirrors the CLI guard, and the collision odds are negligible against the misrouted-cwd risk it closes. Pinned by tests either way. - **The update advisory is silent for the population it targets during this rollout.** `archcore update --check` exists only in CLI ≥ v0.6.0, so users on older CLIs — the transition's whole audience — get nothing from this channel; the pre-flight gate and the config-rejection advisory cover them instead. Accepted: the advisory starts paying off one CLI generation out. Do NOT "fix" the silence with version-sniffing in the hook — that re-imports the prose-version-logic drift class. @@ -63,5 +66,5 @@ Rollout order is part of the decision: CLI v0.6.0 first (dedup protects old plug ## Key files -- CLI: @cmd/init.go, @cmd/host_wiring.go, @cmd/hooks_install.go, @cmd/mcp_root.go, @cmd/hooks_claude_code.go, @cmd/doctor.go, @cmd/update.go, @internal/agents/mcp_helpers.go, @internal/agents/instructions.go, @internal/mcp/tools/install_host_config.go, @internal/mcp/server.go -- Plugin: @plugins/archcore/bin/detect-host, @plugins/archcore/bin/cli-gte, @plugins/archcore/bin/session-start, @plugins/archcore/hooks/hooks.json, @plugins/archcore/hooks/codex.hooks.json, @plugins/archcore/agents/archcore-auditor.toml, @plugins/archcore/skills/init/SKILL.md \ No newline at end of file +- CLI: @cmd/init.go, @cmd/host_wiring.go, @cmd/hooks_install.go, @cmd/hooks_copilot.go, @cmd/mcp_root.go, @cmd/hooks_claude_code.go, @cmd/doctor.go, @cmd/update.go, @internal/agents/mcp_helpers.go, @internal/agents/copilot.go, @internal/agents/instructions.go, @internal/wiring/hooks_agents.go, @internal/mcp/tools/install_host_config.go, @internal/mcp/server.go +- Plugin: @plugins/archcore/bin/detect-host, @plugins/archcore/bin/cli-gte, @plugins/archcore/bin/session-start, @plugins/archcore/bin/lib/normalize-stdin.sh, @plugins/archcore/hooks/hooks.json, @plugins/archcore/hooks/codex.hooks.json, @plugins/archcore/hooks/copilot.hooks.json, @plugins/archcore/agents/archcore-auditor.toml, @plugins/archcore/skills/init/SKILL.md diff --git a/.archcore/plugin/multi-host-plugin-architecture.adr.md b/.archcore/plugin/multi-host-plugin-architecture.adr.md index 7e0463a..fc608c7 100644 --- a/.archcore/plugin/multi-host-plugin-architecture.adr.md +++ b/.archcore/plugin/multi-host-plugin-architecture.adr.md @@ -19,9 +19,9 @@ Analysis showed that ~95% of plugin code is host-agnostic: skills, agents, hook ### Drivers -- Users of Cursor, Copilot, and Codex CLI need Archcore integration — Codex CLI v0.117.0+ (March 2026) added a plugin system with near-1:1 surface to Claude Code, validating the third-host bet. +- Users of Cursor, Copilot, and Codex CLI need Archcore integration — Codex CLI v0.117.0+ (March 2026) added a plugin system with near-1:1 surface to Claude Code, validating the multi-host bet; GitHub Copilot CLI followed as the fourth host. - Industry convergence on Agent Skills + MCP makes cross-host support low-effort. -- Maintaining separate repos per host would mean duplicating 16 skills, 2 agents, and 6 hook scripts. +- Maintaining separate repos per host would mean duplicating every skill, agent, and hook script. ## Decision @@ -29,9 +29,9 @@ Analysis showed that ~95% of plugin code is host-agnostic: skills, agents, hook The plugin ships: -- **Shared core** (host-agnostic): `skills/`, `agents/` (both `.md` and `.toml` variants), `bin/` (hook scripts + stdin normalizer), `commands/` (Codex slash command wrappers). -- **Per-host adapter files** (configuration only, no logic): plugin manifest, hooks config, and an MCP config per host. -- **No bundled CLI**: the Archcore CLI is installed globally by the user per https://docs.archcore.ai/cli/install/. All three MCP configs name `archcore` as the command, resolved via PATH. The plugin does not bundle, download, cache, or pin a CLI binary. +- **Shared core** (host-agnostic): `skills/`, `agents/` (both `.md` and `.toml` variants), `copilot-agents/` (`*.agent.md` copies, byte-identical), `bin/` (hook scripts + stdin normalizer), `commands/` (slash command wrappers). +- **Per-host adapter files** (configuration only, no logic): plugin manifest, hooks config, and — where the host can safely run one — an MCP config. +- **No bundled CLI**: the Archcore CLI is installed globally by the user per https://docs.archcore.ai/cli/install/. Every MCP config names `archcore` as the command, resolved via PATH. The plugin does not bundle, download, cache, or pin a CLI binary. The plugin itself lives in a dedicated `plugins/archcore/` subdirectory; the marketplace catalogs stay at the repo root and point each host's plugin `source`/`path` at that subdirectory. @@ -40,16 +40,20 @@ repo-root/ # marketplace CATALOGS + dev tooling ├── .claude-plugin/marketplace.json # Claude catalog → source: ./plugins/archcore ├── .cursor-plugin/marketplace.json # Cursor catalog → source: ./plugins/archcore ├── .agents/plugins/marketplace.json # Codex catalog → path: ./plugins/archcore +│ # (no Copilot catalog — it installs by subdir spec) ├── docs/cursor.mcp.example.json # Reference template users copy into ~/.cursor/mcp.json │ └── plugins/archcore/ # ← the plugin (single source of truth; what each host installs) - ├── commands/ # Codex CLI slash command wrappers (7, host-adapter shims) + ├── commands/ # Slash command wrappers (7, host-adapter shims) ├── skills/ # Shared — Agent Skills standard (7 skills) ├── agents/ # Shared — markdown agent definitions + Codex TOML variants │ ├── archcore-assistant.md # Claude Code / Cursor │ ├── archcore-assistant.toml # Codex CLI (sandbox_mode = "workspace-write") │ ├── archcore-auditor.md # Claude Code / Cursor │ └── archcore-auditor.toml # Codex CLI (sandbox_mode = "read-only" + disabled_tools) + ├── copilot-agents/ # GitHub Copilot CLI — *.agent.md copies (cmp-tested) + │ ├── archcore-assistant.agent.md + │ └── archcore-auditor.agent.md ├── bin/ # Shared — hook scripts + stdin normalizer (no CLI binary) │ ├── lib/normalize-stdin.sh │ ├── session-start @@ -63,18 +67,20 @@ repo-root/ # marketplace CATALOGS + dev tooling ├── .claude-plugin/plugin.json # Claude Code manifest ├── .cursor-plugin/plugin.json # Cursor manifest (no `mcpServers` field — deliberate) ├── .codex-plugin/plugin.json # Codex CLI manifest (single file) + ├── .plugin/plugin.json # GitHub Copilot CLI manifest (no `mcpServers` — deliberate) │ ├── hooks/ │ ├── hooks.json # Claude Code (PascalCase events) │ ├── cursor.hooks.json # Cursor (camelCase events) - │ └── codex.hooks.json # Codex CLI (PascalCase events + apply_patch matcher) + │ ├── codex.hooks.json # Codex CLI (PascalCase events + apply_patch matcher) + │ └── copilot.hooks.json # GitHub Copilot CLI (camelCase events, "bash" + timeoutSec) │ ├── .mcp.json # Claude Code — { "archcore": { "command": "archcore", "args": ["mcp"] } } ├── .codex.mcp.json # Codex CLI — direct server map └── rules/ # Cursor-only context rules (.mdc) ``` -**Catalog vs. plugin location.** The three marketplace catalogs stay at the repo root; each points its plugin `source`/`path` at the `plugins/archcore/` subdirectory, which holds the per-host manifests and all shared content. This subdirectory layout is *required* for Codex marketplace discovery — a catalog `source.path` of `./` (the marketplace root) is not scanned, so the plugin is never discovered there — and it is the canonical layout for Claude Code and Cursor as well. The reporter that surfaced this is issue #2; the full rationale, the cross-host docs matrix, and the rejected alternatives (generated copy, Windows-breaking symlinks) live in `subdirectory-plugin-layout.adr`, which extends this ADR. +**Catalog vs. plugin location.** The three marketplace catalogs stay at the repo root; each points its plugin `source`/`path` at the `plugins/archcore/` subdirectory, which holds the per-host manifests and all shared content. This subdirectory layout is *required* for Codex marketplace discovery — a catalog `source.path` of `./` (the marketplace root) is not scanned, so the plugin is never discovered there — and it is the canonical layout for Claude Code and Cursor as well. Copilot CLI has no marketplace concept at all and installs by subdir spec (`archcore-ai/plugin:plugins/archcore`), so it needs no fourth catalog. The reporter that surfaced the Codex case is issue #2; the full rationale, the cross-host docs matrix, and the rejected alternatives (generated copy, Windows-breaking symlinks) live in `subdirectory-plugin-layout.adr`, which extends this ADR. ### Shared core principle @@ -82,23 +88,27 @@ Skills, agents, and hook scripts are maintained once. All host-specific adapters ### MCP wiring -MCP is wired via plugin-shipped configs for Claude Code (`.mcp.json`) and Codex CLI (`.codex.mcp.json` pointed at by `.codex-plugin/plugin.json`'s `mcpServers` field). Both name `archcore` directly; Codex uses a direct server map in `.codex.mcp.json`, while Claude Code uses its `mcpServers` wrapper. The host runtime resolves `archcore` from PATH. Both hosts launch the MCP with cwd inherited from the user's project process, which is the correct workspace. +MCP is wired via plugin-shipped configs on the two hosts where that is safe: Claude Code (`.mcp.json`, discovered at the plugin root with no manifest key) and Codex CLI (`.codex.mcp.json` pointed at by `.codex-plugin/plugin.json`'s `mcpServers` field). Both name `archcore` directly; Codex uses a direct server map, Claude Code uses its `mcpServers` wrapper. The host runtime resolves `archcore` from PATH. Both hosts launch the MCP with cwd inherited from the user's project process, which is the correct workspace. -Cursor is the exception. Cursor 2.5+ does auto-detect plugin-shipped MCP configs (per the [official plugins reference](https://cursor.com/docs/reference/plugins.md), an `mcp.json` at the plugin root registers under "Plugin MCP Servers"), but it spawns the plugin-MCP from the plugin install directory rather than the workspace, and its MCP stdio schema has no `cwd` field ([forum #74861](https://forum.cursor.com/t/allow-workspacefolder-in-mcp-project-configration/74861), [forum #99215](https://forum.cursor.com/t/how-get-the-correct-current-work-directory-in-mcp-server/99215)). Until those gaps close upstream, shipping a plugin-MCP for Cursor would cause the server to read from the plugin install dir instead of the user's project. We therefore deliberately do **not** ship a plugin-MCP for Cursor: no `mcpServers` field in `.cursor-plugin/plugin.json`, no `mcp.json` at the plugin root, and the reference template lives under `docs/` so it cannot trigger auto-detection. Cursor users copy `docs/cursor.mcp.example.json` into `~/.cursor/mcp.json` or `.cursor/mcp.json`; the template passes `--project ${workspaceFolder}` in `args` so the server resolves the workspace regardless of cwd. See `cursor-mcp-architecture.adr.md` for the full rationale and three-layer defense (release-strip, docs-only template, runtime guards). +**Cursor and GitHub Copilot CLI are the exceptions, for the same underlying reason: the host launches a plugin's MCP child somewhere other than the user's project.** + +Cursor 2.5+ does auto-detect plugin-shipped MCP configs (per the [official plugins reference](https://cursor.com/docs/reference/plugins.md), an `mcp.json` at the plugin root registers under "Plugin MCP Servers"), but it spawns the plugin-MCP from the plugin install directory rather than the workspace, and its MCP stdio schema has no `cwd` field ([forum #74861](https://forum.cursor.com/t/allow-workspacefolder-in-mcp-project-configration/74861), [forum #99215](https://forum.cursor.com/t/how-get-the-correct-current-work-directory-in-mcp-server/99215)). We therefore ship no plugin-MCP for Cursor: no `mcpServers` field in `.cursor-plugin/plugin.json`, no `mcp.json` at the plugin root, and the reference template lives under `docs/` so it cannot trigger auto-detection. See `cursor-mcp-architecture.adr`. + +Copilot CLI launches a plugin's MCP child with cwd set to the plugin install root and passes it no project path — not even the `COPILOT_PROJECT_DIR` its plugin *hooks* receive ([github/copilot-cli#4234](https://github.com/github/copilot-cli/issues/4234)). Documents would be written into `~/.copilot/installed-plugins/` while every tool reported success. `.plugin/plugin.json` therefore declares no `mcpServers`, and Copilot users register the server per project via `archcore init --agent copilot`. The Cursor remedy of moving the file out of the plugin root was not available here: `.mcp.json` must stay where Claude Code finds it. See `copilot-mcp-architecture.adr`. The plugin does not bundle the CLI, does not download it, and does not cache it; users install it once via the official installer at https://docs.archcore.ai/cli/install/. ### Stdin normalization -Hook scripts source a shared `bin/lib/normalize-stdin.sh` that detects the host from stdin JSON structure and exposes a canonical schema (`ARCHCORE_HOST`, `ARCHCORE_TOOL_NAME`, `ARCHCORE_FILE_PATH`, etc.). Detection uses each host's distinct stdin fields (Claude Code → `tool_name`; Cursor → `conversation_id`; Copilot → `hookEventName`; Codex → `turn_id`). Codex shares Claude Code's snake_case schema, so the field-extraction logic for `codex` mirrors `claude-code`. +Hook scripts source a shared `bin/lib/normalize-stdin.sh` that detects the host from stdin JSON structure and exposes a canonical schema (`ARCHCORE_HOST`, `ARCHCORE_TOOL_NAME`, `ARCHCORE_FILE_PATH`, etc.). Detection uses each host's distinct stdin fields (Claude Code → `tool_name`; Cursor → `conversation_id`; Copilot → `hookEventName` on legacy payloads, `toolName`/`toolArgs` on native ones; Codex → `turn_id`). Codex shares Claude Code's snake_case schema, so the field-extraction logic for `codex` mirrors `claude-code`. The normalizer also folds all three MCP tool namings — `mcp__archcore__*`, `mcp__plugin_archcore_archcore__*`, and Copilot's flat `archcore-` — to one canonical name, so guard scripts match exactly one string instead of three. ## Alternatives Considered ### 1. Separate repository per host -One repo for Claude Code, one for Cursor, one for Codex CLI. Each contains full copies of skills, agents, and bin scripts. +One repo per host, each containing full copies of skills, agents, and bin scripts. -**Rejected because:** 16 skills × N hosts = massive duplication; any skill update must be synced across all repos; only ~5% of code is actually host-specific. +**Rejected because:** duplication scales with host count; any skill update must be synced across all repos; only ~5% of code is actually host-specific. ### 2. Build system that generates per-host packages @@ -118,14 +128,14 @@ Ship a `bin/archcore` launcher that resolves the Archcore CLI on demand from `$A ### 5. Ship `mcp.json` at the plugin root with `--project ${workspaceFolder}` in args -This is the canonical Cursor 2.5+ way to register a plugin MCP. Rejected because Cursor's `${workspaceFolder}` interpolation inside plugin-MCP `args` is undocumented and the open feature request ([forum #74861](https://forum.cursor.com/t/allow-workspacefolder-in-mcp-project-configration/74861)) implies plugin-MCPs do not get the interpolation that user-config MCPs do. Until Cursor confirms support, we cannot rely on it; see `cursor-mcp-architecture.adr.md` for the layered defense we adopted instead. +This is the canonical Cursor 2.5+ way to register a plugin MCP. Rejected because Cursor's `${workspaceFolder}` interpolation inside plugin-MCP `args` is undocumented and the open feature request ([forum #74861](https://forum.cursor.com/t/allow-workspacefolder-in-mcp-project-configration/74861)) implies plugin-MCPs do not get the interpolation that user-config MCPs do. The same shape was reconsidered for Copilot and rejected harder: #4234 reports that the child gets no project path at all, so there is nothing to interpolate. ## Consequences ### Positive -- **Zero skill/agent duplication**: skills and agents maintained in one place. -- **Low per-host cost**: adding a new host requires only a manifest (~10 lines), a hooks config (~30 lines), and an MCP config (~5 lines). Codex was the first real test — port took ~1 dev-day for scaffolding plus tests. +- **Zero skill/agent duplication**: skills and agents maintained in one place; per-host format variants are copies held byte-identical by tests, not forks. +- **Low per-host cost**: adding a new host requires only a manifest (~10-15 lines) and a hooks config (~30-40 lines), plus an MCP config where the host can run one. Codex was the first real test (~1 dev-day for scaffolding plus tests); Copilot confirmed the estimate. - **Decoupled CLI lifecycle**: the Archcore CLI ships and patches on its own cadence; plugin releases never gate CLI security fixes (and vice versa). `archcore update` is the user-facing upgrade path. - **Standard compliance**: uses Agent Skills, MCP, and markdown agents — all open standards. - **Single source of truth**: bug fixes in skills/agents/bin propagate to all hosts automatically; the plugin lives in exactly one place (`plugins/archcore/`), with no per-host copy or symlink to keep in sync. @@ -134,6 +144,6 @@ This is the canonical Cursor 2.5+ way to register a plugin MCP. Rejected because - **CLI install is the user's responsibility**: an unsupported user expectation (e.g., "the plugin should just work") surfaces when `archcore` is missing from PATH. Mitigation: `bin/session-start` prints the install command and a docs link on every fresh session where the CLI is absent; `plugin-development.guide` documents the MCP session-start lifecycle gotcha (installing the CLI mid-session does not reconnect a Claude Code MCP that failed to register at session start — restart required). - **Stdin normalization complexity**: hook scripts must handle multiple JSON formats. Mitigated by the centralized normalizer. -- **Hook event mapping is imperfect**: not all hosts have equivalent hook events (e.g., Cursor has no direct `SessionStart` equivalent; its PreToolUse matcher is `Write` only, not `Write|Edit`). Mitigation: use the closest available event per host; document gaps. -- **Cursor MCP is user-installed only**: Cursor users have a one-time copy step from `docs/cursor.mcp.example.json` into `~/.cursor/mcp.json`. This is a deliberate trade-off (see `cursor-mcp-architecture.adr.md`) — shipping a plugin-MCP would leak the plugin's bundled `.archcore/` into every Cursor install. Revisit when Cursor's plugin-MCP cwd handling improves. -- **Subagent format divergence**: Claude Code and Cursor read MD agents with YAML frontmatter; Codex requires TOML. Mitigated by shipping both formats side-by-side; `test/structure/agents.bats` enforces parity between MD and TOML `developer_instructions` bodies. +- **Hook event mapping is imperfect**: not all hosts have equivalent hook events or equivalent semantics. Cursor has no direct `SessionStart` equivalent and its PreToolUse matcher is `Write` only, not `Write|Edit`; Copilot's `postToolUse` carries no matcher at all (the scripts self-filter there) and its `preToolUse` timeout fails **open**, which turns guard latency into a correctness concern rather than a comfort one. Mitigation: use the closest available event per host, document gaps, and keep the PreToolUse guards far inside their budget (`test/unit/hook-latency.bats`). +- **On two hosts the plugin is not self-contained**: Cursor and Copilot users get MCP only after host wiring runs (`archcore init --agent `). Deliberate — see the two MCP ADRs — and the reason `host-wiring-parity.adr` treats wiring as part of init rather than an extra. +- **Subagent format divergence**: Claude Code and Cursor read MD agents with YAML frontmatter; Codex requires TOML; Copilot requires the `*.agent.md` extension and therefore its own directory, since `.agent.md` would otherwise be picked up as a duplicate by the MD-globbing hosts. Mitigated by shipping the variants side by side, with `test/structure/agents.bats` enforcing parity between MD and TOML bodies and byte-identity between MD and `*.agent.md`. diff --git a/.archcore/plugin/plugin-architecture.spec.md b/.archcore/plugin/plugin-architecture.spec.md index a96c811..b2fc2dc 100644 --- a/.archcore/plugin/plugin-architecture.spec.md +++ b/.archcore/plugin/plugin-architecture.spec.md @@ -9,7 +9,7 @@ tags: ## Purpose -Define how the Archcore Plugin's components — intent skills, agents, hooks, and MCP server — compose into a unified multi-host system (Claude Code, Cursor, Codex CLI). This specification describes the runtime model, invocation pathways, data flow, component interactions, and the architectural invariants that hold everything together. +Define how the Archcore Plugin's components — intent skills, agents, hooks, and MCP server — compose into a unified multi-host system (Claude Code, Cursor, Codex CLI, GitHub Copilot CLI). This specification describes the runtime model, invocation pathways, data flow, component interactions, and the architectural invariants that hold everything together. Individual component contracts are defined in dedicated specs (`skills-system.spec`, `commands-system.spec`, `agent-system.spec`, `hooks-validation-system.spec`). This document is the overarching architecture that explains *how they work together*. @@ -31,7 +31,7 @@ The plugin makes Archcore effortless. It exposes a flat surface of **7 auto-invo ``` ┌─────────────────────────────────────────────────────────────────┐ -│ User / Claude Model │ +│ User / Host Model │ │ │ │ "plan this feature" "record decision" "/archcore:capture" │ └──────┬──────────────────────┬──────────────────────┬────────────┘ @@ -48,7 +48,7 @@ The plugin makes Archcore effortless. It exposes a flat surface of **7 auto-invo │ → Inline per-type creation recipes │ │ → Per-flow references under skills//references/ │ │ → Per-mode lib under skills/audit/lib/ │ -│ → Auto-invocable in Claude Code, Cursor, Codex CLI │ +│ → Auto-invocable on all four hosts │ ├─────────────────────────────────────────────────────────────────┤ │ MCP PRIMITIVES (INFRASTRUCTURE) │ │ │ @@ -89,13 +89,13 @@ Other component types: | Per-mode lib | 1 | inside `audit` | `skills/audit/lib/drift-detection.md` | | Continuation references | 1 | inside `decide` | `skills/decide/references/continuations.md` | | Shared assets | 2 | shared | `skills/_shared/{precision-rules,adr-contract}.md` | -| Agents | 2 | cross-cutting | `agents/archcore-{assistant,auditor}.{md,toml}` | -| Hooks | 6 entries | cross-cutting | `hooks/{hooks,cursor.hooks,codex.hooks}.json` | +| Agents | 2 | cross-cutting | `agents/archcore-{assistant,auditor}.{md,toml}` + `copilot-agents/archcore-{assistant,auditor}.agent.md` | +| Hooks | 6 entries | cross-cutting | `hooks/{hooks,cursor.hooks,codex.hooks,copilot.hooks}.json` | | Bin scripts | 7 | cross-cutting | `bin/{session-start,check-archcore-write,check-code-alignment,validate-archcore,check-staleness,check-cascade,check-precision}` | | MCP server | 1 | infra | Provided by archcore CLI | -| Codex command wrappers | 7 | host adapter | `commands/.md` | +| Command wrappers | 7 | host adapter | `commands/.md` (Codex requires them; Copilot loads them behind its skills) | -Total skills on disk: **7**. All are visible in the `/` menu of every supported host. +Total skills on disk: **7**. All are visible in the `/` menu of every supported host — directly on Claude Code, Cursor and Copilot, and through the `commands/` wrappers on Codex. ## Contract Surface @@ -121,11 +121,11 @@ Example: User says "show me the docs dashboard" → model auto-invokes `audit` #### Path 2: Agent Delegation (complex tasks) ``` -User request or Claude judgment → Agent spawned → +User request or model judgment → Agent spawned → Agent uses MCP tools directly → Hooks validate ``` -Trigger: Claude decides the task is complex enough for a subagent, or user explicitly requests agent help. +Trigger: The host decides the task is complex enough for a subagent, or the user explicitly requests agent help. The agent definition the host loads depends on its loader: `.md` (Claude Code, Cursor), `.toml` (Codex), `*.agent.md` from `copilot-agents/` (Copilot) — same content, three containers (`agent-system.spec.md`). #### Path 3: Direct MCP (any document type) @@ -136,6 +136,8 @@ Model or user invokes mcp__archcore__create_document(type=, ...) → Trigger: The model decides to create a document type that matches user intent, or the user calls MCP directly. MCP accepts every Archcore document type; no skill is required. +Where the MCP server comes from differs by host: plugin-shipped on Claude Code and Codex, project-registered on Cursor and Copilot (`cursor-mcp-architecture.adr`, `copilot-mcp-architecture.adr`). The tool names differ with it — `mcp__archcore__*`, `mcp__plugin_archcore_archcore__*`, or Copilot's flat `archcore-` — which is why every matcher and allow-list carries all three and the normalizer folds them to one. + #### Path 4: Staleness Detection (freshness pathway) ``` @@ -170,18 +172,20 @@ The `plan` skill demonstrates the per-flow reference pattern: ### Data Flow: Direct Write Interception -When Claude attempts to Write/Edit a `.archcore/*.md` file directly: +When the model attempts to Write/Edit a `.archcore/*.md` file directly: ``` -1. Claude calls Write/Edit with .archcore/ path +1. Model calls Write/Edit with .archcore/ path 2. [PreToolUse] bin/check-archcore-write - ├──→ Extracts file_path from stdin JSON + ├──→ Extracts the target path from stdin JSON ├──→ Matches .archcore/**/*.md pattern - ├──→ Exit 2 + stderr message → BLOCKED - └──→ Claude receives feedback: "Use MCP tools instead" -3. Claude retries via create_document or update_document + ├──→ Denies via the host's mechanism → BLOCKED + └──→ Model receives feedback: "Use MCP tools instead" +3. Model retries via create_document or update_document ``` +The deny mechanism is the one architectural detail that cannot be host-neutral: exit 2 + stderr on Claude Code, Codex and Cursor; on Copilot exit 2 is only a *warning*, so the guard writes `{"permissionDecision":"deny",…}` to stdout instead. A guard that reports a block the host ignores is a guard that does not block — the translation table lives in `host-adapter-contract.spec.md`. + Note: PreToolUse blocks the write BEFORE it happens, so PostToolUse never fires for blocked `.archcore/*.md` writes. There is no PostToolUse `Write|Edit` validate-archcore entry — it would be dead weight forking a shell on every write anywhere in the repo. Validation runs only on the MCP path. ### Data Flow: Staleness Detection @@ -252,20 +256,22 @@ Agents are an escalation path, not the primary interface. Most documentation tas #### Agent tool boundaries -Both agents are restricted: no Write, Edit, or Bash on `.archcore/` files. The assistant gets all MCP tools + Read/Grep/Glob. The auditor gets only read MCP tools + Read/Grep/Glob. +Both agents are restricted: no Write, Edit, or Bash on `.archcore/` files. The assistant gets all MCP tools + Read/Grep/Glob. The auditor gets only read MCP tools + Read/Grep/Glob. Every entry appears under all three MCP namings, because a deny-list that misses one fails open. ### Hook Enforcement Layer Hooks form a cross-cutting layer that enforces architectural invariants and detects documentation staleness regardless of which path initiated the operation. -| Hook | Event (Claude Code) | Event (Cursor) | Event (Codex) | Purpose | -|---|---|---|---|---| -| session-start | SessionStart | sessionStart | SessionStart | Load `.archcore/` context, check CLI, detect code-doc drift | -| check-archcore-write | PreToolUse (`Write\|Edit`) | preToolUse (`Write`) | PreToolUse (`Write\|Edit\|apply_patch`) | Block direct `.archcore/*.md` writes | -| check-code-alignment | PreToolUse (`Write\|Edit`) | preToolUse (`Write`) | PreToolUse (`Write\|Edit\|apply_patch`) | Inject relevant `.archcore/` context for source-file edits | -| validate-archcore | PostToolUse (MCP mutations) | afterMCPExecution | PostToolUse (MCP mutations) | Primary validation after MCP mutations | -| check-cascade | PostToolUse (`update_document`) | afterMCPExecution (filtered) | PostToolUse (`update_document`) | Cascade staleness detection via relation graph | -| check-precision | PostToolUse (`create_document\|update_document`) | afterMCPExecution (filtered) | PostToolUse (`create_document\|update_document`) | Forbidden vagueness + section + stub-length warnings | +| Hook | Event (Claude Code) | Event (Cursor) | Event (Codex) | Event (Copilot) | Purpose | +|---|---|---|---|---|---| +| session-start | SessionStart | sessionStart | SessionStart | sessionStart | Load `.archcore/` context, check CLI, detect code-doc drift | +| check-archcore-write | PreToolUse (`Write\|Edit`) | preToolUse (`Write`) | PreToolUse (`Write\|Edit\|apply_patch`) | preToolUse (`create\|edit\|str_replace_editor\|apply_patch`) | Block direct `.archcore/*.md` writes | +| check-code-alignment | PreToolUse (`Write\|Edit`) | preToolUse (`Write`) | PreToolUse (`Write\|Edit\|apply_patch`) | preToolUse (same matcher) | Inject relevant `.archcore/` context for source-file edits | +| validate-archcore | PostToolUse (MCP mutations) | afterMCPExecution | PostToolUse (MCP mutations) | postToolUse (no matcher — script self-filters) | Primary validation after MCP mutations | +| check-cascade | PostToolUse (`update_document`) | afterMCPExecution (filtered) | PostToolUse (`update_document`) | postToolUse (no matcher — script self-filters) | Cascade staleness detection via relation graph | +| check-precision | PostToolUse (`create_document\|update_document`) | afterMCPExecution (filtered) | PostToolUse (`create_document\|update_document`) | postToolUse (no matcher — script self-filters) | Forbidden vagueness + section + stub-length warnings | + +Two properties of this matrix are load-bearing. First, where a host offers no matcher, selection moves into the script — the *set* of tool calls that trigger a behavior must not differ by host, only the mechanism that selects them. Second, Copilot's entries are structurally different (flat objects, `bash` instead of `command`, `timeoutSec` instead of `timeout`), so a config or a test produced by copying another host's row will load cleanly and do nothing. ### Cross-Layer Interaction Patterns @@ -283,9 +289,9 @@ Example: `/archcore:decide` with a finalized decision → inline ADR recipe → #### Pattern 3: Model → Auto-Routed Skill → MCP → Hook -Claude auto-activates a skill from conversation context. +The host's model auto-activates a skill from conversation context. -Example: User discusses a decision → Claude activates `decide` → routes to ADR creation via MCP. +Example: User discusses a decision → the model activates `decide` → routes to ADR creation via MCP. #### Pattern 4: Agent → MCP → Hook (complex flow) @@ -293,7 +299,7 @@ An agent makes multiple MCP calls autonomously. Hooks validate each one. #### Pattern 5: Write → Hook → Block → MCP (correction flow) -When any component attempts a direct `.archcore/` write, PreToolUse blocks it. Claude retries via MCP. +When any component attempts a direct `.archcore/` write, PreToolUse blocks it and the model retries via MCP. #### Pattern 6: Update → Hook → Cascade Warning (freshness flow) @@ -309,22 +315,25 @@ Example: User updates a PRD → check-cascade fires → finds plan that `impleme - Skills MUST default to minimum viable path, offering expansion via one scope question. - Skills that create documents MUST be self-contained with inline creation recipes (or load a per-flow reference). The plan skill MUST hold per-flow logic in `skills/plan/references/.md`. - Skills MUST NOT instruct direct file writes to `.archcore/`. They reference MCP tools by exact name. +- Skills MUST NOT branch on the host; host differences live in `bin/` and in per-host configuration (`host-adapter-contract.spec`). - Agents MUST use MCP tools exclusively for `.archcore/` operations. - Hooks MUST fire for every relevant tool call, regardless of which path initiated it. -- The PreToolUse hook MUST block `.archcore/**/*.md` writes with exit code 2. +- The PreToolUse hook MUST block `.archcore/**/*.md` writes using the deny mechanism the host honors. - PostToolUse validation hooks MUST run `archcore doctor` after every MCP document mutation. - PostToolUse cascade hook MUST run after `update_document` to detect relation-graph staleness. - PostToolUse precision hook MUST run after `create_document` and `update_document`. - No PostToolUse hook MUST be registered for `Write|Edit` — PreToolUse already blocks `.archcore/*.md` writes before they succeed. - SessionStart MUST include staleness check after context loading. +- A behavior's trigger set MUST be identical across hosts; only the mechanism that selects it (matcher or in-script filter) may differ. ## Constraints - Exactly 7 visible intent skills. Adding an eighth requires a new ADR. - Maximum 2 agents. New agents require an ADR. -- Hooks must complete within their timeout (PreToolUse: 1s, PostToolUse: 3s). +- Hooks must complete within their timeout (PreToolUse: 1s, PostToolUse: 3s), with enough margin that a host whose pre-mutation timeout fails open never reaches it. - Skill files must not exceed 300 lines. - Per-flow reference files must not exceed 200 lines. +- A new host costs a manifest, a hooks config, a normalizer case, and enrollment in the coverage matrix — no changes to skills, agents, or `bin/` logic. ## Invariants @@ -333,21 +342,22 @@ Example: User updates a PRD → check-cascade fires → finds plan that `impleme - Every MCP mutation triggers PostToolUse validation. - Every `update_document` triggers cascade detection in addition to validation. - Every `create_document` and `update_document` triggers precision check. -- Every direct `.archcore/*.md` write attempt is blocked by PreToolUse. +- Every direct `.archcore/*.md` write attempt is blocked by PreToolUse on every host that supports pre-mutation hooks. - Every session starts with project context loaded and staleness check run (or a warning if the CLI is missing). - Skills inline per-type elicitation; this duplication is intentional and accepted per `skills-system.spec.md` to keep each entry point self-contained. - Agents never have Write/Edit/Bash access to `.archcore/` files. - No skill carries `disable-model-invocation`. Every skill is auto-invocable. - Staleness detection never modifies documents autonomously — only `/archcore:audit --drift` modifies, and only with user confirmation. - Every Archcore document type is reachable through at least one intent skill (or directly via MCP). +- Skill and agent content is byte-identical across hosts; only the container format differs. ## Error Handling -- **MCP server unavailable**: All skills inform the user with install/init instructions. Hooks degrade gracefully. +- **MCP server unavailable**: All skills inform the user with install/init instructions. Hooks degrade gracefully. On Cursor and Copilot this is also the expected pre-wiring state — the fix is `archcore init --agent `, not a plugin reinstall. - **Duplicate document**: `create_document` fails. Skills suggest alternative filename. - **Intent routing ambiguous**: Skill asks one scope-confirmation question. If still ambiguous, falls back to the `capture` skill (most general). - **Flow interrupted mid-cascade**: `plan` skill detects existing documents via `list_documents` and resumes from the next step. -- **Hook timeout**: PostToolUse fails open. PreToolUse fail-closed behavior handled by the host. +- **Hook timeout**: PostToolUse fails open. PreToolUse behavior is the host's: fail-closed on Claude Code, Codex and Cursor; fail-**open** on Copilot, which is why both PreToolUse guards are held far inside budget rather than trusted to the host. - **Agent exceeds turn limit**: Agent returns partial results. User can re-invoke or continue manually. - **Git unavailable for staleness**: SessionStart skips staleness check. `/archcore:audit --drift` skips code-drift analysis but performs cascade and temporal. @@ -357,7 +367,7 @@ The plugin architecture conforms to this specification if: 1. All document operations flow through MCP tools. 2. The skill surface is exactly 7 skills: `init`, `capture`, `decide`, `plan`, `audit`, `context`, `help`. All are auto-invocable (no `disable-model-invocation`). -3. PreToolUse hook blocks 100% of direct `.archcore/**/*.md` writes. +3. PreToolUse hook blocks 100% of direct `.archcore/**/*.md` writes, on every host, through that host's honored deny mechanism. 4. PostToolUse validation fires after every MCP document mutation. 5. PostToolUse cascade detection fires after every `update_document`. 6. PostToolUse precision check fires after every `create_document` and `update_document`. @@ -367,3 +377,4 @@ The plugin architecture conforms to this specification if: 10. Drift-mode logic for `audit` lives under `skills/audit/lib/drift-detection.md`. 11. Continuation logic for `decide` (ADR → CPAT → rule → guide) lives under `skills/decide/references/continuations.md`. 12. Every Archcore document type is reachable through at least one intent skill. +13. The event matrix above lists every implemented host, and each host's row is backed by a row in `test/structure/host-coverage-matrix.bats`. diff --git a/.archcore/plugin/plugin-development.guide.md b/.archcore/plugin/plugin-development.guide.md index 149aaaa..87f8d0a 100644 --- a/.archcore/plugin/plugin-development.guide.md +++ b/.archcore/plugin/plugin-development.guide.md @@ -8,20 +8,24 @@ tags: ## Prerequisites -- Claude Code, Cursor, or Codex CLI installed with plugin support +- Claude Code, Cursor, Codex CLI, or GitHub Copilot CLI installed with plugin support - Git for version control - bats-core for tests (`brew install bats-core` on macOS) - jq for JSON validation (`brew install jq`) - ShellCheck (optional, `brew install shellcheck`) - **Archcore CLI** installed globally via the official installer at https://docs.archcore.ai/cli/install/ — `curl -fsSL https://archcore.ai/install.sh | bash` (macOS/Linux/WSL) or `irm https://archcore.ai/install.ps1 | iex` (Windows PowerShell 5.1+). Verify with `archcore --version`. -That's it. The plugin does not bundle a launcher — it assumes users have the Archcore CLI installed globally on PATH. MCP is registered automatically for Claude Code via plugin-root `.mcp.json`, and for Codex CLI via `.codex-plugin/plugin.json` pointing at plugin-root `.codex.mcp.json`. Both `.mcp.json` and `.codex.mcp.json` simply name `archcore` as the command — host runtimes resolve it via PATH. +That's it. The plugin does not bundle a launcher — it assumes users have the Archcore CLI installed globally on PATH. MCP is registered automatically for Claude Code via plugin-root `.mcp.json`, and for Codex CLI via `.codex-plugin/plugin.json` pointing at plugin-root `.codex.mcp.json`. Both simply name `archcore` as the command — host runtimes resolve it via PATH. -For Cursor development, you register MCP externally by copying `docs/cursor.mcp.example.json` into `~/.cursor/mcp.json` (user-scoped) or `.cursor/mcp.json` (project-scoped). The plugin deliberately does **not** ship a Cursor plugin-MCP — see `cursor-mcp-architecture.adr.md` for the three-layer rationale (Cursor 2.5+ spawns plugin-MCPs from the plugin install dir rather than the workspace, and its MCP stdio schema has no `cwd` field). The template passes `--project ${workspaceFolder}` in `args` so the server always resolves the workspace, regardless of how Cursor invokes it. +**Two hosts get no plugin-shipped MCP, deliberately.** Cursor and GitHub Copilot CLI each launch a plugin's MCP child outside the user's project, so a plugin-shipped server would read and write the wrong tree. -For Codex development, `codex plugin marketplace add /path/to/plugin` registers the marketplace. The current CLI loads enabled plugins from its installed plugin cache; run `make test-codex-smoke` for the local installed-cache smoke that verifies skill discovery and plugin-managed MCP. +For Cursor development, register MCP externally by copying `docs/cursor.mcp.example.json` into `~/.cursor/mcp.json` (user-scoped) or `.cursor/mcp.json` (project-scoped). See `cursor-mcp-architecture.adr.md` for the three-layer rationale (Cursor 2.5+ spawns plugin-MCPs from the plugin install dir rather than the workspace, and its MCP stdio schema has no `cwd` field). The template passes `--project ${workspaceFolder}` in `args` so the server always resolves the workspace, regardless of how Cursor invokes it. -Initialize a project for testing with `mcp__archcore__init_project` (via a Claude Code or Cursor session) rather than an out-of-band CLI command; the plugin routes initialization through MCP. +For Copilot development, run `archcore init --agent copilot --project "$PWD"` in the test project (CLI ≥ v0.6.4). That writes the workspace-root `.mcp.json` Copilot CLI actually reads. See `copilot-mcp-architecture.adr.md`: Copilot launches a plugin's MCP child in the plugin install directory and passes it no project path at all (github/copilot-cli#4234), so documents would land in `~/.copilot/installed-plugins/` while every tool reported success. + +For Codex development, `codex plugin marketplace add /path/to/plugin` registers the marketplace. The current CLI loads enabled plugins from its installed plugin cache; run `make test-codex-smoke` for the local installed-cache smoke that verifies skill discovery and plugin-managed MCP. `make test-copilot-smoke` is the equivalent for Copilot. + +Initialize a project for testing with `mcp__archcore__init_project` (via any host session) rather than an out-of-band CLI command; the plugin routes initialization through MCP. ## Steps @@ -39,11 +43,14 @@ The plugin uses a `dev → main` split: all PRs land on `dev`. The `main` branch ### 2. Run the host with the plugin loaded locally ```bash -claude --plugin-dir . # Claude Code -cursor --plugin-dir . # Cursor +claude --plugin-dir plugins/archcore # Claude Code +cursor --plugin-dir plugins/archcore # Cursor +copilot --plugin-dir plugins/archcore # GitHub Copilot CLI ``` -This loads the plugin from the current directory without requiring marketplace installation. Changes to plugin files are picked up after running `/reload-plugins` inside the session. +Codex has no `--plugin-dir`; use a local marketplace (`codex plugin marketplace add /path/to/plugin`). + +This loads the plugin without requiring marketplace installation. Changes to plugin files are picked up after running `/reload-plugins` inside the session. ### 3. Modify an existing skill @@ -53,9 +60,9 @@ Edit `skills//SKILL.md`. Required frontmatter fields: `name` (must match d Reload and test: `/reload-plugins`, then try `/archcore:`. -#### 3a. Add a Codex slash command wrapper (required for user-facing skills) +#### 3a. Add a slash command wrapper (required for user-facing skills) -Claude Code and Cursor surface skills directly in the `/` menu. Codex CLI does not — it discovers slash commands from root-level `commands/.md` files. The plugin ships 7 wrappers, one per skill. If you ever add a new top-level skill (requires a new ADR), add the matching wrapper: +Claude Code, Cursor and Copilot surface skills directly in the `/` menu. Codex CLI does not — it discovers slash commands from `commands/.md` files. The plugin ships 7 wrappers, one per skill. If you ever add a new top-level skill (requires a new ADR), add the matching wrapper: ```markdown --- @@ -73,11 +80,13 @@ The user invoked this command with: $ARGUMENTS Use the Archcore skill at `skills/my-skill/SKILL.md`. ``` -Wrappers carry no workflow logic — behavior lives in the skill, the single source of truth. `test/structure/codex-plugin.bats` enforces parity: every wrapper must exist, carry `description:`, and reference its matching `skills//SKILL.md`. +Wrappers carry no workflow logic — behavior lives in the skill, the single source of truth. `test/structure/codex-plugin.bats` and `copilot-plugin.bats` enforce parity: every wrapper must exist, carry `description:`, and reference its matching `skills//SKILL.md`. + +Copilot loads the same wrappers, but only because `.plugin/plugin.json` names `./commands/` explicitly — that field has no default path on Copilot. A skill outranks a command of the same name there, so the wrappers are a fallback surface rather than the primary one; the pointer still has to be present, and a structure test keeps it there. ### 4. Add or modify hooks -Edit `hooks/hooks.json` (Claude Code), `hooks/cursor.hooks.json` (Cursor), or `hooks/codex.hooks.json` (Codex CLI) to add event handlers. +Edit `hooks/hooks.json` (Claude Code), `hooks/cursor.hooks.json` (Cursor), `hooks/codex.hooks.json` (Codex CLI), or `hooks/copilot.hooks.json` (GitHub Copilot CLI) to add event handlers. Every hooks config must also be enrolled in `test/structure/host-coverage-matrix.bats` and in the resolution table in `hooks.bats`; both have enrollment guards that fail until it is. Hook scripts go in `bin/` and must: @@ -86,24 +95,33 @@ Hook scripts go in `bin/` and must: - Source `bin/lib/normalize-stdin.sh` if they read hook stdin - Add `# shellcheck source=lib/normalize-stdin.sh` before the source line - Invoke the CLI directly as `archcore` (resolved via PATH); the plugin no longer ships a launcher wrapper -- If the script reads `.archcore/` or emits user-visible context, guard against being launched from a plugin install directory by exiting silently when cwd contains a sibling `.cursor-plugin/`, `.claude-plugin/`, or `.codex-plugin/` manifest (see `bin/session-start` for the canonical pattern, and `cursor-mcp-architecture.adr.md` for the rationale) +- If the script reads `.archcore/` or emits user-visible context, guard against being launched from a plugin install directory by exiting silently when cwd contains — or sits beneath — a `.cursor-plugin/`, `.claude-plugin/`, `.codex-plugin/`, or `.plugin/` manifest (see `bin/session-start` for the canonical pattern, and `cursor-mcp-architecture.adr.md` for the rationale) Each host's hook config uses its host's canonical plugin-root env var: - `${CLAUDE_PLUGIN_ROOT}` — Claude Code's native injection (`hooks/hooks.json`). - `${CURSOR_PLUGIN_ROOT}` — Cursor's native injection (`hooks/cursor.hooks.json`). - `${PLUGIN_ROOT}` — Codex CLI's canonical, host-neutral env var (`hooks/codex.hooks.json`). Codex's hooks engine (`codex-rs/hooks/src/engine/discovery.rs`) injects `PLUGIN_ROOT` as the canonical name; `CLAUDE_PLUGIN_ROOT` is also injected but only as a backward-compat alias for porting old Claude plugins — do NOT use it in a Codex-native hook config. `CODEX_PLUGIN_ROOT` does not exist in Codex. +- `${COPILOT_PLUGIN_ROOT}` — Copilot CLI's injection (`hooks/copilot.hooks.json`). It exists **only inside hook processes**, which is why `bin/detect-host` cannot key on it and why a Copilot session resolves to `__UNKNOWN__` there. + +Copilot's config differs from the others in shape, not just in names: entries use `bash` rather than `command`, `timeoutSec` rather than `timeout`, are flat objects rather than nested groups, carry `cwd: "."` so the hook runs from the user's project, and its `postToolUse` entries have no matcher at all — the scripts self-filter. A test written by copying another host's and swapping the filename will iterate an empty set and report `ok`. Plugin-shipped Codex hooks require `codex features enable plugin_hooks` to actually fire (the `plugin_hooks` feature is `under development, false` by default in Codex 0.130.0). See `codex-path-resolution.adr.md` for the full mechanism. +Two Copilot hook semantics differ from every other host and both are load-bearing: `exit 2` is only a **warning** there, so a deny must be written to stdout as `{"permissionDecision":"deny","permissionDecisionReason":…}`; and a `preToolUse` **timeout fails open**, which makes guard latency a correctness concern rather than a comfort one. `test/unit/hook-latency.bats` keeps both PreToolUse guards far inside the 1-second budget for that reason. + ### 5. Modify agents Edit `agents/archcore-assistant.md` or `agents/archcore-auditor.md`: - Frontmatter: `name`, `description`, `model`, `maxTurns`, `tools` - The auditor must remain read-only (only list_documents, get_document, list_relations MCP tools) +- Tool lists carry every MCP naming: `mcp__archcore__*`, `mcp__plugin_archcore_archcore__*`, and Copilot's flat `archcore-` + +Then propagate to the two format variants, both checked by `test/structure/agents.bats`: -For Codex CLI, also update the matching TOML variant (`agents/archcore-assistant.toml`, `agents/archcore-auditor.toml`) — TOML and MD must keep identical `developer_instructions` content; structural drift is detected by `test/structure/agents.bats`. +- **Codex** — `agents/.toml`; TOML and MD must keep identical `developer_instructions` content. +- **Copilot** — `copilot-agents/.agent.md`; a byte-identical copy (`cmp`), because Copilot's loader accepts only the `*.agent.md` extension. Keep it in `copilot-agents/`, never beside the original: `.agent.md` still matches the `*.md` glob Claude Code and Cursor use, so a sibling copy would hand both hosts two files declaring the same `name:`. ### 6. Run tests @@ -116,12 +134,14 @@ make verify # full check: JSON + permissions + shellcheck + tests Or run individual checks: ```bash -make test # all bats tests -make test-unit # unit tests (bin script logic) -make test-structure # structure tests (configs, frontmatter) -make lint # shellcheck -make check-json # JSON validity -make check-perms # executable permissions +make test # all bats tests +make test-unit # unit tests (bin script logic) +make test-structure # structure tests (configs, frontmatter) +make test-codex-smoke # install smoke, skips without the codex CLI +make test-copilot-smoke # install smoke, skips without the copilot CLI +make lint # shellcheck +make check-json # JSON validity +make check-perms # executable permissions ``` `make verify` is the canonical way to run plugin integrity checks; there is no `/archcore:verify` skill (removed by `skill-surface-collapse.adr.md`). @@ -130,15 +150,18 @@ See `plugin-testing.guide.md` for detailed testing instructions. ### 7. Test all components manually -- Skills: discuss relevant topics and verify Claude activates the skill -- Commands: run each `/archcore:` command (in all three hosts where applicable) and verify behavior — Codex pulls these from `commands/`, Claude Code and Cursor pull them from `skills/` +- Skills: discuss relevant topics and verify the host activates the skill +- Commands: run each `/archcore:` command on every host you can reach and verify behavior — Codex pulls these from `commands/`, Claude Code, Cursor and Copilot from `skills/` - Agent: invoke the agent on a multi-document task -- Hooks: trigger Write/Edit on `.archcore/` and verify PreToolUse blocks it +- Hooks: trigger Write/Edit on `.archcore/` and verify the pre-mutation guard blocks it - MCP availability: ensure `archcore` is on PATH and `archcore --version` works - For Codex: from a directory **outside** the plugin source repo (e.g., `cd $(mktemp -d)`), call any `mcp__archcore__*` tool and verify the MCP starts. - For Cursor: after copying `docs/cursor.mcp.example.json` into `.cursor/mcp.json`, open an empty project. `list_documents` should return empty (not the plugin's own dev docs). If it returns dev docs, the plugin-install-dir guards regressed — file an issue against this repo and `archcore-ai/cli`. +- For Copilot: `copilot mcp list` must NOT show an `archcore` server contributed by the plugin. If it does, the manifest regressed or the host auto-discovers plugin-root `.mcp.json` — either way, capture it, because that question is unresolved from GitHub's own documentation (see `copilot-mcp-architecture.adr.md`). - Integrity check: `make verify` +For the questions no manual checklist can settle — whether a deny is honored or merely displayed, whether pre-mutation hooks fire on delegated calls — follow `host-probe-protocol.spec.md` and record the result. + ## Verification - `make verify` exits 0 with "All checks passed" @@ -153,30 +176,36 @@ See `plugin-testing.guide.md` for detailed testing instructions. ### Plugin not loading -- Ensure `.claude-plugin/plugin.json` (Claude Code), `.cursor-plugin/plugin.json` (Cursor), or `.codex-plugin/plugin.json` (Codex CLI) exists and has valid JSON -- Check that directories (skills/, agents/, hooks/, commands/) are at the plugin root +- Ensure the manifest for your host exists and has valid JSON: `.claude-plugin/plugin.json` (Claude Code), `.cursor-plugin/plugin.json` (Cursor), `.codex-plugin/plugin.json` (Codex CLI), `.plugin/plugin.json` (GitHub Copilot CLI) +- Check that directories (skills/, agents/, copilot-agents/, hooks/, commands/) are at the plugin root - Run `claude --debug` to see plugin loading details ### Skill not activating -- Check the `description` field in SKILL.md frontmatter — it determines when Claude activates the skill +- Check the `description` field in SKILL.md frontmatter — it determines when the host activates the skill - Ensure `name` matches the directory name - Run `/reload-plugins` after changes -### `/archcore:` missing in Codex `/` menu +### `/archcore:` missing in the Codex `/` menu - Confirm `commands/.md` exists and has `description:` frontmatter - Confirm it references `skills//SKILL.md` (the bats parity test enforces this) - Run `make test-structure` — `codex-plugin.bats` will flag missing or malformed wrappers - Restart Codex after adding new wrappers (the marketplace cache is read once on session start) +### Agents missing in Copilot + +- Confirm the file is `copilot-agents/.agent.md`. Copilot derives the agent id from the filename and loads only that extension — a plain `.md` is invisible to it, which is exactly why the copies exist. +- Confirm `.plugin/plugin.json` has `"agents": "./copilot-agents/"`. The field defaults to `agents/`, so an omitted pointer silently looks in the directory that holds no `*.agent.md` files at all. + ### Hook not firing - Ensure bin/ scripts are executable: `chmod +x bin/` - Check the shebang line: `#!/bin/sh` -- Verify the hook JSON structure matches the expected format +- Verify the hook JSON structure matches the expected format for that host - Test scripts manually: `echo '{"tool_name":"Write","tool_input":{"file_path":".archcore/test.adr.md"}}' | bin/check-archcore-write` - For Codex specifically: hooks require `codex features enable plugin_hooks` (the `plugin_hooks` feature is under development; absent the flag, Codex does not run plugin-shipped hooks) +- For Copilot specifically: check the entry uses `bash`, not `command`, and `timeoutSec`, not `timeout` — a config written in Claude's shape loads without error and does nothing ### Tests failing @@ -193,9 +222,15 @@ The plugin ships `.mcp.json` for Claude Code and `.codex.mcp.json` for Codex CLI 2. **CLI available?** — run `archcore --version` from the terminal. Expected: prints a version. - Not found? → Install via the official installer: `curl -fsSL https://archcore.ai/install.sh | bash` (macOS/Linux/WSL) or `irm https://archcore.ai/install.ps1 | iex` (Windows). Full docs: https://docs.archcore.ai/cli/install/ - Permission denied? → Check that the CLI binary is executable -3. **Session lifecycle** — Claude Code registers MCP servers at session start. If the CLI was missing at that moment, installing it mid-session will NOT reconnect the server. Restart the host (Claude Code / Codex CLI) after a fresh install. +3. **Session lifecycle** — Claude Code registers MCP servers at session start. If the CLI was missing at that moment, installing it mid-session will NOT reconnect the server. Restart the host after a fresh install. 4. **Duplicate suppression?** — if `/plugin` shows "Errors (1)" with an `archcore` MCP message, a user- or project-registered `archcore` has the same command. This is benign; the resolved binary is the same either way. To silence the warning, remove the redundant user/project registration. +### No MCP tools at all (GitHub Copilot CLI) + +Expected until the project is wired — the plugin ships no MCP server for Copilot. Run `archcore init --agent copilot --project "$PWD"` (CLI ≥ v0.6.4), which writes the workspace-root `.mcp.json`, then restart the session. Copilot discovers `.mcp.json` by walking from the working directory up to the git root, so a repo-root file covers monorepo layouts. + +If tools appear but documents land somewhere unexpected, check where: github/copilot-cli#4234 puts a plugin-contributed MCP child in `~/.copilot/installed-plugins/`. A project-registered server does not have that problem, because the host launches it from the project. + ### MCP server not connecting (Cursor) Cursor uses a user-installed MCP, not a plugin-shipped one (deliberate — see `cursor-mcp-architecture.adr.md`). Copy `docs/cursor.mcp.example.json` into one of: diff --git a/.archcore/plugin/plugin-testing.guide.md b/.archcore/plugin/plugin-testing.guide.md index 1539ea7..545fb45 100644 --- a/.archcore/plugin/plugin-testing.guide.md +++ b/.archcore/plugin/plugin-testing.guide.md @@ -37,7 +37,7 @@ Runs all checks in order: JSON validation → permission check → ShellCheck make test ``` -Runs both unit and structure tests via bats-core (212 tests total as of the global-CLI cutover). +Runs both unit and structure tests via bats-core. The suite grows with the plugin, so treat the count `make test` prints as the number to compare against — not one written down here. To run a subset: @@ -49,8 +49,8 @@ make test-structure # structure tests for configs and frontmatter To run a single test file: ```bash -PLUGIN_ROOT=$(pwd) bats test/unit/normalize-stdin.bats -PLUGIN_ROOT=$(pwd) bats test/unit/validate-archcore.bats +PLUGIN_ROOT=$(pwd)/plugins/archcore REPO_ROOT=$(pwd) bats test/unit/normalize-stdin.bats +PLUGIN_ROOT=$(pwd)/plugins/archcore REPO_ROOT=$(pwd) bats test/unit/validate-archcore.bats ``` ### 3. Run ShellCheck lint @@ -68,11 +68,24 @@ make check-json # validates all JSON configs via jq make check-perms # verifies bin/ scripts are executable ``` -### 5. Plugin integrity check +### 5. Host install smoke tests + +```bash +make test-codex-smoke # requires the codex CLI on PATH +make test-copilot-smoke # requires the copilot CLI on PATH +``` + +Neither runs as part of `make test`, and both **skip** rather than fail when their host binary is absent — so they ship, stay quiet in CI, and run for free on a contributor's machine that has the host installed. They cover what static tests cannot: that a real install of the plugin produces a tree the host can actually load. That is the exact gap issue #2 fell through for Codex, where every structural test was green while marketplace discovery silently found nothing. + +The Copilot smoke test asserts filesystem facts rather than CLI output: every path the manifest names survives the install, the hook scripts keep their executable bit, and no plugin MCP server is registered (github/copilot-cli#4234). Host output wording is not a contract we control; what the plugin promises is what lands on disk. + +### 6. Plugin integrity check `make verify` is the canonical way to run plugin integrity checks. The previous `/archcore:verify` skill was retired by `skill-surface-collapse.adr.md` — use the Makefile target instead. Inside a host session, ask the model to "run make verify and report the results" if you want AI-assisted verification. -### 6. Write a new test +Two structure files carry the per-host regression guards specifically: `test/structure/host-coverage-matrix.bats` (one enrolled row per hooks config, with an enrollment guard so a new host cannot ship unchecked) and `test/structure/copilot-plugin.bats` / `codex-plugin.bats` / `cursor-plugin.bats` for the per-host manifests. + +### 7. Write a new test **Unit test** — for bin/ script logic (stdin parsing, exit codes, output): @@ -87,6 +100,7 @@ make check-perms # verifies bin/ scripts are executable 3. Use helpers from `test/helpers/common.bash`: - `run_with_fixture