Coherence survey fixes — instructions, read scope, pull telemetry, dedup #99
@@ -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 always-on rules + active-project context, process-skills (writing-plans, 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": "0.1.23",
|
||||
"version": "0.1.24",
|
||||
"author": { "name": "Bryan Van Deusen" },
|
||||
"mcpServers": {
|
||||
"scribe": {
|
||||
|
||||
@@ -51,5 +51,17 @@ for the operator's work, and as your own working memory across sessions.
|
||||
`/compact` (name what you logged). You can't run it yourself — surface the
|
||||
recommendation and let them decide. Suggest it at seams, not every turn.
|
||||
|
||||
**If two Scribe instruction surfaces disagree** — this file, the MCP server's
|
||||
tool instructions, the `using-scribe` skill — **follow the one that assumes
|
||||
least about its own delivery.** This file is the floor: it ships with the
|
||||
plugin and needs no API key and no network, so it still applies in exactly the
|
||||
session where the others never arrived. The others may elaborate on what is
|
||||
written here; they must not contradict it. Weigh a disagreement by which way it
|
||||
fails, not by which surface said more: doing something a push would also have
|
||||
covered costs one redundant call, while skipping it because you expected a push
|
||||
that never came means working without the operator's rules and not knowing.
|
||||
A contradiction between surfaces is a defect in the product — say so, so it
|
||||
gets recorded and fixed rather than silently arbitrated again next session.
|
||||
|
||||
If the Scribe tools are unavailable, say so rather than silently falling back
|
||||
to local notes.
|
||||
|
||||
+23
-12
@@ -174,19 +174,30 @@ written as a rule. A repeatable procedure is a PROCESS. Reusable code is a
|
||||
SNIPPET. Reach for a rule when the thing genuinely is a standing instruction
|
||||
about how to work, and nothing else can hold it.
|
||||
|
||||
Rules are pull-only: CALL list_always_on_rules() YOURSELF at the start of a
|
||||
session, before substantive work. Do not wait to be handed them. The plugin's
|
||||
SessionStart hook also pushes the always-on rules + active-project context, but
|
||||
that is a delivery optimisation, not the bridge — it can be absent (instance
|
||||
unreachable, key not configured, or the hook itself silently inert, which is
|
||||
what #2198 was) and its absence does not announce itself. When the push did
|
||||
arrive, the pull costs one redundant call; when it didn't, skipping the pull
|
||||
means working without the operator's binding rules and having no signal that
|
||||
you are. Those two costs are not comparable, so pull.
|
||||
|
||||
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.
|
||||
host's native memory. That explicit pull IS the bridge — it is written into
|
||||
every instruction surface the session already loads, so it needs no pointer on
|
||||
disk. 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,
|
||||
and a pointer Scribe cannot update is one that goes stale without anyone
|
||||
noticing. 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 both
|
||||
the push and these instructions, 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
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""The instruction surfaces must agree that the agent pulls the rules itself.
|
||||
|
||||
WHY THIS EXISTS
|
||||
|
||||
Rule #119 makes the instruction surfaces the SPECIFICATION for product
|
||||
behaviour — there is no other place the "load the operator's rules" obligation
|
||||
is written down, and no code path enforces it. So a surface that states it
|
||||
differently isn't a documentation slip; it is the product behaving differently.
|
||||
|
||||
That happened (#2497). `_INSTRUCTIONS` said the SessionStart hook "is the
|
||||
bridge" for getting rules into a session, while the `using-scribe` skill said to
|
||||
pull them yourself and treat any push as a bonus. An agent weighting the first
|
||||
would reasonably skip the pull.
|
||||
|
||||
#2198 is the case where that is wrong: every plugin hook was silently inert for
|
||||
an extended period, and nothing announced it. An agent trusting the push would
|
||||
have run with no binding rules and no signal — while those rules govern branch,
|
||||
commit, push and other hard-to-reverse actions.
|
||||
|
||||
The asymmetry is the whole argument, and it is what these tests pin: pulling
|
||||
when a push also arrived costs one redundant call; not pulling when the push
|
||||
never came costs the operator's rules entirely.
|
||||
|
||||
WHAT THIS DOES NOT DO
|
||||
|
||||
It cannot tell whether two surfaces contradict each other in prose generally —
|
||||
that needs a reader. It pins the one instruction whose absence is known to be
|
||||
load-bearing, and the specific shape the #2497 defect took: naming the push
|
||||
without also stating the pull.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
# The pull instruction, however a surface phrases the surrounding prose.
|
||||
PULL = "list_always_on_rules"
|
||||
|
||||
# Surfaces a session loads before substantive work. Hand-written because
|
||||
# "is this a session-start surface?" is an editorial fact, not a derivable one —
|
||||
# but each entry is asserted to EXIST, so a move or rename fails loudly here
|
||||
# instead of quietly dropping that surface from the check.
|
||||
SESSION_START_SURFACES = (
|
||||
ROOT / "src" / "scribe" / "mcp" / "server.py",
|
||||
ROOT / "plugin" / "hooks" / "scribe_static_context.md",
|
||||
ROOT / "plugin" / "skills" / "using-scribe" / "SKILL.md",
|
||||
)
|
||||
|
||||
|
||||
def _all_surfaces() -> list[tuple[str, str]]:
|
||||
"""(label, text) for every file a SESSION loads as instructions.
|
||||
|
||||
Deliberately not every markdown file under plugin/: `README.md` describes
|
||||
the push channel to the operator installing the plugin, and telling a human
|
||||
what the hook does is not the same act as telling an agent it need not pull.
|
||||
The boundary is "does a session read this", which is skills (loaded by
|
||||
description match), the hook-injected static context, and the MCP server's
|
||||
own instructions.
|
||||
"""
|
||||
found = [(str(p.relative_to(ROOT)), p.read_text())
|
||||
for p in (ROOT / "plugin" / "skills").rglob("SKILL.md")]
|
||||
found += [(str(p.relative_to(ROOT)), p.read_text())
|
||||
for p in (ROOT / "plugin" / "hooks").glob("*.md")]
|
||||
server = ROOT / "src" / "scribe" / "mcp" / "server.py"
|
||||
found.append((str(server.relative_to(ROOT)), server.read_text()))
|
||||
return found
|
||||
|
||||
|
||||
def test_every_session_start_surface_states_the_pull():
|
||||
missing = []
|
||||
for path in SESSION_START_SURFACES:
|
||||
assert path.exists(), (
|
||||
f"{path.relative_to(ROOT)} is gone — it was one of the surfaces "
|
||||
f"carrying the load-the-rules instruction. If it moved, update "
|
||||
f"SESSION_START_SURFACES; if it was retired, check the instruction "
|
||||
f"still lives somewhere a fresh session reads."
|
||||
)
|
||||
if PULL not in path.read_text():
|
||||
missing.append(str(path.relative_to(ROOT)))
|
||||
assert not missing, (
|
||||
f"these surfaces no longer tell the agent to call {PULL}(): {missing}. "
|
||||
f"The rules are pull-only and the push is best-effort, so a surface "
|
||||
f"that omits this leaves a session bound by nothing (#2198, #2497)."
|
||||
)
|
||||
|
||||
|
||||
def test_no_surface_names_the_push_without_stating_the_pull():
|
||||
"""The exact shape #2497 took.
|
||||
|
||||
Mentioning the SessionStart hook is fine and often useful. Mentioning it
|
||||
*instead of* the pull is the defect: it reads as "this is handled", and the
|
||||
surface that says so is the one an agent has least reason to doubt.
|
||||
"""
|
||||
offenders = [
|
||||
label for label, text in _all_surfaces()
|
||||
if "SessionStart" in text and PULL not in text
|
||||
]
|
||||
assert not offenders, (
|
||||
f"these surfaces describe the SessionStart push but never state the "
|
||||
f"explicit pull: {offenders}. The push is a delivery optimisation, not "
|
||||
f"the bridge — it can be absent without saying so. Name it if it helps, "
|
||||
f"but say to call {PULL}() regardless."
|
||||
)
|
||||
Reference in New Issue
Block a user