From 6c1fd281797231536414cdd74449933fd7ef1b92 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 14 Sep 2026 10:39:27 -0400 Subject: [PATCH] fix(#4022): instruction surfaces stop describing the always-on tier milestone 394 removed Skill bodies and tool docstrings still taught the deleted model: using-scribe said always-on rules "arrive whether or not you ask" and that SessionStart may inject a rule index; create_project, decide_project_inception and enter_project said an undecided project gets "every always-on rulebook"; create_rule pointed standards at "the always-on one"; the verification sweeps and retrieval_telemetry listed always-on paths and a live preload. Every passage now describes the current model: every rule is retrieved, a rulebook binds only by subscription, an undecided project inherits nothing, and the preload survives only in telemetry rows older than 394. Also repairs three sentences left half-replaced by the 394 edits: the static context's "If you have not loaded the no rule has arrived", create_rule's "an A subscribed rulebook", and create_project's doubled subscribe_rulebooks entry. Plugin version minted. Co-Authored-By: Claude Opus 5 (1M context) --- plugin/.claude-plugin/plugin.json | 2 +- plugin/hooks/scribe_static_context.md | 6 +++--- plugin/skills/using-scribe/SKILL.md | 26 ++++++++++++-------------- src/scribe/mcp/tools/notes.py | 4 ++-- src/scribe/mcp/tools/projects.py | 23 +++++++++++------------ src/scribe/mcp/tools/rulebooks.py | 11 +++++------ src/scribe/mcp/tools/search.py | 23 ++++++++++++----------- 7 files changed, 46 insertions(+), 49 deletions(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 6aeb9d5..7643c6c 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.1411", + "version": "2026.09.14.1438", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/hooks/scribe_static_context.md b/plugin/hooks/scribe_static_context.md index c2cb568..a6b2172 100644 --- a/plugin/hooks/scribe_static_context.md +++ b/plugin/hooks/scribe_static_context.md @@ -17,9 +17,9 @@ for the operator's work, and as your own working memory across sessions. 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 you have not loaded the - 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 + 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** diff --git a/plugin/skills/using-scribe/SKILL.md b/plugin/skills/using-scribe/SKILL.md index 124134f..d05e69e 100644 --- a/plugin/skills/using-scribe/SKILL.md +++ b/plugin/skills/using-scribe/SKILL.md @@ -27,10 +27,9 @@ If the working repo maps to a Scribe project (you're in a known repo, or 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 -unreachable, or the token didn't reach the hook), so the reliable path is this -explicit pull. Rules loaded this way are **binding** for the session. +Do this actively. Nothing is handed to a session up front to stand in for it — +rules arrive by retrieval, when your work or the operator's message matches +one — so asking and entering the project are the reliable path. ## Scribe holds these functions — don't keep a second copy @@ -88,17 +87,16 @@ Two constraints on *how* that's achieved: asks. If what you learned is that something MUST be done a certain way, that is a rule to propose, not a preference to harden in place. - Rules come in two tiers. **Always-on** rules are delivered — they arrive - whether or not you ask. **Conditional** rules are RETRIEVED, and one binds - just as hard for never having been handed to you. So before a consequential - act, `search(content_type="rule")` on what you are about to do. An empty - loaded set is not evidence that no rule applies; it is only evidence that - none was pushed, and those are different claims. + Every rule is RETRIEVED: one reaches you when a command, the code you are + writing or the operator's message resembles what it is about, and a rule + binds just as hard for never having been handed to you. So before a + consequential act, `search(content_type="rule")` on what you are about to + do. An empty session is not evidence that no rule applies; it is only + evidence that nothing has matched yet, and those are different claims. - The tier split exists because delivery does not scale: every resident rule - costs tokens in every session forever, so a rulebook that grows past a few - dozen either stops growing or stops fitting. Retrieval is what lets the - rulebook keep growing — but retrieval only fires if something asks. + Retrieval is what lets a rulebook keep growing — a rule costs nothing in a + session it has nothing to do with — but retrieval only fires if something + asks. **Ask hardest where you feel most certain.** Rules about which TOOL to reach for — use the forge's MCP client rather than curling its API, don't stand up diff --git a/src/scribe/mcp/tools/notes.py b/src/scribe/mcp/tools/notes.py index f4df26a..4cc68ed 100644 --- a/src/scribe/mcp/tools/notes.py +++ b/src/scribe/mcp/tools/notes.py @@ -384,8 +384,8 @@ async def notes_due_for_verification( thing there is. 0 = no age filter. project_id: narrow to one project. 0 = every project. Unlike the rules sweep, this filter is safe: a note belongs to at most one project - outright, with none of the subscription and always-on paths that - would make a project filter UNDER-report a rule. + outright, with none of the subscription paths that would make a + project filter UNDER-report a rule. never_only: only notes nobody has ever verified. """ uid = current_user_id() diff --git a/src/scribe/mcp/tools/projects.py b/src/scribe/mcp/tools/projects.py index e1e8c98..1dfc43b 100644 --- a/src/scribe/mcp/tools/projects.py +++ b/src/scribe/mcp/tools/projects.py @@ -83,7 +83,7 @@ async def enter_project(project_id: int) -> dict: `inception` (milestone 297) appears ONLY when the project is yours and nobody has decided what it inherits: it carries the current defaults - (which always-on rulebooks bind, design system, Systems), what to ask the + (the rulebooks it could subscribe to, design system, Systems), what to ask the operator — once — and the decide_project_inception call that answers it; it repeats on every enter until a decision is recorded. @@ -149,7 +149,7 @@ async def enter_project(project_id: int) -> dict: ) # The inception ask (milestone 297): a project nobody has decided on - # inherits its defaults silently — always-on rulebooks, no design system, + # inherits nothing, silently — no rulebook subscriptions, no design system, # no Systems. Owner-only (deciding is the owner's), and only until a # decision is recorded; the key is ABSENT otherwise (#2483). inception_ask = None @@ -288,8 +288,8 @@ async def create_project( before calling, ask the operator the four inception questions and pass the answers; a project created without any of them is UNDECIDED and enter_project will ask until decide_project_inception records it. - Defaults if nobody decides: every always-on rulebook binds, nothing is - subscribed, no design system, no Systems. + Defaults if nobody decides: no rulebook subscriptions, no design system, + no Systems. Args: title: Project name (required). @@ -297,11 +297,10 @@ async def create_project( goal: The desired outcome or definition of done for the project. status: one of active (default), paused, completed, archived. color: Optional hex colour for the project card (e.g. "#6366f1"). - subscribe_rulebooks: rulebook ids this project opts into. Since - milestone 394 subscription is the only way a rulebook binds a - project, so there is no automatic tier left to decline. Was - NOT inherit ([] = inherit them all). list_rulebooks shows which are - subscribe_rulebooks: rulebook ids to subscribe (the non-always-on ones). + subscribe_rulebooks: rulebook ids this project opts into. + Subscription is the only way a rulebook binds a project, so a + rulebook left out simply does not apply. list_rulebooks shows + which exist. design_system_id: the design system this project's UI is built from (list_design_systems); -1 = explicitly none; 0 = not stated. seed_systems: true mints the standard starter Systems (CI & Release, @@ -351,9 +350,9 @@ async def decide_project_inception( unsubscribe_project_from_rulebook to undo one), replaces the design system, and never re-seeds Systems a project already has. - Args: as create_project's inception args. Passing nothing records an - inherit-all decision (every always-on rulebook binds, no subscriptions, - no design system, no seed) — a valid answer, stated. + Args: as create_project's inception args. Passing nothing records a + decision to take nothing (no subscriptions, no design system, no seed) — + a valid answer, stated. """ uid = current_user_id() choices = _inception_choices( diff --git a/src/scribe/mcp/tools/rulebooks.py b/src/scribe/mcp/tools/rulebooks.py index db505fd..686119e 100644 --- a/src/scribe/mcp/tools/rulebooks.py +++ b/src/scribe/mcp/tools/rulebooks.py @@ -308,13 +308,12 @@ async def create_rule( and let the answer stand; re-raising a declined proposal argues a rule into existence, which is the thing this whole loop exists to prevent. - A rulebook rule is shared by every project that gets the rulebook: an - A subscribed rulebook binds the - projects that opt in. So a rulebook rule must read as a general standard — + A rulebook rule is shared by every project subscribed to the rulebook, so + it must read as a general standard — never pin it to one project's files, paths, or quirks. For a rule that applies to a single project only, use create_project_rule instead (no rulebook+topic ceremony). If it's a standard a CATEGORY of projects shares, - put it in a themed subscribed rulebook, not the always-on one. + put it in a rulebook for that category and subscribe those projects to it. Write it general WITHOUT hedging for the exceptions. A project that needs to strengthen, narrow or replace this rule writes its own and links it @@ -1048,8 +1047,8 @@ async def rules_due_for_verification( never_only: only rules nobody has ever verified. NOT filterable by project, deliberately: a project reaches rules through - project scope, subscriptions, always-on rulebooks and exclusions, and a - filter that missed one of those paths would UNDER-report — which is the + project scope and rulebook subscriptions, and a filter that missed one of + those paths would UNDER-report — which is the exact failure this whole surface exists to prevent. Read the whole list. """ uid = current_user_id() diff --git a/src/scribe/mcp/tools/search.py b/src/scribe/mcp/tools/search.py index df89e19..ace9009 100644 --- a/src/scribe/mcp/tools/search.py +++ b/src/scribe/mcp/tools/search.py @@ -280,19 +280,20 @@ It is an UPPER BOUND per surface: a pull records the door it came `surfaced` VS `ambient` IS THE READING THAT MATTERS HERE. `surfaced` counts rules a ranker chose — today only the write-path arm — and those are claims - a pull can settle. `ambient` counts BULK DELIVERIES: the SessionStart - preload and the `rules_payload` surfaces - (`enter_project`, `get_project`, `get_milestone`, `start_planning`, - `get_task`), which hand over the whole applicable set at once with nobody - choosing anything. A large `ambient` says the resident set is big and - arrives often — never that it is useful, and never that it is read. + a pull can settle. `ambient` counts BULK DELIVERIES: the `rules_payload` + surfaces (`enter_project`, `get_project`, `get_milestone`, + `start_planning`, `get_task`), which hand over the whole applicable set at + once with nobody choosing anything — plus, in rows older than milestone + 394, the SessionStart preload it removed. A large `ambient` says a bulk set + is big and arrives often — never that it is useful, and never that it is + read. `pull_through` therefore divides by `surfaced` alone. Fold the preload in - and growing the always-on set would depress the arm's measured precision - while trimming it would flatter it, for reasons having nothing to do with - the arm. To judge the PRELOAD instead, compare `ambient` against pulls of - those same rules over time: a resident set surfaced thousands of times and - opened never is the dead-weight signal, one tier up. + and growing a bulk set would depress the arm's measured precision while + trimming it would flatter it, for reasons having nothing to do with the + arm. To judge a BULK surface instead, compare `ambient` against pulls of + those same rules over time: a set surfaced thousands of times and opened + never is the dead-weight signal, one level up. Read it against `sources["write_path_rule"]`. That arm was once believed never to decline — the reading that scoped #3311 — but it was the arm's