docs(410): a packaging contract, so a second client is a manifest and an adapter (#4032)
CI & Build / Python lint (push) Successful in 7s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 16s
CI & Build / Python lint (push) Successful in 7s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m26s
CI & Build / Build & push image (push) Successful in 16s
Step 5 of milestone 410 "One owner per piece of guidance". The operator wants any attempt to package Scribe for another agent client to find the repo already in the right shape. plugin/PACKAGING.md (linked from the README) states: - what every client package shares: plugin/skills/ (verbatim), the /mcp endpoint and its in-band responses, the /api/plugin/* adapter endpoints (context, retrieve, prior-art, tool-rules, processes), and one fmcp_ key - what each client adds: a manifest; hooks limited to timing and transport; optional commands; adapter static text that never copies a skill or the index - the Claude Code adapter file by file, as the worked example - how Agent Plugins 1.0 clients (Codex, Cursor, Copilot/VS Code, Kiro, ChatGPT) and Gemini CLI would map, marked researched-not-tested (#4023) - four open questions for the second package: passing the key to the MCP server, whether two manifests can share one folder, hook parity, and where process skills go Hook audit: every hook prints only server-provided text, status or outage lines, the running version, or the compaction reload pointer. No guidance copies, so nothing moved. Guard: test_the_skills_reference_nothing_outside_their_folder fails on a relative path upward or a reference to plugin/, hooks/, commands/ or a manifest from inside a skill, with a companion test showing it can fail. Plugin version minted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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** | `<base URL>/mcp` | HTTP, `Authorization: Bearer <fmcp_ key>`. 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** | `<base URL>/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.<client>/`) 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.
|
||||
+2
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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") == []
|
||||
|
||||
Reference in New Issue
Block a user