Merge pull request 'feat(plugin): Scribe replaces native memory by instruction; tighten project-scope discipline' (#70) from dev into main
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 49s
CI & Build / Build & push image (push) Successful in 15s

This commit was merged in pull request #70.
This commit is contained in:
2026-06-10 13:35:28 -04:00
4 changed files with 64 additions and 16 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "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.", "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" }, "author": { "name": "Bryan Van Deusen" },
"mcpServers": { "mcpServers": {
"scribe": { "scribe": {
+37
View File
@@ -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 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. 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 ## The reflex
1. **Recall before acting.** Before answering a question about the operator's 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 5. **Keep state honest.** Set a task `in_progress` when you start it, `done` the
moment it's complete; log progress as you go. 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 ## Where a new rule goes
When codifying a rule, pick its home by **who it should bind** — and keep When codifying a rule, pick its home by **who it should bind** — and keep
+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 you re-derive it or open a duplicate.
- Before creating a task, search for an existing one (search content_type= - Before creating a task, search for an existing one (search content_type=
'task') — don't open a second ticket for work already tracked. '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 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 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 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 deliberate cross-project sweep. get_recent takes no project filter and spans
server (each call is self-contained); carrying its id forward is on you. 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: 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 - 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 standard a CATEGORY of projects shares -> subscribed rulebook; a universal
norm -> always-on rulebook. Coordinate with the operator on which home fits. 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 One thing NOT to do: don't bridge Scribe into a session by writing to the
session won't reach for them unless its always-loaded context says to. So host's native memory. Rules are pull-only, so a fresh session won't reach for
when the current project subscribes to a rulebook (enter_project / them unless its always-loaded context says to — but the bridge for that is the
get_project reports subscribed_rulebooks), make sure the host's persistent Scribe plugin's SessionStart hook, which pushes the always-on rules +
memory — the CLAUDE.md / AGENTS.md / ~/.claude memory the client loads at active-project context into each session directly. So do NOT create or refresh
startup — carries a short pointer: that the engineering and workflow rules a "rules live in Scribe" pointer in CLAUDE.md / AGENTS.md / ~/.claude memory,
for this work live in Scribe and must be loaded via list_always_on_rules (or and do NOT keep rules, recall, or plans in those stores in parallel with Scribe
enter_project when a project is in scope), plus a one-line note of what the — Scribe holds the single copy. Native auto-memory stays for facts about the
current project is and what is in flight. Add or refresh that pointer when user; CLAUDE.md for codebase onboarding. Never make Scribe's correctness depend
it's missing or stale; never copy the rules' content into memory — the on the operator disabling a native function (e.g. autoMemoryEnabled): the
pointer plus project context is the whole job. This is what lets the next plugin must work with auto-memory at its default. If the plugin is ever removed
session reach for Scribe instead of trusting a stale local copy. 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) 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 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: Returns:
{"items": [{"id", "type", "title", "updated_at"}], "total": int} {"items": [{"id", "type", "title", "updated_at"}], "total": int}
Sorted by updated_at descending. 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() uid = current_user_id()
days = max(1, min(days, 90)) days = max(1, min(days, 90))