diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 23f5ecc..aa8a854 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "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.", - "version": "2026.09.14.1550", + "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", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/README.md b/plugin/README.md index 8007946..2d5e219 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -5,13 +5,16 @@ 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 your - rules + active-project context so Scribe surfaces *without being asked*. +- **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. -- **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), systematic-debugging, verification, brainstorming, reusing-code (record and 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 on demand with `/scribe:sync`. -It is designed so you can uninstall `superpowers` and disable auto-memory and -depend on neither. +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`). ## Install diff --git a/plugin/hooks/scribe_session_context.sh b/plugin/hooks/scribe_session_context.sh index 13471bc..d167c1c 100755 --- a/plugin/hooks/scribe_session_context.sh +++ b/plugin/hooks/scribe_session_context.sh @@ -1,11 +1,12 @@ #!/usr/bin/env bash # Scribe plugin — SessionStart push channel (two tiers + compaction re-grounding). # -# Tier 1 (STATIC, always fires, no auth, no network): injects a bundled -# behavioral mandate (scribe_static_context.md) so a fresh session knows to -# reach for Scribe — record work, recall before acting — even when the instance -# is unreachable or unconfigured. The static tier is the load-bearing floor that -# does not depend on the key or the network. +# Tier 1 (STATIC, always fires, no auth, no network): injects the Claude Code +# adapter's own guidance (scribe_static_context.md) — where Scribe's reflexes +# are stated (the using-scribe skill), Claude Code's memory files, /compact, +# /scribe:sync, and what to do when Scribe is unavailable. Since milestone 410 +# (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 # 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 # drift a later write could be told about — a rule is retrieved at # 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 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 diff --git a/plugin/hooks/scribe_static_context.md b/plugin/hooks/scribe_static_context.md index a6b2172..5dac691 100644 --- a/plugin/hooks/scribe_static_context.md +++ b/plugin/hooks/scribe_static_context.md @@ -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 -of record (notes, tasks, projects, milestones, rules) reachable through the -`scribe` MCP tools. Treat Scribe — **not local files** — as the source of truth -for the operator's work, and as your own working memory across sessions. +This session is connected to **Scribe**, the operator's system of record for +their work, through the `scribe` MCP tools. How to work with Scribe is stated +once, in the **`using-scribe`** skill: reach for it at the start of the session +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:** -- 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()` to load that project's rules, open tasks, and - recent notes in one shot. +What only Claude Code needs said: -**While you work:** -- **Operator rules govern consequential actions** — before any git branch / - commit / push, or any other hard-to-reverse or outward-facing action, the - operator's Scribe rules decide what to do — NOT generic conventions baked - into the harness or your defaults (e.g. "branch before committing," "open a - feature branch per task," "push to a fork"). If no rule has arrived for the - act in front of you, `search(content_type="rule")` BEFORE acting rather than - falling back on a default habit. When a - retrieved rule and a default habit disagree, the rule wins; if no rule - speaks to it, ask rather than assume. -- **Rules bind; preferences do not.** A record's `kind` says which. A **rule** - must be followed — ignoring it breaks something or crosses a boundary. A - **preference** is how the operator wants work done: worth following for - consistency, not a defect to miss. Injected lines name the kind in their - opening words. A preference is also yours to keep current when they correct - you (`update_preference`); a rule waits for them. -- **Silence is not absence.** Nothing is preloaded: every rule is RETRIEVED, - when what you are doing resembles what the rule is about. Most turns - retrieve none, and a rule you were never handed binds exactly as hard as one - 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. +- **Keep one copy — in Scribe, not Claude Code's local memory.** The + operator's rules, plans and project notes go to Scribe, not also to + `CLAUDE.md` or auto-memory. Leave auto-memory at its default setting: you + replace its job by doing the work in Scribe, not by switching it off. +- **Lines injected beside your work are retrieval.** When the operator sends a + message, and before a write or a command, Scribe may add rules, preferences, + notes and prior art that resemble what you are doing. Open the ones that + apply. They are what matched, never the whole set — using-scribe says how to + ask for the rest. +- **Compact at clean seams.** Because work is recorded as you go, a compaction + is safe once in-flight state is logged. After finishing a block of work in a + long session, log it to Scribe, then tell the operator it's a good moment to + `/compact` and name what you logged. You can't run it yourself; suggest it at + seams, not every turn. +- **Stored Processes arrive as skills** (`scribe-proc-*`), refreshed at session + start. After a Process is added or edited, `/scribe:sync` makes it available + straight away. +- **If the Scribe tools are unavailable, say so** rather than silently falling + back to local notes. diff --git a/tests/test_instruction_surfaces_agree.py b/tests/test_instruction_surfaces_agree.py index 98a9224..cc8abbc 100644 --- a/tests/test_instruction_surfaces_agree.py +++ b/tests/test_instruction_surfaces_agree.py @@ -69,13 +69,19 @@ ABSENCE_CLAIMS = ( "not evidence there is none", ) -# Surfaces a session loads before substantive work. Hand-written because -# "is this a session-start surface?" is an editorial fact, not a derivable one — -# but each entry is asserted to EXIST, so a move or rename fails loudly here -# instead of quietly dropping that surface from the check. +# The surfaces that STATE the rules reflex. Hand-written because "who owns +# this?" is an editorial fact, not a derivable one — but each entry is asserted +# to EXIST, so a move or rename fails loudly here instead of quietly dropping +# 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 = ( ROOT / "src" / "scribe" / "mcp" / "server.py", - ROOT / "plugin" / "hooks" / "scribe_static_context.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(): - """Write-time tagging guidance must live on the surface that always arrives. +def test_the_systems_reflex_is_stated_by_its_owner(): + """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, - sessions filed records untagged. The static context is the delivery floor, - so the tag-as-you-write reflex has to be stated there. + sessions filed records untagged. The tag-as-you-write reflex was pinned on + 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() - for needle in ("system_ids", "create_system"): - assert needle in floor, ( - f"plugin/hooks/scribe_static_context.md no longer mentions " - f"{needle} — the Systems tagging reflex must be stated on the " - f"floor, not only in tool descriptions (#2562)." + owner = (ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md").read_text() + for needle in ("system_ids", "create_system", "systems_hint"): + assert needle in owner, ( + f"plugin/skills/using-scribe/SKILL.md no longer mentions " + f"{needle} — the Systems tagging reflex must be stated by its " + f"owner, not only in tool descriptions (#2562)." ) -def test_floor_names_the_snippet_recording_triggers(): - """The floor must state the pattern-library recording model, by name. +def test_the_snippet_recording_triggers_are_stated_by_their_owner(): + """The pattern-library recording model must be stated, by name. #2664's behavioral finding: recording guidance as a trailing clause of the reuse bullet converted zero times outside snippet-minded sessions. The 2026-08-16 ruling (decision #2686) then replaced the reactive model entirely: every shape is recorded at FIRST build — no "will it recur?" - judgment — and second-copy consolidation is only the backstop. The floor - is the delivery surface for that reflex, so all three elements must stay - stated: the tool, the first-build trigger, and the backstop. + judgment — and second-copy consolidation is only the backstop. All three + elements must stay stated: the tool, the first-build trigger, and the + 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"): - assert needle in floor, ( - f"plugin/hooks/scribe_static_context.md no longer states the " + assert needle in owner, ( + f"plugin/skills/reusing-code/SKILL.md no longer states the " f"snippet-recording model ({needle!r}) — record-every-shape-at-" 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) ────────────────────────────── # # 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 -# surfaces a session always reads; the in-band cue on update_task is the half -# that reaches clients with no plugin at all. Since milestone 410 the server's -# index carries a REPORT line too, pointing at `placement`. +# fires. The reflex that sends a session to it is stated in using-scribe; the +# server's index carries a REPORT line pointing at `placement`, and the in-band +# cue on update_task is the half that fires on its own in every client. The +# static context was a second plugin-side copy until milestone 410. REPORT_REFLEX = "report back in a shape the operator can read" REPORT_SURFACES = ( - ROOT / "plugin" / "hooks" / "scribe_static_context.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 = [] for path in REPORT_SURFACES: text = " ".join(path.read_text().split()).lower()