wip(394): steps 6+7 — backend path and instruction surfaces
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"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 always-on rules + active-project context, process-skills (writing-plans, 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 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, 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.11.1154",
|
||||
"author": {
|
||||
"name": "Bryan Van Deusen"
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ 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 always-on
|
||||
- **Session-start push channel** — a `SessionStart` hook injects your
|
||||
rules + active-project context so Scribe surfaces *without being asked*.
|
||||
- **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
|
||||
|
||||
@@ -175,16 +175,11 @@ while IFS= read -r rel_path; do
|
||||
derive_seen=$(tr '\n' ',' < "$derivefile" 2>/dev/null | sed 's/,$//' | jq -sRr '@uri' 2>/dev/null) || derive_seen=""
|
||||
[ -n "$derive_seen" ] && derive_exclude_q="&exclude_derive=${derive_seen}"
|
||||
fi
|
||||
# The rules marker the SessionStart hook stored, handed back so the server
|
||||
# can say whether those rules moved since (milestone 323). Nothing stored
|
||||
# means nothing sent, which the server reads as silence rather than as a
|
||||
# mismatch — an install that never reached /api/plugin/context must not
|
||||
# start claiming its rules changed.
|
||||
# The rules marker is gone with the resident set it aged (milestone 394).
|
||||
# A session no longer holds a fixed set of rules from turn zero, so there
|
||||
# is nothing that can have drifted since it loaded them — each rule is
|
||||
# retrieved at the moment it applies.
|
||||
etag_q=""
|
||||
if [ -f "$state_dir/${safe_sid}.rules_etag" ]; then
|
||||
held=$(jq -sRr '@uri' < "$state_dir/${safe_sid}.rules_etag" 2>/dev/null) || held=""
|
||||
[ -n "$held" ] && etag_q="&rules_etag=${held}"
|
||||
fi
|
||||
if [ -n "$path_enc" ]; then
|
||||
# 8s, not the pre-write hook's 5: this hook runs AFTER the tool, so it
|
||||
# gates nothing the session is waiting on, and the first prior-art call
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# does not depend on the key or the network.
|
||||
#
|
||||
# Tier 2 (DYNAMIC, best-effort enrichment): curls the operator's Scribe instance
|
||||
# for always-on rules + active-project context and appends it. Config comes from
|
||||
# for active-project context and appends it. Config comes from
|
||||
# the plugin's userConfig, exported to hooks as:
|
||||
# CLAUDE_PLUGIN_OPTION_API_ENDPOINT base URL, no trailing slash
|
||||
# CLAUDE_PLUGIN_OPTION_API_TOKEN fmcp_ API key (sensitive)
|
||||
@@ -158,7 +158,7 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
[ -n "$body" ] && dyn=$(printf '%s' "$body" | jq -r '.context // empty' 2>/dev/null)
|
||||
# Stash the rules marker for the write-path hook (milestone 323). THIS is
|
||||
# where it has to be captured: the model receives one from
|
||||
# list_always_on_rules too, but a hook cannot see an MCP tool's result. Stored
|
||||
# an MCP tool too, but a hook cannot see a tool's result. Stored
|
||||
# under the same state dir the prior-art hook already uses, keyed by session,
|
||||
# so "changed since" means since THIS session loaded its rules.
|
||||
#
|
||||
@@ -178,9 +178,9 @@ if [ -n "$url" ] && [ -n "$token" ] && command -v curl >/dev/null 2>&1; then
|
||||
&& printf '%s' "$etag" > "$etag_dir/${safe_sid}.rules_etag" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
[ -z "$dyn" ] && status="> ⚠️ Scribe: live rules/project context could not be loaded this session (instance unreachable or request failed). The standing guidance above still applies — pull rules with \`list_always_on_rules()\` 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 standing guidance above 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; pull rules with \`list_always_on_rules()\` 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
|
||||
# NEITHER value arrived. Previously this case stayed silent as "an unconfigured
|
||||
# install", which made issue #2198 invisible for weeks: a *casing* bug here
|
||||
@@ -189,7 +189,7 @@ elif [ -z "$url" ] && [ -z "$token" ]; then
|
||||
# silently disabled auto-inject and the write-path trigger too. It is not a
|
||||
# benign state — the plugin prompts for both values at enable time, so if
|
||||
# neither reached the hook, something is wrong. Say so.
|
||||
status="> ⚠️ Scribe: live context disabled this session — neither the Scribe base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Scribe), or export SCRIBE_URL + SCRIBE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; pull rules with \`list_always_on_rules()\` and project context with \`enter_project()\`."
|
||||
status="> ⚠️ Scribe: live context disabled this session — neither the Scribe base URL nor the API key reached this hook. Configure the plugin (\`/plugin\` → Scribe), or export SCRIBE_URL + SCRIBE_TOKEN. Note this also disables prompt auto-inject and the write-path prior-art trigger. Tools still work; ask for rules with \`search(content_type=\"rule\")\` and project context with \`enter_project()\`."
|
||||
fi
|
||||
|
||||
[ -n "$dyn" ] && append "$dyn"
|
||||
@@ -197,7 +197,7 @@ fi
|
||||
|
||||
# Compaction re-grounding: lead with a reload banner when this fire is a compact.
|
||||
if [ "$source" = "compact" ]; then
|
||||
prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Before continuing, reload your bearings from Scribe: re-pull the operator's binding rules with \`list_always_on_rules()\` (a compaction can summarize them out of context, leaving only generic harness defaults in their place), re-run \`enter_project()\` for the active project, check its open tasks and recent notes, and reconcile what you're mid-way through against what Scribe records. Don't trust half-remembered state — Scribe is the record."
|
||||
prepend "> ⟳ This session was just COMPACTED — earlier turns are now a summary, so in-flight detail may be lost. Any rules that had been retrieved went into that summary with everything else, so treat yourself as holding none: before the next consequential act, ask again with \`search(content_type=\"rule\")\` rather than trusting a half-remembered one. Re-run \`enter_project()\` for the active project, check its open tasks and recent notes, and reconcile what you are mid-way through against what Scribe records. Scribe is the record."
|
||||
fi
|
||||
|
||||
# Nothing at all to inject → stay silent.
|
||||
|
||||
@@ -6,7 +6,8 @@ of record (notes, tasks, projects, milestones, rules) reachable through the
|
||||
for the operator's work, and as your own working memory across sessions.
|
||||
|
||||
**At the start of this session:**
|
||||
- Call `list_always_on_rules()` to load the operator's standing rules.
|
||||
- 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.
|
||||
@@ -17,22 +18,23 @@ for the operator's work, and as your own working memory across sessions.
|
||||
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 you have not loaded the
|
||||
operator's rules this session — or earlier turns were summarized away by a
|
||||
compaction — call `list_always_on_rules()` (and `enter_project()` when a
|
||||
project is in scope) BEFORE acting. When a loaded rule and a default habit
|
||||
disagree, the rule wins; if no rule speaks to it, ask rather than assume.
|
||||
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.
|
||||
- **What you loaded is not all of the rules.** Only the always-on tier arrives
|
||||
that way; conditional rules are RETRIEVED, and one you were never handed
|
||||
binds exactly as hard. So before a consequential act, `search` for a rule
|
||||
about it (`content_type="rule"`) rather than concluding from an empty
|
||||
loaded set that nothing applies. "I was not told" is not the same as "there
|
||||
is no rule," and only one of those is checkable.
|
||||
- **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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# asks "what is recorded about the file being written". This one asks "does a
|
||||
# standing rule speak to the command about to be run" — the question nothing
|
||||
# could ask before, and the reason every rule about which tool to reach for had
|
||||
# to live in the always-on preload instead.
|
||||
# to live in the preload instead, back when there was one.
|
||||
#
|
||||
# WHY A HOOK AND NOT AN INSTRUCTION. A reflex generates no query (note #3089):
|
||||
# you reach for `curl` confidently, with no moment of doubt, so a surface that
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: using-scribe
|
||||
description: Use at the START of every session, and before answering anything about the operator's work or starting any task — establishes the Scribe-first reflex. FIRST ACTION of a session: call list_always_on_rules() (and enter_project when a repo/project is in scope) to load the operator's binding rules. Then recall before acting, update over duplicate, plan in Scribe not in files.
|
||||
description: Use at the START of every session, and before answering anything about the operator's work or starting any task — establishes the Scribe-first reflex. You hold none of the operator's rules: they arrive by retrieval when your work matches one, and search(content_type="rule") is how you ask before a consequential act. Call enter_project when a repo/project is in scope. Then recall before acting, update over duplicate, plan in Scribe not in files.
|
||||
---
|
||||
|
||||
# Using Scribe
|
||||
@@ -13,12 +13,19 @@ asked for.
|
||||
|
||||
## Do this first (every session)
|
||||
|
||||
**Pull the standing rules yourself — do not wait for them to be handed to you.**
|
||||
At the start of a session, before substantive work, call
|
||||
`list_always_on_rules()` to load the operator's always-on rules. If the working
|
||||
repo maps to a Scribe project (you're in a known repo, or `list_repo_bindings`
|
||||
shows a binding), call `enter_project(id)` instead/as-well — it returns the
|
||||
project plus its applicable rules, open tasks, and recent notes in one shot.
|
||||
**You are not holding the operator's rules, and no call loads them all.**
|
||||
There is no standing set to pull. A rule reaches you when what you are about to
|
||||
do matches it — a command, code you are writing, or what the operator just
|
||||
asked for — and on most turns none will. That is the surface working.
|
||||
|
||||
**So the reflex is to ASK, not to load.** Before a consequential act — anything
|
||||
hard to reverse or outward-facing — `search(content_type="rule")` for the thing
|
||||
you are about to do. An empty session is not evidence of an empty rulebook.
|
||||
|
||||
If the working repo maps to a Scribe project (you're in a known repo, or
|
||||
`list_repo_bindings` shows a binding), call `enter_project(id)` — it returns the
|
||||
project plus the rules bound to the areas it works in, open tasks, and recent
|
||||
notes in one shot.
|
||||
|
||||
Do this actively. A SessionStart hook *may* also inject a rule index, but treat
|
||||
that as a bonus, not a precondition: it can be absent (e.g. when the instance is
|
||||
@@ -56,11 +63,12 @@ Two constraints on *how* that's achieved:
|
||||
re-deriving it or opening a duplicate. When a project is in scope, pass its
|
||||
`project_id` so results stay scoped.
|
||||
|
||||
2. **Standing rules are binding — and the ones you were handed are not all of
|
||||
them.** Load the resident set via `list_always_on_rules()` at session start
|
||||
(see "Do this first"). Pull a record's full statement with `get_rule(id)`
|
||||
when it's about to bite. When a project is in scope, `enter_project(id)`
|
||||
also returns its applicable rules.
|
||||
2. **Rules are binding, and silence does not mean there are none.** Nothing
|
||||
is preloaded, so "no rule arrived" means "nothing matched" — never "no rule
|
||||
exists". Ask with `search(content_type="rule")` before a consequential act,
|
||||
and pull a record's full statement with `get_rule(id)` when it is about to
|
||||
bite. When a project is in scope, `enter_project(id)` also returns the rules
|
||||
bound to its areas.
|
||||
|
||||
**`kind` says how much force a record carries, and it is never something to
|
||||
infer.** A **rule** must be followed: ignoring it breaks something or
|
||||
@@ -222,13 +230,12 @@ bound — confine the session to it:
|
||||
## Starting a project: decide what it inherits
|
||||
|
||||
A project's inheritance is a **decision, not a default**. Before
|
||||
`create_project`, ask the operator the four inception questions and pass the
|
||||
`create_project`, ask the operator the three inception questions and pass the
|
||||
answers — never create a project bare by default:
|
||||
|
||||
- which **always-on rulebooks** it should NOT inherit (`list_rulebooks` shows
|
||||
which are always_on; default: inherit them all) →
|
||||
`exclude_always_on_rulebooks=[...]`
|
||||
- which other rulebooks to **subscribe** → `subscribe_rulebooks=[...]`
|
||||
- which rulebooks to **subscribe** (`list_rulebooks` shows them; default: none
|
||||
— a rulebook binds a project only when it opts in) →
|
||||
`subscribe_rulebooks=[...]`
|
||||
- which **design system** its UI is built from (`list_design_systems`; or
|
||||
none) → `design_system_id=<id | -1>`
|
||||
- whether to **seed the standard starter Systems** so records can be tagged
|
||||
@@ -246,19 +253,23 @@ inception is the moment they are decided together, and the record of why.
|
||||
When codifying a rule, pick its home by **who it should bind** — and keep
|
||||
shared homes general:
|
||||
|
||||
- **Always-on rulebook** (`create_rule` in an `always_on` rulebook) — universal
|
||||
norms that bind *every* project. Cross-project standards only.
|
||||
- **Subscribed rulebook** (`create_rule` + `subscribe_project_to_rulebook`) — a
|
||||
reusable, *themed* module of general rules that binds only projects that opt
|
||||
in (e.g. a review checklist → every service). Themed, but project-agnostic.
|
||||
- **Rulebook** (`create_rule` + `subscribe_project_to_rulebook`) — a reusable,
|
||||
*themed* module of general rules that binds the projects which opt in (e.g. a
|
||||
review checklist → every service). Themed, but project-agnostic.
|
||||
- **Project rule** (`create_project_rule`) — anything specific to one project
|
||||
(its files, paths, quirks).
|
||||
|
||||
Both rulebook tiers are shared, so their rules stay general; they differ in
|
||||
**reach** (all vs opt-in), not generality. Names one project's specifics →
|
||||
project rule; a standard a category shares → subscribed rulebook; a universal
|
||||
norm → always-on rulebook. Never put project-specific detail in a shared
|
||||
rulebook — it leaks to every other project that gets it.
|
||||
There used to be a third home — an `always_on` rulebook that bound every
|
||||
project automatically. It is gone: subscription is the only reach a rulebook
|
||||
has. Names one project's specifics → project rule; anything a category of
|
||||
projects shares → rulebook. Never put project-specific detail in a rulebook —
|
||||
it leaks to every other project that subscribes.
|
||||
|
||||
**Whichever home it gets, a rule needs `when_to_apply`.** It is the only thing
|
||||
that decides whether the rule is ever seen: nothing is preloaded, so a rule
|
||||
with no trigger is not a quiet rule, it is an unreachable one. Write the moment
|
||||
in the words a session actually produces — the command, the error, the
|
||||
half-formed ask — not the category it belongs to.
|
||||
|
||||
**First ask whether it's a rule at all.** A rule is prose you have to remember
|
||||
and apply; Scribe's other entities are structure a tool can resolve and check.
|
||||
|
||||
Reference in New Issue
Block a user