CI caught two things, and the first is the feature not working rather than a test being wrong. THE SIGNAL WAS GATED ON A COINCIDENCE. build_write_path_hint returns early when no prior art, stamp, divergence or derive matched, and that guard sat ABOVE the new arm — so a session whose rules had changed was told only if the file it happened to be editing also matched something else. A staleness signal that fires on that coincidence is not a staleness signal. The arm now runs above the guard, collecting into its own list that `lines` is seeded from, and the guard accounts for it. The standing-rule arm (milestone 307) is deliberately LEFT below that guard, and this is a finding rather than a fix: it has the same gating and probably should not, but it runs a SEMANTIC search, so lifting it would put an embedding query on every write in every session. That is a cost decision, not a bug fix, and not this task's to make. THE MARKER MUST NOT BREAK THE PAYLOAD IT DECORATES. rules_etag is computed on the SessionStart path, where `max()` raising costs the whole context payload — every rule title, the project, all of it — to save a hint. A row with no usable timestamp is now skipped and a set with none degrades to a count-only marker, which still catches a rule added or deleted and only loses edits. That is the right way round to lose information. CI found it because build_session_context's tests pass MagicMock rules and `max()` over those raises TypeError. Also: list_always_on_rules on an install with no always-on rulebooks returns `rules_etag: "empty|0"`. Its exact-dict test is updated rather than loosened — the key being present on an empty install is the behaviour, not noise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
14 KiB
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.
Using Scribe
Scribe is the operator's self-hosted system of record (notes, tasks, issues,
projects, milestones, systems) and rulebook, reachable
through the bundled scribe MCP server. Its value is mostly in what it
already holds — so make reading it a reflex, not something you wait to be
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.
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.
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 Scribe milestones
(via start_planning) — 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:falseor 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
-
Recall before acting. Before answering a question about the operator's work, or starting a task,
searchScribe (andlist_tasks/list_notes) for related prior work — an existing task, decision, or note — instead of re-deriving it or opening a duplicate. When a project is in scope, pass itsproject_idso results stay scoped. -
Standing rules are binding. Load them via
list_always_on_rules()at session start (see "Do this first"); treat every one as binding. Pull a rule's full statement withget_rule(id)when it's about to bite. When a project is in scope,enter_project(id)also returns its applicable rules. -
Update over duplicate. When recording, prefer updating an existing note/rule/task over creating a new one. Search first; revise what's there.
-
When you plan, plan in Scribe. Work with an arc — several steps toward one goal — gets a plan, and a plan is a milestone:
start_planning(project_id, title)creates one whosebodyholds the design, each step is its own task under it (create_task(milestone_id=...)), progress goes in work-logs (add_task_log). Work without an arc (a fix, a one-file change, a question) is just a task — don't wrap it in a milestone. Either way, do not write plans/specs to local.mdfiles. See the writing-plans skill. -
Keep state honest. Set a task
in_progresswhen you start it,donethe moment it's complete; log progress as you go. -
Fixes are issues, not work-logs. When you fix a problem — even one solved in passing — record it as its own issue (
create_task(kind="issue")) with symptom → root cause → fix, optionally linked to the task it arose from (arose_from_id) and the subsystem it touches (system_ids). Don't bury a fix as a work-log line on whatever task happened to be open. -
Tag records to Systems.
enter_projectlists the project's Systems — its named subsystems/areas. When you create or meaningfully update a record, ask which areas it is about and passsystem_ids. The test: would someone investigating that subsystem want this record in the pilelist_system_recordsreturns? If the area has no System yet, create one (create_system: name + a one-paragraph charter) — an area that plainly exists deserves naming the moment two records would share it; don't wait to be asked. Cross-cutting records — audits, sweeps, reviews — take several tags and are the prime discovery moment: a pass that walks the subsystems has just enumerated the vocabulary, so mint the Systems it names as it names them. Create liberally —create_systemis duplicate-gated, and that gate (plus reviewing the existing list) is the guardrail against sprawl, not restraint. Only a record genuinely about no particular area goes untagged. -
Name the record, never just its number. Whenever you refer to a Scribe record — in a message to the operator, a commit message, a task body, a work-log — write the id and its title:
#3244 "the staleness signal",milestone 323 "rule versioning". Not#3244.You have the record open; the operator does not. A bare id reads as complete to you and as homework to them — they have to look it up to know what their own conversation is about, or guess. Scribe's own duplicate gate already writes
id 412: "debounce helper"for exactly this reason; match it everywhere else.The first mention in a message carries the title; later mentions of the same record can use the bare id. If you don't know the title, look it up before citing the number — an id you can't name is one you haven't checked. This matters most in the places read later by someone with even less context than the operator has now: commit messages, task bodies, and any record that cites another.
-
State updates in place; chronicles don't. A dev-log records what happened — write it once, never rewrite it. A durable finding (how a subsystem works, a measured number) lives in that System's reference note ("«System» — reference"), which you UPDATE as facts change — safe, because every meaningful edit is snapshotted and the version history is the changelog. The dev-log then
[[links]]the reference note instead of restating state. When a new record outright corrects an older one (a re-measurement, a reversed decision), pass the old id insupersedesso the stale record is demoted and labelled rather than left competing. -
A few notes assert a FACT, and those can carry their own check. Supersession only fires once somebody has read a note and disagreed — which is the case where it was already believed. A note asserting something about someone else's software — what a service does on a duplicate upload, how a forge numbers its CI runs, what an updater compares — can instead carry
verify_with(how to check it) andexpires_when(the STATE that ends it: "when the forge numbers runs per workflow", never "in six months").notes_due_for_verificationlists them least-recently-confirmed first, with never-checked at the top;mark_note_verifiedrecords what you found, andstill_true=Falsedeliberately writes nothing — a note whose check failed is wrong rather than in a state worth recording, so it keeps its place.
The test is one question: could this note become false without anyone
editing it? If no, leave both fields empty. That is the normal case, and
an empty verify_with is the positive marker for "this is a decision, there
is nothing to go and check" — not an unfinished record. The sweep is only
worth reading while almost nothing is on it, so a check added out of
tidiness costs the whole surface, not just that note.
The sharper form of the same test: is the thing this note describes yours to change? If yes it is a decision — editing your own software is how it changes, and you will know you did it. Measured against a real corpus, every note that earned a check was about somebody ELSE's software: a signing service, a forge, a hub, an SDK, a model, a dependency set.
Three that look like candidates and are not:
- Resume pointers and "current state" notes. They go stale fastest of anything, which is exactly why they tempt — but the cure is to update or delete them, not to schedule a check. A sweep full of pointers is a sweep nobody reads.
- Measurements of your own system. They go false because you changed something, and you knew. A measurement earns a check only when what it measures is outside your control.
- A decision that RESTS on somebody else's behaviour. The decision is still a decision. Put the check on the note asserting the fact, and link the decision to it.
Not for tasks — a task's decay is its status, and a done issue records what
happened rather than asserting something that can go false. Not for snippets
either: verify_snippet compares the recorded location and code against the
repo, which is richer and already wired to drift detection.
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_idto 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.
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
answers — never create a project bare by default:
- which always-on rulebooks it should NOT inherit (
list_rulebooksshows which are always_on; default: inherit them all) →exclude_always_on_rulebooks=[...] - which other rulebooks to subscribe →
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
from day one →
seed_systems=true|false
If enter_project returns an inception key, the project was never decided
(it inherits its defaults silently): raise that ask once, with the defaults it
carries, then decide_project_inception(project_id, …). Existing projects
were stamped "legacy" (inherit-all) and do not ask; any project can be
re-decided. The rules/design-system/Systems tools still work one at a time —
inception is the moment they are decided together, and the record of why.
Where a new rule goes
When codifying a rule, pick its home by who it should bind — and keep shared homes general:
- Always-on rulebook (
create_rulein analways_onrulebook) — 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. - 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.
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. Visual standards belong in a design system, not a rulebook — a token can be inherited, resolved per mode, rendered to a stylesheet and diffed against code, and none of that survives being written as a rule. A repeatable procedure is a process; reusable code is a snippet. Reach for a rule when the thing really is a standing instruction about how to work.
Building UI: the project's design system binds
enter_project returns a design_system when the project has one, with the
guidance chain-merged — the house style it inherits plus its own departures
from it. Treat it the way you treat a rule.
Before writing a colour, size, radius, weight or duration by hand, reach for a
token: resolve_design_system(id) for the values, or
get_design_system_stylesheet(id) for the rendered sheet. A literal is a value
stated outside the system, so it can never follow a palette change — and
nothing will tell you it drifted.
Other Scribe process-skills
This plugin also ships focused process-skills — writing-plans, systematic debugging, verification, and brainstorming. Reach for the matching one when its situation arises, the same way you reach for this skill.