feat(410): the Claude Code plugin becomes a thin adapter (#4031)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Build & push image (push) Successful in 12s

Step 4 of milestone 410 "One owner per piece of guidance". The plugin's
static session context was a second copy of using-scribe and the server
index. It now says only what Claude Code needs said: 8,705 -> 1,791 chars.

scribe_static_context.md:
- points at using-scribe for how to work with Scribe, and names the process
  skills
- Claude Code specifics: keep one copy in Scribe rather than CLAUDE.md or
  auto-memory (leave auto-memory at its default); injected lines are
  retrieval, not the whole set; compact at clean seams; stored Processes
  arrive as scribe-proc-* skills with /scribe:sync; say so when the tools
  are unavailable
- retired: the restated reflexes, "how the surfaces divide the work", and
  the "follow the surface that assumes least" precedence (decision #4027)

Hook behaviour is unchanged. The SessionStart hook header says what the
static tier now carries, and the unreachable-instance status points at the
using-scribe skill instead of "the standing guidance above".

README and manifest describe the plugin as the Claude Code adapter over the
shared, client-neutral skills. Fixed along the way: the README said the
SessionStart hook "injects your rules" and suggested disabling auto-memory,
both contradicting the product since milestone 394.

Tests: the session-start rules guards now pin the owner (using-scribe) and
the index (_INSTRUCTIONS) rather than every surface; the Systems-reflex and
snippet-trigger guards pin their owners; the reporting-reflex guard pins
using-scribe. The loss guard and client-neutral guard stay green. Plugin
version minted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:45:33 -04:00
co-authored by Claude Opus 5
parent 76bfd92c21
commit 106e396b09
5 changed files with 89 additions and 167 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "name": "scribe",
"description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your active-project context, process-skills (writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", "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.1550", "version": "2026.09.14.1645",
"author": { "author": {
"name": "Bryan Van Deusen" "name": "Bryan Van Deusen"
}, },
+15 -5
View File
@@ -5,13 +5,16 @@ instance into a first-class Claude Code extension:
- **MCP tools** over your notes, tasks, projects, milestones, systems, and - **MCP tools** over your notes, tasks, projects, milestones, systems, and
rulebook (the `scribe` server). rulebook (the `scribe` server).
- **Session-start push channel** — a `SessionStart` hook injects your - **Session-start push channel** — a `SessionStart` hook injects the
rules + active-project context so Scribe surfaces *without being asked*. 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 - **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 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 path, and what resembles the code) and offers them before the helper is
rewritten. Titles only, never blocks the edit. rewritten. Titles only, never blocks the edit.
- **Universal process-skills** — using-scribe, writing-plans, reporting-back - **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), (reply to the operator in a shape that says where the work stands),
systematic-debugging, verification, brainstorming, reusing-code (record and systematic-debugging, verification, brainstorming, reusing-code (record and
recall reusable code as snippets). Replaces superpowers. recall reusable code as snippets). Replaces superpowers.
@@ -20,8 +23,15 @@ instance into a first-class Claude Code extension:
stub fetches the live procedure via `get_process`. Refreshed each session and stub fetches the live procedure via `get_process`. Refreshed each session and
on demand with `/scribe:sync`. on demand with `/scribe:sync`.
It is designed so you can uninstall `superpowers` and disable auto-memory and It is designed so you can uninstall `superpowers` and depend on Scribe instead
depend on neither. 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`).
## Install ## Install
+7 -6
View File
@@ -1,11 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding). # Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding).
# #
# Tier 1 (STATIC, always fires, no auth, no network): injects a bundled # Tier 1 (STATIC, always fires, no auth, no network): injects the Claude Code
# behavioral mandate (scribe_static_context.md) so a fresh session knows to # adapter's own guidance (scribe_static_context.md) — where Scribe's reflexes
# reach for Scribe — record work, recall before acting — even when the instance # are stated (the using-scribe skill), Claude Code's memory files, /compact,
# is unreachable or unconfigured. The static tier is the load-bearing floor that # /scribe:sync, and what to do when Scribe is unavailable. Since milestone 410
# does not depend on the key or the network. # (decision #4027) it carries only what this client needs said; the reflexes
# themselves are owned by the shared skills and the server's index.
# #
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance # Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance
# for active-project context and appends it. Config comes from # for active-project context and appends it. Config comes from
@@ -160,7 +161,7 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
# (milestone 394). Nothing is preloaded, so there is no set whose # (milestone 394). Nothing is preloaded, so there is no set whose
# drift a later write could be told about — a rule is retrieved at # drift a later write could be told about — a rule is retrieved at
# the moment it applies, which cannot be stale. # the moment it applies, which cannot be stale.
[ -z "$dyn" ] && status="> ⚠️ Scribe: live project context could not be loaded this session (instance unreachable or request failed). The standing guidance above still applies — ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\` as needed." [ -z "$dyn" ] && status="> ⚠️ Scribe: live project context could not be loaded this session (instance unreachable or request failed). The using-scribe skill still applies — ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\` as needed."
elif [ -n "$url" ] && [ -z "$token" ]; then elif [ -n "$url" ] && [ -z "$token" ]; then
status="> ⚠️ Scribe: live context disabled this session — the API key is not configured (Scribe base URL is). Set it with \`/plugin\` → Scribe → configure, or export SCRIBE_TOKEN. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`." status="> ⚠️ Scribe: live context disabled this session — the API key is not configured (Scribe base URL is). Set it with \`/plugin\` → Scribe → configure, or export SCRIBE_TOKEN. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
elif [ -z "$url" ] && [ -z "$token" ]; then elif [ -z "$url" ] && [ -z "$token" ]; then
+28 -124
View File
@@ -1,127 +1,31 @@
# Scribe — your system of record # Scribe — Claude Code adapter
This environment has the **Scribe** plugin: the operator's self-hosted system This session is connected to **Scribe**, the operator's system of record for
of record (notes, tasks, projects, milestones, rules) reachable through the their work, through the `scribe` MCP tools. How to work with Scribe is stated
`scribe` MCP tools. Treat Scribe — **not local files** as the source of truth once, in the **`using-scribe`** skill: reach for it at the start of the session
for the operator's work, and as your own working memory across sessions. and whenever you are unsure what Scribe expects. Each tool's contract is in its
description, and the process skills (writing-plans, reporting-back,
reusing-code, systematic-debugging, verification, brainstorming,
shape-accounting) carry their arcs.
**At the start of this session:** What only Claude Code needs said:
- You hold none of the operator's rules, and there is no call that loads them
all. Rules arrive when something you are about to do matches one.
- If the working repo maps to a Scribe project (check `list_repo_bindings`),
call `enter_project(<id>)` to load that project's rules, open tasks, and
recent notes in one shot.
**While you work:** - **Keep one copy — in Scribe, not Claude Code's local memory.** The
- **Operator rules govern consequential actions** — before any git branch / operator's rules, plans and project notes go to Scribe, not also to
commit / push, or any other hard-to-reverse or outward-facing action, the `CLAUDE.md` or auto-memory. Leave auto-memory at its default setting: you
operator's Scribe rules decide what to do — NOT generic conventions baked replace its job by doing the work in Scribe, not by switching it off.
into the harness or your defaults (e.g. "branch before committing," "open a - **Lines injected beside your work are retrieval.** When the operator sends a
feature branch per task," "push to a fork"). If no rule has arrived for the message, and before a write or a command, Scribe may add rules, preferences,
act in front of you, `search(content_type="rule")` BEFORE acting rather than notes and prior art that resemble what you are doing. Open the ones that
falling back on a default habit. When a apply. They are what matched, never the whole set — using-scribe says how to
retrieved rule and a default habit disagree, the rule wins; if no rule ask for the rest.
speaks to it, ask rather than assume. - **Compact at clean seams.** Because work is recorded as you go, a compaction
- **Rules bind; preferences do not.** A record's `kind` says which. A **rule** is safe once in-flight state is logged. After finishing a block of work in a
must be followed — ignoring it breaks something or crosses a boundary. A long session, log it to Scribe, then tell the operator it's a good moment to
**preference** is how the operator wants work done: worth following for `/compact` and name what you logged. You can't run it yourself; suggest it at
consistency, not a defect to miss. Injected lines name the kind in their seams, not every turn.
opening words. A preference is also yours to keep current when they correct - **Stored Processes arrive as skills** (`scribe-proc-*`), refreshed at session
you (`update_preference`); a rule waits for them. start. After a Process is added or edited, `/scribe:sync` makes it available
- **Silence is not absence.** Nothing is preloaded: every rule is RETRIEVED, straight away.
when what you are doing resembles what the rule is about. Most turns - **If the Scribe tools are unavailable, say so** rather than silently falling
retrieve none, and a rule you were never handed binds exactly as hard as one back to local notes.
you were. So before a consequential act, `search` for a rule about it
(`content_type="rule"`) rather than concluding from an empty session that
nothing applies. "I was not told" is not the same as "there is no rule," and
only one of those is checkable.
This bites hardest on which TOOL to reach for — curling an API that has an
MCP client, standing up a local stack, running a suite CI owns. Those feel
like mechanics rather than decisions, so they raise no doubt and generate no
query; the moment you are most confident is the moment to look.
- **Recall before acting** — before you answer anything about the operator's
work or start a task, `search` Scribe first; assume a related note, task, or
decision already exists. Concretely, reach for recall whenever a request
touches the operator's projects, people, places, prior decisions, or existing
work: check for an existing task before opening a new one, and for a prior
note/decision before re-deriving one. When a project is in scope (you entered
one), pass its id to `search` so results stay scoped to it. Treating Scribe as
the first place you look — not just somewhere you write — is what makes it a
trustworthy record.
- **Record as you go** — track work as Scribe tasks and log progress with
`add_task_log`. Always log when you **complete a task** and when you **hit or
discover a problem** — so changes of direction are captured, not just
successes. Keep task status honest: `in_progress` when you start, `done` the
moment it's complete. When you **fix** something — even in passing — record it
as its own issue (`create_task(kind="issue")`), not as a work-log line on an
unrelated open task.
- **Report back in a shape the operator can read** — they were not there while
you worked, so organise each reply around where the work stands rather than
the order you did things in: which task or milestone it belongs to, what now
works, what needs them, what comes next. Take the placement from the
`placement` block task writes return, not from memory. The `reporting-back`
skill holds the shape for each kind of reply.
- **Tag to Systems as you write** — `enter_project` lists the project's
Systems (its named subsystems/areas). When you create or meaningfully update
a record, ask which areas it is about and pass `system_ids`; if an area has
no System yet, create it with `create_system` (name + a one-paragraph
charter) rather than leaving it unmodelled. Cross-cutting records — audits,
sweeps, reviews — take SEVERAL tags, and are the best moment to DISCOVER
missing Systems: a pass that walks the subsystems has just enumerated the
vocabulary, so mint what it names. Create liberally; the duplicate gate on
`create_system` (and reviewing the existing list) is the guardrail against
sprawl, not restraint. Every read and write of a project record shows its
`systems` — that is the "am I in a System's territory?" signal, and
`list_system_records` reads that territory's whole pile before you work in
it. An untagged project record carries the `systems_hint` question instead,
on creates, updates, and work-logs alike — treat it as the tagging question
asked at the moment of work, not as noise to skip past.
- **The pattern library: start from recorded shapes, and record every shape
at first build** — recorded **snippets** are the project's pattern library,
not a dedup net. Before building ANY shape — a button, an input field, a
modal, a route handler, a service class, a test scaffold, up through complex
subsystem patterns — search snippets and START from the recorded shape; a
deliberate departure is recorded as its own named variant, never left as
silent drift. And the FIRST time a shape is built, record it with
`create_snippet` (name, when-to-reach-for-it, location, code) in the same
breath — do not judge whether it "might recur": the builder of the first
instance can never know, and a missed record is invisible until it
resurfaces as an uninformed duplicate. A mature project's snippet corpus
should read as a map of every shape in it. The backstop still holds:
noticing the second copy of anything, or consolidating copies into a shared
X, means X gets recorded before that work is finished — which is how a
codebase is kept from growing four `.btn-primary` definitions. The write-path
hooks (before a Write/Edit, and after any Bash call that changed the tree)
name a known duplicate family or a canon elsewhere for what was just
written — act on that line at the write, not at the next audit.
- Do **not** keep the operator's rules, plans, or project notes in local
memory / CLAUDE.md in parallel with Scribe — Scribe holds the single copy.
- **Compact at clean seams** — because you record as you go, a context
compaction is safe: the durable record lives in Scribe, not the transcript.
After finishing a block of work in a long session, make sure in-flight state
is logged to Scribe, then tell the operator it's a good, safe moment to
`/compact` (name what you logged). You can't run it yourself — surface the
recommendation and let them decide. Suggest it at seams, not every turn.
**How the instruction surfaces divide the work:** this file carries the
session-level reflexes (WHEN to reach for Scribe); each tool's own description
carries its full contract (HOW to call it — read it when you load the tool);
the bundled skills carry process arcs (planning, debugging, verification). The
MCP server's instruction block is deliberately only a map — the client injects
roughly its first 2,000 characters and silently cuts the rest, so nothing
load-bearing lives below that fold.
**If two Scribe instruction surfaces disagree** — this file, the MCP server's
tool instructions, the `using-scribe` skill — **follow the one that assumes
least about its own delivery.** This file is the floor: it ships with the
plugin and needs no API key and no network, so it still applies in exactly the
session where the others never arrived. The others may elaborate on what is
written here; they must not contradict it. Weigh a disagreement by which way it
fails, not by which surface said more: doing something a push would also have
covered costs one redundant call, while skipping it because you expected a push
that never came means working without the operator's rules and not knowing.
A contradiction between surfaces is a defect in the product — say so, so it
gets recorded and fixed rather than silently arbitrated again next session.
If the Scribe tools are unavailable, say so rather than silently falling back
to local notes.
+37 -30
View File
@@ -69,13 +69,19 @@ ABSENCE_CLAIMS = (
"not evidence there is none", "not evidence there is none",
) )
# Surfaces a session loads before substantive work. Hand-written because # The surfaces that STATE the rules reflex. Hand-written because "who owns
# "is this a session-start surface?" is an editorial fact, not a derivable one — # this?" is an editorial fact, not a derivable one — but each entry is asserted
# but each entry is asserted to EXIST, so a move or rename fails loudly here # to EXIST, so a move or rename fails loudly here instead of quietly dropping
# instead of quietly dropping that surface from the check. # that surface from the check.
#
# Since milestone 410 (decision #4027) that is the owner and the index, not
# every surface a session loads: `using-scribe` states the reflex in full and
# the server's `_INSTRUCTIONS` gives it one line for every MCP client. The
# Claude Code adapter's static context used to be a third copy; it now points
# at the skill instead, and tests/test_guidance_ownership.py keeps the topic
# from falling off.
SESSION_START_SURFACES = ( SESSION_START_SURFACES = (
ROOT / "src" / "scribe" / "mcp" / "server.py", ROOT / "src" / "scribe" / "mcp" / "server.py",
ROOT / "plugin" / "hooks" / "scribe_static_context.md",
ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md", ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md",
) )
@@ -174,40 +180,42 @@ def test_instructions_fit_the_fold():
) )
def test_floor_states_the_systems_reflex(): def test_the_systems_reflex_is_stated_by_its_owner():
"""Write-time tagging guidance must live on the surface that always arrives. """Write-time tagging guidance must be stated as a reflex, not only per tool.
#2562's behavioral finding: with the guidance only in tool descriptions, #2562's behavioral finding: with the guidance only in tool descriptions,
sessions filed records untagged. The static context is the delivery floor, sessions filed records untagged. The tag-as-you-write reflex was pinned on
so the tag-as-you-write reflex has to be stated there. the static context then; since milestone 410 its owner is using-scribe
(decision #4027), with the in-band `systems_hint` as the half that fires on
its own.
""" """
floor = (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text() owner = (ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md").read_text()
for needle in ("system_ids", "create_system"): for needle in ("system_ids", "create_system", "systems_hint"):
assert needle in floor, ( assert needle in owner, (
f"plugin/hooks/scribe_static_context.md no longer mentions " f"plugin/skills/using-scribe/SKILL.md no longer mentions "
f"{needle} — the Systems tagging reflex must be stated on the " f"{needle} — the Systems tagging reflex must be stated by its "
f"floor, not only in tool descriptions (#2562)." f"owner, not only in tool descriptions (#2562)."
) )
def test_floor_names_the_snippet_recording_triggers(): def test_the_snippet_recording_triggers_are_stated_by_their_owner():
"""The floor must state the pattern-library recording model, by name. """The pattern-library recording model must be stated, by name.
#2664's behavioral finding: recording guidance as a trailing clause of the #2664's behavioral finding: recording guidance as a trailing clause of the
reuse bullet converted zero times outside snippet-minded sessions. The reuse bullet converted zero times outside snippet-minded sessions. The
2026-08-16 ruling (decision #2686) then replaced the reactive model 2026-08-16 ruling (decision #2686) then replaced the reactive model
entirely: every shape is recorded at FIRST build — no "will it recur?" entirely: every shape is recorded at FIRST build — no "will it recur?"
judgment — and second-copy consolidation is only the backstop. The floor judgment — and second-copy consolidation is only the backstop. All three
is the delivery surface for that reflex, so all three elements must stay elements must stay stated: the tool, the first-build trigger, and the
stated: the tool, the first-build trigger, and the backstop. backstop. Since milestone 410 the owner is the reusing-code skill.
""" """
floor = (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text() owner = " ".join((ROOT / "plugin" / "skills" / "reusing-code" / "SKILL.md").read_text().split())
for needle in ("create_snippet", "first build", "second copy"): for needle in ("create_snippet", "first build", "second copy"):
assert needle in floor, ( assert needle in owner, (
f"plugin/hooks/scribe_static_context.md no longer states the " f"plugin/skills/reusing-code/SKILL.md no longer states the "
f"snippet-recording model ({needle!r}) — record-every-shape-at-" f"snippet-recording model ({needle!r}) — record-every-shape-at-"
f"first-build with second-copy consolidation as the backstop must " f"first-build with second-copy consolidation as the backstop must "
f"be stated on the floor (#2664, decision #2686)." f"be stated by its owner (#2664, decision #2686)."
) )
@@ -287,19 +295,18 @@ def test_a_surface_claiming_rules_bind_also_names_what_does_not():
# ── Reporting back (milestone 409 step 3) ────────────────────────────── # ── Reporting back (milestone 409 step 3) ──────────────────────────────
# #
# The reporting-back skill carries the shapes, but a skill only helps if it # The reporting-back skill carries the shapes, but a skill only helps if it
# fires. The reflex that sends a session to it lives on the two plugin # fires. The reflex that sends a session to it is stated in using-scribe; the
# surfaces a session always reads; the in-band cue on update_task is the half # server's index carries a REPORT line pointing at `placement`, and the in-band
# that reaches clients with no plugin at all. Since milestone 410 the server's # cue on update_task is the half that fires on its own in every client. The
# index carries a REPORT line too, pointing at `placement`. # static context was a second plugin-side copy until milestone 410.
REPORT_REFLEX = "report back in a shape the operator can read" REPORT_REFLEX = "report back in a shape the operator can read"
REPORT_SURFACES = ( REPORT_SURFACES = (
ROOT / "plugin" / "hooks" / "scribe_static_context.md",
ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md", ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md",
) )
def test_the_reporting_reflex_reaches_every_plugin_surface(): def test_the_reporting_reflex_is_stated_in_using_scribe():
missing = [] missing = []
for path in REPORT_SURFACES: for path in REPORT_SURFACES:
text = " ".join(path.read_text().split()).lower() text = " ".join(path.read_text().split()).lower()