Coherence survey fixes — instructions, read scope, pull telemetry, dedup #99
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scribe",
|
"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.",
|
"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" },
|
"author": { "name": "Bryan Van Deusen" },
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"scribe": {
|
"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
|
`/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.
|
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
|
If the Scribe tools are unavailable, say so rather than silently falling back
|
||||||
to local notes.
|
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
|
SNIPPET. Reach for a rule when the thing genuinely is a standing instruction
|
||||||
about how to work, and nothing else can hold it.
|
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
|
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
|
host's native memory. That explicit pull IS the bridge — it is written into
|
||||||
them unless its always-loaded context says to — but the bridge for that is the
|
every instruction surface the session already loads, so it needs no pointer on
|
||||||
Scribe plugin's SessionStart hook, which pushes the always-on rules +
|
disk. So do NOT create or refresh a "rules live in Scribe" pointer in
|
||||||
active-project context into each session directly. So do NOT create or refresh
|
CLAUDE.md / AGENTS.md / ~/.claude memory, and do NOT keep rules, recall, or
|
||||||
a "rules live in Scribe" pointer in CLAUDE.md / AGENTS.md / ~/.claude memory,
|
plans in those stores in parallel with Scribe — Scribe holds the single copy,
|
||||||
and do NOT keep rules, recall, or plans in those stores in parallel with Scribe
|
and a pointer Scribe cannot update is one that goes stale without anyone
|
||||||
— Scribe holds the single copy. Native auto-memory stays for facts about the
|
noticing. Native auto-memory stays for facts about the user; CLAUDE.md for
|
||||||
user; CLAUDE.md for codebase onboarding. Never make Scribe's correctness depend
|
codebase onboarding. Never make Scribe's correctness depend on the operator
|
||||||
on the operator disabling a native function (e.g. autoMemoryEnabled): the
|
disabling a native function (e.g. autoMemoryEnabled): the plugin must work with
|
||||||
plugin must work with auto-memory at its default. If the plugin is ever removed
|
auto-memory at its default. If the plugin is ever removed the session loses both
|
||||||
the session loses this push and rebuilds context over time — an acceptable cost,
|
the push and these instructions, and rebuilds context over time — an acceptable
|
||||||
and far better than a silent settings change the operator may not know about.
|
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
|
||||||
|
|||||||
@@ -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