feat(plugin): Scribe replaces native memory by instruction; tighten project-scope discipline
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 32s
CI & Build / Python tests (push) Successful in 44s
CI & Build / Build & push image (push) Successful in 57s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 13:08:17 -04:00
parent 974fa6a215
commit 9eddb8497c
4 changed files with 64 additions and 16 deletions
+22 -15
View File
@@ -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
+4
View File
@@ -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))