From 9eddb8497c6c9d1d85b26a8b909f63988ad22208 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 10 Jun 2026 13:08:17 -0400 Subject: [PATCH] feat(plugin): Scribe replaces native memory by instruction; tighten project-scope discipline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direction change (operator, see plan task #755 work-log): the plugin must NOT depend on disabling a native Claude function to work. It earns its place by steering behavior, not by toggling autoMemoryEnabled. Memory doctrine (no dual-write): - using-scribe SKILL.md gains "Scribe holds these functions — don't keep a second copy": route rules/recall/planning to Scribe, don't also write them to native auto-memory, never instruct disabling a native function, and accept a "Scribe-shaped hole" if the plugin is removed (recover over time). - mcp/server.py _INSTRUCTIONS: drop the paragraph that told the model to create/refresh a "rules live in Scribe" pointer in CLAUDE.md / ~/.claude memory. That was an active dual-write instruction; the SessionStart hook is the bridge now. Replaced with the no-dual-write / no-settings-dependency doctrine. Supersedes plan #755 Phase 6 ("set autoMemoryEnabled:false"). Project-scope discipline (stop cross-project bleed): - using-scribe SKILL.md gains "Stay inside the active project's scope": pass project_id to every read, only reference/offer work on the in-scope project, ask before switching. - _INSTRUCTIONS scope bullet extended from reads to referencing/offering, and flags get_recent as cross-project. - get_recent docstring gains a scope note steering to scoped list_* when a project is active. plugin.json 0.1.4 -> 0.1.5 so clients' caches actually refresh (re-shipping under the same version does not bust the cache). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugin/.claude-plugin/plugin.json | 2 +- plugin/skills/using-scribe/SKILL.md | 37 +++++++++++++++++++++++++++++ src/scribe/mcp/server.py | 37 +++++++++++++++++------------ src/scribe/mcp/tools/recent.py | 4 ++++ 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index a77171e..4cae889 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe second brain 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, and a set of universal process-skills (brainstorm, debug, TDD, plan, verify). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "0.1.4", + "version": "0.1.5", "author": { "name": "Bryan Van Deusen" }, "mcpServers": { "scribe": { diff --git a/plugin/skills/using-scribe/SKILL.md b/plugin/skills/using-scribe/SKILL.md index 6932a72..1525626 100644 --- a/plugin/skills/using-scribe/SKILL.md +++ b/plugin/skills/using-scribe/SKILL.md @@ -24,6 +24,29 @@ 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. +## Scribe holds these functions — don't keep a second copy + +This plugin makes Scribe the home for the operator's **rules, recall, and +planning** — the jobs Claude's native auto-memory would otherwise do. When the +plugin is present, route those jobs to Scribe and **do not also write them to +native memory**: codify rules with `create_rule` / `create_project_rule`, +capture durable knowledge as Scribe notes, and keep plans in `kind=plan` tasks — +not in `MEMORY.md` or `CLAUDE.md`. One copy, in Scribe; let any existing local +memory shrink as Scribe takes over. Don't maintain both stores in parallel. + +Two constraints on *how* that's achieved: + +- **Steer behavior; never flip a native switch.** The plugin must work with + native auto-memory at its default (ON). Never tell the operator to set + `autoMemoryEnabled:false` or otherwise disable a built-in function to make + Scribe "win" — a setting the operator may not know was changed (and wouldn't + know to restore) is exactly the hidden breakage to avoid. You replace memory's + functions by *doing the work in Scribe*, not by turning memory off. +- **A Scribe-shaped hole is acceptable.** If the plugin is later removed, the + operator recovers context over time — that's fine. You do **not** need to keep + native memory as a self-sufficient fallback. The only thing to avoid is + breakage caused by a settings change the operator didn't make knowingly. + ## The reflex 1. **Recall before acting.** Before answering a question about the operator's @@ -48,6 +71,20 @@ explicit pull. Rules loaded this way are **binding** for the session. 5. **Keep state honest.** Set a task `in_progress` when you start it, `done` the moment it's complete; log progress as you go. +## Stay inside the active project's scope + +Once a project is in scope — you called `enter_project`, or the working repo is +bound — confine the session to it: + +- **Pass that `project_id` to every read** (`search`, `list_tasks`, + `list_notes`). An unscoped read bleeds every other project's work into your + context. +- **Only reference or offer work on the in-scope project.** Don't surface, + suggest, or start work on other projects unless the operator explicitly widens + scope. +- If something clearly belongs to a *different* project, say so and **ask before + switching** — never silently operate cross-project. + ## Where a new rule goes When codifying a rule, pick its home by **who it should bind** — and keep diff --git a/src/scribe/mcp/server.py b/src/scribe/mcp/server.py index 0c2ba81..de6b7b8 100644 --- a/src/scribe/mcp/server.py +++ b/src/scribe/mcp/server.py @@ -48,12 +48,18 @@ not something you wait to be asked for: before you re-derive it or open a duplicate. - Before creating a task, search for an existing one (search content_type= 'task') — don't open a second ticket for work already tracked. -- Scope reads to the project in scope. When a project is active (you called +- Scope to the project in scope. When a project is active (you called enter_project), pass its project_id to search / list_tasks / list_notes so results stay inside that project. Querying with no project_id pulls in every project and bleeds unrelated work into the session — only do it for a - deliberate cross-project sweep. The active project does not stick on the - server (each call is self-contained); carrying its id forward is on you. + deliberate cross-project sweep. get_recent takes no project filter and spans + every project; when one is active, prefer the scoped list_* tools over it. + And this is not only about reads: once a project is in scope, only reference + or offer work on THAT project — don't surface or propose work from other + projects unless the operator widens scope. If something clearly belongs to a + different project, say so and ask before switching; never silently operate + cross-project. The active project does not stick on the server (each call is + self-contained); carrying its id forward is on you. Keep task state honest — this is what makes the project a trustworthy record: - When you begin working a task, set it to in_progress (update_task @@ -101,18 +107,19 @@ of thumb: names a specific project's files/paths/quirks -> project rule; a standard a CATEGORY of projects shares -> subscribed rulebook; a universal norm -> always-on rulebook. Coordinate with the operator on which home fits. -That boundary cuts the other way too. Because rules are pull-only, a fresh -session won't reach for them unless its always-loaded context says to. So -when the current project subscribes to a rulebook (enter_project / -get_project reports subscribed_rulebooks), make sure the host's persistent -memory — the CLAUDE.md / AGENTS.md / ~/.claude memory the client loads at -startup — carries a short pointer: that the engineering and workflow rules -for this work live in Scribe and must be loaded via list_always_on_rules (or -enter_project when a project is in scope), plus a one-line note of what the -current project is and what is in flight. Add or refresh that pointer when -it's missing or stale; never copy the rules' content into memory — the -pointer plus project context is the whole job. This is what lets the next -session reach for Scribe instead of trusting a stale local copy. +One thing NOT to do: don't bridge Scribe into a session by writing to the +host's native memory. Rules are pull-only, so a fresh session won't reach for +them unless its always-loaded context says to — but the bridge for that is the +Scribe plugin's SessionStart hook, which pushes the always-on rules + +active-project context into each session directly. So do NOT create or refresh +a "rules live in Scribe" pointer in CLAUDE.md / AGENTS.md / ~/.claude memory, +and do NOT keep rules, recall, or plans in those stores in parallel with Scribe +— Scribe holds the single copy. Native auto-memory stays for facts about the +user; CLAUDE.md for codebase onboarding. Never make Scribe's correctness depend +on the operator disabling a native function (e.g. autoMemoryEnabled): the +plugin must work with auto-memory at its default. If the plugin is ever removed +the session loses this push and rebuilds context over time — an acceptable cost, +and far better than a silent settings change the operator may not know about. When you are working on a specific project, call enter_project(project_id) ONCE at session start (or whenever the active project changes). It returns the diff --git a/src/scribe/mcp/tools/recent.py b/src/scribe/mcp/tools/recent.py index 682fbc7..bc49fe2 100644 --- a/src/scribe/mcp/tools/recent.py +++ b/src/scribe/mcp/tools/recent.py @@ -30,6 +30,10 @@ async def get_recent(days: int = 7, limit: int = 25) -> dict: Returns: {"items": [{"id", "type", "title", "updated_at"}], "total": int} Sorted by updated_at descending. + + Scope note: this spans ALL projects and takes no project filter. When a + project is in scope, prefer list_tasks(project_id=...) / + list_notes(project_id=...) so you don't surface other projects' activity. """ uid = current_user_id() days = max(1, min(days, 90))