CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 47s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / Python tests (push) Failing after 59s
CI & Build / Build & push image (push) Skipped
Everything else Scribe gives an agent arrives before the reply is written. A
Stop hook is the one moment the finished reply exists, so it is the last
chance to fix a report the operator can't read, and the only place adherence
to the shape can be measured.
- plugin/hooks/scribe_report_check.sh (Stop): deterministic, no model call.
1. Did this turn close a task? That means an update_task/create_task call
with status "done" since the turn's prompt, whose tool_result is not an
error. Otherwise it stays silent, which covers most turns (one grep).
2. Does the reply that ends the turn say where the work sits (a record by
id and title, or step N of M), what needs the operator, and what comes
next? Matched on those words, not on exact headings.
3. If sections are missing, it blocks once. With stop_hook_active set, a
rewrite is recorded (passed_after_rewrite / missing_after_rewrite) and
never blocked again. A block loop started by another plugin (no marker
from this hook) is left alone.
- Measured: every checked reply is reported to GET /api/plugin/report-check
(passed / blocked / after rewrite). Turns that close nothing are not
reported; they would cost a request per turn and add nothing to the rate.
Outcomes go to app_logs as category "plugin", action "report_check".
- It blocks only when the block was recorded, and only in the server's words.
The endpoint returns the block reason, so the hook carries timing and
transport only (PACKAGING.md), and an unconfigured or unreachable instance
never stops a session.
- The transcript format is read from real transcripts and marked in the hook
as observed rather than documented. The Stop contract (transcript_path,
stop_hook_active, decision/reason, no matcher, SubagentStop separate) was
checked against the Claude Code hooks docs. A prompt-type hook was not
needed: the deterministic check passed a real completion report from this
session and blocked a stripped one.
- A pipefail trap was caught while exercising the hook: `tail | grep -q`
reports failure exactly when grep matches, because tail dies of SIGPIPE.
The prefilter reads through process substitution; the section checks use
here-strings.
- Tests: an end-to-end hook suite over synthetic transcripts and the shared
HTTP sink (silence, pass, server-worded block, rewrite recorded, foreign
loop, errored write, earlier turn, unwritten reply, no recorded check, bare
id), and service tests for the reason wording and the outcome record. Smoke
event added to check_plugin; README and PACKAGING list the hook and
endpoint. Plugin version minted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
6.4 KiB
Markdown
112 lines
6.4 KiB
Markdown
# Scribe plugin for Claude Code
|
|
|
|
Turns a self-hosted [Scribe](https://git.fabledsword.com/bvandeusen/FabledScribe)
|
|
instance into a first-class Claude Code extension:
|
|
|
|
- **MCP tools** over your notes, tasks, projects, milestones, systems, and
|
|
rulebook (the `scribe` server).
|
|
- **Session-start push channel** — a `SessionStart` hook injects the
|
|
active project's live state (from the server) and this adapter's short
|
|
Claude Code guidance, so Scribe surfaces *without being asked*. Rules are
|
|
never preloaded; they arrive by retrieval when your work matches one.
|
|
- **Prior-art recall on writes** — a `PreToolUse` hook on Write/Edit checks the
|
|
file about to be written against your recorded snippets (what's kept at that
|
|
path, and what resembles the code) and offers them before the helper is
|
|
rewritten. Titles only, never blocks the edit.
|
|
- **The shared Scribe skills** — client-neutral Agent Skills, the same files
|
|
any client's package would ship: using-scribe, writing-plans, reporting-back
|
|
(reply to the operator in a shape that says where the work stands),
|
|
systematic-debugging, verification, brainstorming, reusing-code (record and
|
|
recall reusable code as snippets). Replaces superpowers.
|
|
- **Your Scribe Processes as skills** — saved Processes are synced into local
|
|
`~/.claude/skills/scribe-proc-*` stubs that auto-surface by relevance; the
|
|
stub fetches the live procedure via `get_process`. Refreshed each session and
|
|
on demand with `/scribe:sync`.
|
|
|
|
It is designed so you can uninstall `superpowers` and depend on Scribe instead
|
|
of auto-memory — leave auto-memory at its default; Scribe replaces its job by
|
|
holding the one copy, not by switching it off.
|
|
|
|
**How the pieces divide the work** (decision #4027): the Scribe server orients
|
|
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`). Packaging Scribe for
|
|
another client: see [PACKAGING.md](PACKAGING.md).
|
|
|
|
## Install
|
|
|
|
The plugin ships inside the Scribe app repo, so the marketplace *is* that repo —
|
|
you always get the plugin version that matches your Scribe instance.
|
|
|
|
```
|
|
/plugin marketplace add https://git.fabledsword.com/bvandeusen/FabledScribe.git
|
|
/plugin install scribe@scribe-plugin
|
|
```
|
|
|
|
On install you'll be asked for:
|
|
|
|
| Setting | What |
|
|
|---|---|
|
|
| **Scribe base URL** | e.g. `https://scribe.example.com` (no trailing slash) |
|
|
| **Scribe API key** | an `fmcp_` key from **Settings → API Keys** (stored in your OS keychain) |
|
|
| **Active project id** | optional — numeric project id to scope the session-start context |
|
|
|
|
## What gets wired
|
|
|
|
- `plugin.json` `mcpServers` → the `scribe` MCP server at `<base URL>/mcp` (Bearer auth).
|
|
- `hooks/hooks.json` → SessionStart hook (`hooks/scribe_session_context.sh`),
|
|
**fail-open**: if Scribe is unreachable it injects nothing and never blocks
|
|
the session.
|
|
- `hooks/hooks.json` → PreToolUse hook on `Write|Edit`
|
|
(`hooks/scribe_prior_art.sh`) → `GET /api/plugin/prior-art`. Returns
|
|
`additionalContext` with **no** permission decision, so it can inform the write
|
|
but never stop it; silent when nothing is recorded, which is most of the time.
|
|
Two framings: a REUSE menu (similar/nearby records), and a SYNC nudge when a
|
|
snippet records the exact file being edited — "updating the record is part of
|
|
the edit" — each with its own once-per-session dedup. A third, ledger-fed
|
|
line names a duplicate family (no canon) or a canon recorded elsewhere for
|
|
the names being written (its own dedup channel, `exclude_derive`).
|
|
Fail-open but not fail-silent: a configured instance that does not answer
|
|
in time is said, once per outage ("Scribe did not answer … this write went
|
|
UNCHECKED"), so a session can tell "checked, nothing there" from "never
|
|
checked"; an answer clears the marker. The local by-name arm needs no
|
|
server and always runs. Toggle in **Settings → Knowledge auto-inject**.
|
|
- `hooks/hooks.json` → PostToolUse hook on `Bash`
|
|
(`hooks/scribe_after_write.sh`): code written through sed/heredocs/scripts
|
|
never reaches the PreToolUse hook, so this one diffs the working tree after
|
|
every Bash call (per-session path+blob snapshot; one `git status` when
|
|
nothing changed) and runs the same arms on the definitions just written,
|
|
through the same endpoint and the same dedup channels. `additionalContext`
|
|
only; never blocks, and shares the pre-write hook's once-per-outage "did not
|
|
answer" line (8 s budget here — it runs after the tool, so it gates
|
|
nothing). The extractor, the prose/data skip list, the local by-name
|
|
duplicate arm and the outage line are shared in `hooks/scribe_defs.sh`.
|
|
- `hooks/hooks.json` → Stop hook (`hooks/scribe_report_check.sh`): when the
|
|
turn closed a Scribe task (`update_task`/`create_task` with status done),
|
|
checks the reply that ends it for the completion sections — where the work
|
|
sits, what needs you, what comes next — and reports the outcome to
|
|
`GET /api/plugin/report-check`. If sections are missing it blocks once with
|
|
the reason the server returns, and records how the rewrite came out; it
|
|
never blocks twice, and never blocks when the instance did not record the
|
|
check (unconfigured or unreachable). Outcomes land in the admin logs under
|
|
category `plugin`, action `report_check`.
|
|
- `skills/` → the universal process-skills, surfaced by description match.
|
|
- `hooks/scribe_sync_processes.sh` (a 2nd SessionStart hook) + the `/scribe:sync`
|
|
command → generate `~/.claude/skills/scribe-proc-*` stubs from your Scribe
|
|
Processes (via `GET /api/plugin/processes`); also **fail-open**, and pruned to
|
|
match what exists in Scribe.
|
|
|
|
## Notes
|
|
|
|
- **Do not hand-edit `version` in `.claude-plugin/plugin.json`.** It is minted
|
|
from the clock — run `python3 scripts/mint_plugin_version.py` (or `make
|
|
mint-plugin`, where `make` is installed) after changing anything under
|
|
`plugin/`, and commit the result. The installer decides whether to refresh the cache it
|
|
executes from by comparing that string, so content that ships without a new
|
|
version reaches the repo and stops there (#2209). CI fails the lane if you
|
|
forget.
|
|
- The session-start, auto-inject and prior-art hooks need only a **read**-scoped
|
|
key; the MCP tools need **write** scope to create/update. Every hook is a GET
|
|
for that reason — a read key cannot POST.
|