diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index aa8a854..f53a334 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).", - "version": "2026.09.14.1645", + "version": "2026.09.14.1723", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/PACKAGING.md b/plugin/PACKAGING.md new file mode 100644 index 0000000..a0919f6 --- /dev/null +++ b/plugin/PACKAGING.md @@ -0,0 +1,62 @@ +# Packaging Scribe for an agent client + +This is the maintainer contract for shipping Scribe into an agent client: +what every client package shares, what each one adds, and where a new client's +files go. It follows from decision #4027 (milestone 410): **the server orients, +the skills hold the depth, and a client adapter only times delivery and says +what that one client needs said.** + +Claude Code is the only client built today. The layout below is kept so adding +another one means adding files, not moving or rewriting any. + +## Shared by every client package + +| Piece | Where | What it is | +|---|---|---| +| **The skills** | `plugin/skills/*/SKILL.md` | Agent Skills (the open SKILL.md format). They state every Scribe reflex in full and name no client. `tests/test_guidance_ownership.py` fails if a skill names a particular client, or references anything outside its own folder. Every client package ships this folder verbatim. | +| **The MCP server** | `/mcp` | HTTP, `Authorization: Bearer `. Its `_INSTRUCTIONS` is a client-neutral index (≤2,000 chars); each tool's description carries its contract; in-band responses (`placement`, `report_back`, `systems_hint`, the duplicate gate, the guessed-id refusal) fire in every client. | +| **The adapter API** | `/api/plugin/*` | Plain `GET` endpoints any client's hooks can call with the same key (read scope is enough): `context` (live session state), `retrieve` (rules, preferences and notes for a message), `prior-art` (records and shape-ledger hints for code being written), `tool-rules` (rules for a command about to run), `processes` (stored Processes to expose as skills). | +| **The API key** | Scribe → Settings → API Keys | One `fmcp_` key per install. Read scope for hooks; write scope for the MCP tools. | + +## Added by each client + +| Piece | What it holds | Rule of thumb | +|---|---|---| +| **A manifest** | Name, version, how to reach the MCP server with the key, where the skills and hooks are | Only that client's format. | +| **Hooks** | Scripts in that client's hook format that call the adapter API at the right moment and print what comes back | Timing and transport only. A hook may print status lines (unconfigured, unreachable, running version) and pointers; guidance text comes from the server or the adapter's own static text. | +| **Commands** | That client's command wrappers, e.g. a process sync | Optional. | +| **Adapter static text** | What only that client needs said: its local memory files, its compaction command, its commands | Never a copy of a skill or of `_INSTRUCTIONS`. The loss guard and ownership registry in `tests/test_guidance_ownership.py` cover it. | + +## The worked example: Claude Code + +| File | Role | +|---|---| +| `.claude-plugin/plugin.json` | Manifest: `mcpServers.scribe` (HTTP, `Authorization: Bearer ${user_config.api_token}`), `userConfig` for the base URL and key, version (minted, never hand-edited; see README). | +| `../.claude-plugin/marketplace.json` (repo root) | The marketplace entry pointing at `./plugin`. | +| `hooks/hooks.json` | Wires the scripts to Claude Code events. | +| `hooks/scribe_session_context.sh` | SessionStart: adapter static text + running version + `GET /api/plugin/context`; a reload banner after compaction. | +| `hooks/scribe_autoinject.sh` | UserPromptSubmit: `GET /api/plugin/retrieve` for the message. | +| `hooks/scribe_prior_art.sh` | PreToolUse on editor writes: `GET /api/plugin/prior-art`. | +| `hooks/scribe_after_write.sh` | PostToolUse on shell commands: the same check for code written through the shell. | +| `hooks/scribe_tool_rules.sh` | PreToolUse on shell commands: `GET /api/plugin/tool-rules`. | +| `hooks/scribe_sync_processes.sh` + `commands/sync.md` | `GET /api/plugin/processes` → `~/.claude/skills/scribe-proc-*` stubs; `/scribe:sync` on demand. | +| `hooks/scribe_defs.sh` | Shared shell helpers: config, dedup ledgers, outage line. | +| `hooks/scribe_static_context.md` | The adapter static text. | + +Hook config arrives as `CLAUDE_PLUGIN_OPTION_API_ENDPOINT` / `CLAUDE_PLUGIN_OPTION_API_TOKEN` (uppercased by Claude Code, #2198), with `SCRIBE_URL` / `SCRIBE_TOKEN` as an override. + +## How other clients would map + +**Researched, not tested** (spike #4023, September 2026). Re-check each client's current docs before building. + +- **Agent Plugins 1.0** (Codex, Cursor, GitHub Copilot / VS Code, Kiro, ChatGPT): a root `plugin.json`, a `skills/` folder, and an `mcp.json`. That is the same `plugin/skills/` plus two small files. Hooks, commands and rules are not part of v1; each client adds its own under a reverse-domain directory (e.g. `com./`) that other clients ignore. +- **Gemini CLI**: `gemini-extension.json` configuring the MCP server, a context file, and bundled skills. +- **Client-native formats also exist** (`.codex-plugin/`, `.cursor-plugin/`) where a client wants more than the shared standard carries. +- **Claude Code** does not read Agent Plugins: only `.claude-plugin/plugin.json`. + +## Open questions for whoever builds the second package + +1. **Passing the key to the MCP server.** Claude Code substitutes `${user_config.api_token}` into the header. Agent Plugins' `mcp.json` and Gemini's extension config have their own variable and secret handling. Decide per client, and keep the key out of files that get committed. +2. **Coexistence in one folder.** Whether a root `plugin.json` (Agent Plugins) beside `.claude-plugin/plugin.json` changes how Claude Code loads the plugin is untested. Test it before shipping both from `plugin/`, or give the second client its own package directory that reuses `plugin/skills/`. +3. **Hook parity.** Other clients' hook events differ and are partly implemented. Map each Scribe hook to the nearest event, and where none exists, leave that timing to the in-band server responses rather than writing a copy of the guidance. +4. **Process skills.** The Claude Code sync writes `~/.claude/skills`. Another client needs its own skills location, or the MCP skills extension (SEP-2640) once a client supports it. diff --git a/plugin/README.md b/plugin/README.md index 2d5e219..dea320e 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -31,7 +31,8 @@ holding the one copy, not by switching it off. every MCP client and serves live state; the skills in `skills/` state every reflex in full and name no client; this plugin is the Claude Code adapter — hooks that deliver at the right moment, `/scribe:sync`, and the few things only -Claude Code needs said (`hooks/scribe_static_context.md`). +Claude Code needs said (`hooks/scribe_static_context.md`). Packaging Scribe for +another client: see [PACKAGING.md](PACKAGING.md). ## Install diff --git a/tests/test_guidance_ownership.py b/tests/test_guidance_ownership.py index 28f806c..af5ad2a 100644 --- a/tests/test_guidance_ownership.py +++ b/tests/test_guidance_ownership.py @@ -251,3 +251,29 @@ def test_the_client_guard_can_fail(): assert client_specific_hits("keep a copy in CLAUDE.md") == ["claude", "claude.md"] assert client_specific_hits("edits made through Bash") == ["Bash"] assert client_specific_hits("edits made through a bash shell") == [] + + +# A skill is shipped verbatim inside every client's package, at whatever path +# that client installs skills to. A reference from a skill to anything outside +# its own folder — the adapter's hooks, a manifest, a relative path upward — +# points at a file that exists in one package layout and nowhere else +# (plugin/PACKAGING.md). +OUTSIDE_THE_SKILL = re.compile(r"\.\./|plugin/|hooks/|commands/|\.claude-plugin|\bplugin\.json\b|\bhooks\.json\b") + + +def test_the_skills_reference_nothing_outside_their_folder(): + offenders = { + str(p.relative_to(ROOT)): sorted(set(OUTSIDE_THE_SKILL.findall(p.read_text()))) + for p in sorted((ROOT / "plugin/skills").glob("*/SKILL.md")) + } + offenders = {path: refs for path, refs in offenders.items() if refs} + assert not offenders, ( + f"skills that reference files outside their own folder: {offenders}. A " + f"skill ships verbatim in every client package; see plugin/PACKAGING.md." + ) + + +def test_the_layout_guard_can_fail(): + assert OUTSIDE_THE_SKILL.findall("run ../hooks/sync.sh") == ["../", "hooks/"] + assert OUTSIDE_THE_SKILL.findall("see plugin.json") == ["plugin.json"] + assert OUTSIDE_THE_SKILL.findall("record the plugin's behaviour") == []