feat(reuse): recording gets a seam — the prior-art hook asks for create_snippet when duplication is proven and unrecorded (#2664)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Failing after 21s
CI & Build / TypeScript typecheck (push) Successful in 25s
CI & Build / Python tests (push) Failing after 31s
CI & Build / Build & push image (push) Skipped

Zero snippets were ever recorded outside sessions already thinking about
snippets: the read side had a real seam (the PreToolUse hook) and the record
side had a trailing clause of a floor bullet. The trigger moment — 'I just
wrote the second copy' — is mid-Write/Edit, so the nudge now rides the same
hook: when the local arm proves the definition exists elsewhere in the repo
AND Scribe returned no record of it, the context block asks for
create_snippet. Both gates or silence, so a brand-new helper and an
already-recorded one stay nudge-free.

Floor bullet promoted to name the trigger moments (extract, hoist, second
copy); guarded by test the same way the Systems reflex is. Plugin 0.1.29 so
the cache picks up the hook (#2209).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 21:45:32 -04:00
co-authored by Claude Fable 5
parent 77acee9239
commit fb757fb4ba
5 changed files with 95 additions and 6 deletions
+1 -1
View File
@@ -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.28",
"version": "0.1.29",
"author": { "name": "Bryan Van Deusen" },
"mcpServers": {
"scribe": {
+15
View File
@@ -196,6 +196,21 @@ if [ -n "$body" ]; then
fi
fi
# ARM 1½ — the RECORD nudge (#2664). The local arm just proved the thing being
# written already exists elsewhere in this repo, and Scribe returned no record
# of anything for it. That is the one moment "record it" is earned rather than
# noise: the duplication is demonstrated, not guessed. Gated on BOTH sides so
# an ordinary new helper (no other copies) and an already-recorded one (the
# server spoke) stay nudge-free — a reflex that fires on everything is one
# that gets skipped. An unreachable server counts as "nothing recorded": the
# local finding needed no server, and the nudge fails open with it.
if [ -n "$local_lines" ]; then
n_recorded=$(printf '%s' "$body" | jq -r '.note_ids | length' 2>/dev/null) || n_recorded=0
if [ "${n_recorded:-0}" = "0" ] || [ "$n_recorded" = "" ]; then
local_context="${local_context}"$'\n'"> None of those existing copies is recorded in Scribe. If the version being written is the canonical one — or this edit is consolidating the copies — record it now with create_snippet (name, code, when-to-reach-for-it, location) so the next session is offered it instead of writing another copy."
fi
fi
# Local first. It answers "this already EXISTS", which is a stronger claim than
# "this resembles something recorded" — and it is the one the recorded arms are
# structurally unable to make.
+10 -5
View File
@@ -52,11 +52,16 @@ for the operator's work, and as your own working memory across sessions.
it. An untagged project record carries the `systems_hint` question instead,
on creates, updates, and work-logs alike — treat it as the tagging question
asked at the moment of work, not as noise to skip past.
- **Reuse before rebuilding** — before writing a new helper/utility/component,
search recorded **snippets** (reusable code recorded once for recall) and
reuse the prior art instead of re-solving it; when you build something
reusable, record it with `create_snippet` (name, code, when-to-reach-for-it,
location) so a later session is offered it, not left to write it again.
- **Reuse before rebuilding — and record what you build** — before writing a
new helper/utility/component, search recorded **snippets** (reusable code
recorded once for recall) and reuse the prior art instead of re-solving it.
The recording half has NAMED TRIGGERS, not a vibe: the moment you extract a
shared component, hoist a helper into a common module, or notice you are
writing the second copy of anything, record it with `create_snippet` (name,
code, when-to-reach-for-it, location) in the same breath as the commit.
Work that "refactors X into a shared Y" is not finished until Y is recorded
— an unrecorded shared component is invisible to every later session, which
is how a codebase grows four `.btn-primary` definitions.
- Do **not** keep the operator's rules, plans, or project notes in local
memory / CLAUDE.md in parallel with Scribe — Scribe holds the single copy.
- **Compact at clean seams** — because you record as you go, a context
+19
View File
@@ -128,6 +128,25 @@ def test_floor_states_the_systems_reflex():
)
def test_floor_names_the_snippet_recording_triggers():
"""The recording half of reuse needs NAMED trigger moments on the floor.
#2664's behavioral finding: with recording guidance as a trailing clause of
the reuse bullet, zero snippets were ever recorded outside sessions already
thinking about snippets — extracting a shared component (Roundtable's
BaseModal) produced task prose and no record. The floor must name the
moments, not just the tool.
"""
floor = (ROOT / "plugin" / "hooks" / "scribe_static_context.md").read_text()
for needle in ("create_snippet", "second copy"):
assert needle in floor, (
f"plugin/hooks/scribe_static_context.md no longer states the "
f"snippet-recording trigger ({needle!r}) — the record-as-you-build "
f"reflex must be stated on the floor with its trigger moments "
f"(#2664)."
)
# Topics displaced from _INSTRUCTIONS when it was cut to fit the fold. Each
# must remain stated on at least one DELIVERED surface: a tool docstring
# (arrives with the tool schema), the plugin static context (always arrives),
+50
View File
@@ -789,3 +789,53 @@ def test_plugin_version_bumped_with_the_hook():
manifest = json.loads((PLUGIN / ".claude-plugin" / "plugin.json").read_text())
version = tuple(int(p) for p in manifest["version"].split("."))
assert version >= (0, 1, 18)
def test_hook_nudges_recording_when_copies_exist_but_nothing_is_recorded(tmp_path):
"""#2664: the local arm proves duplication; when Scribe has no record of it,
the same context block must ask for create_snippet — the one moment the
recording nudge is earned rather than noise. An unreachable server counts
as "nothing recorded": the local finding needed no server, and the nudge
fails open with it (here: a refused connection stands in for the instance)."""
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
(repo / "a.py").write_text("def debounce(fn):\n return fn\n")
# git grep searches the index, so the existing copy must be staged.
subprocess.run(["git", "add", "."], cwd=repo, check=True)
out = subprocess.run(
["bash", str(HOOK)],
input=json.dumps({
"session_id": "s-nudge", "cwd": str(repo), "tool_name": "Write",
"tool_input": {"file_path": str(repo / "b.py"),
"content": "def debounce(fn):\n return fn\n"},
}),
capture_output=True, text=True,
env={"PATH": "/usr/bin:/bin", "SCRIBE_URL": "http://127.0.0.1:9",
"SCRIBE_TOKEN": "t"},
)
assert out.returncode == 0
ctx = json.loads(out.stdout)["hookSpecificOutput"]["additionalContext"]
assert "already defined" in ctx # the duplication finding
assert "create_snippet" in ctx # the recording ask riding it
def test_hook_stays_quiet_about_recording_when_nothing_is_duplicated(tmp_path):
"""A brand-new helper with no other copies earns no nudge — a reflex that
fires on every Write is one sessions learn to skip."""
repo = tmp_path / "repo"
repo.mkdir()
subprocess.run(["git", "init", "-q"], cwd=repo, check=True)
out = subprocess.run(
["bash", str(HOOK)],
input=json.dumps({
"session_id": "s-quiet", "cwd": str(repo), "tool_name": "Write",
"tool_input": {"file_path": str(repo / "b.py"),
"content": "def debounce(fn):\n return fn\n"},
}),
capture_output=True, text=True,
env={"PATH": "/usr/bin:/bin", "SCRIBE_URL": "http://127.0.0.1:9",
"SCRIBE_TOKEN": "t"},
)
assert out.returncode == 0
assert "create_snippet" not in out.stdout