From e0328f2b1c4364be6cd5c9f672e54aa03755199a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 28 Jul 2026 09:08:17 -0400 Subject: [PATCH] =?UTF-8?q?feat(plugin):=20write-path=20trigger=20?= =?UTF-8?q?=E2=80=94=20offer=20prior=20art=20before=20code=20is=20rewritte?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The milestone headline. Auto-inject fires on the operator's prompt; the moment reuse is actually lost is later, when the agent decides mid-task to write a helper. A PreToolUse hook on Write|Edit now fires there. Channel: `additionalContext` with NO permissionDecision, so the note reaches Claude beside the tool result and the write is never blocked — a recall aid must not be able to stop the operator's work. Plain stdout would have been invisible to the model, and deny/ask would have made a nudge into a gate. Two arms, different in kind: - BY PLACE — a snippet recorded at this path (or its directory) is prior art by definition, not resemblance, so it is neither scored nor thresholded. This is what #2083's reverse lookup was built to answer. - BY MEANING — semantic search restricted to snippets (new `note_type` filter on semantic_search_notes) over the code about to be written. Place ranks first; the top-k cap spans both arms. Gates carried over from milestone 93 verbatim: threshold, margin, session dedup, titles-never-bodies. Own `source='write_path'` in retrieval_logs so precision is tunable separately — the docstring records that the place arm is unlogged and hands that to #2085. Its own on/off in Settings but the SAME threshold/top-k: one "how loud may Scribe be" knob is easier to reason about than two that drift, and splitting them later is then a data-backed change rather than a guess. Details worth keeping: the hook sends a REPO-RELATIVE path because that is how locations are recorded; the git remote resolves to a project and is never used as the location `repo` filter (different namespaces, would silently match nothing); the endpoint stays a GET because a read-scoped API key cannot POST and every other hook depends on that. plugin.json 0.1.17 -> 0.1.18. Refs #2082, milestone #232. --- frontend/src/views/SettingsView.vue | 18 ++ plugin/.claude-plugin/plugin.json | 2 +- plugin/README.md | 14 +- plugin/hooks/hooks.json | 11 + plugin/hooks/scribe_prior_art.sh | 119 ++++++++ plugin/skills/reusing-code/SKILL.md | 5 + src/scribe/routes/plugin.py | 47 +++ src/scribe/services/embeddings.py | 9 + src/scribe/services/plugin_context.py | 183 ++++++++++++ tests/test_write_path_trigger.py | 405 ++++++++++++++++++++++++++ 10 files changed, 810 insertions(+), 3 deletions(-) create mode 100755 plugin/hooks/scribe_prior_art.sh create mode 100644 tests/test_write_path_trigger.py diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 4d3b3b0..1321cc4 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -22,6 +22,7 @@ const retentionSaved = ref(false); const kbInjectEnabled = ref(true); const kbInjectThreshold = ref("0.55"); const kbInjectTopK = ref("3"); +const kbWritePathEnabled = ref(true); const savingKbInject = ref(false); const kbInjectSaved = ref(false); @@ -76,6 +77,9 @@ async function saveKbInject() { kb_autoinject_enabled: kbInjectEnabled.value ? 'true' : 'false', kb_autoinject_threshold: String(t), kb_autoinject_top_k: String(k), + // Its own switch, but deliberately the same threshold/ceiling — see + // WRITEPATH_ENABLED_KEY in services/plugin_context.py. + kb_writepath_enabled: kbWritePathEnabled.value ? 'true' : 'false', }); kbInjectSaved.value = true; setTimeout(() => (kbInjectSaved.value = false), 2000); @@ -459,6 +463,7 @@ onMounted(async () => { if (allSettings.kb_autoinject_top_k !== undefined) { kbInjectTopK.value = allSettings.kb_autoinject_top_k; } + kbWritePathEnabled.value = allSettings.kb_writepath_enabled !== "false"; if (allSettings.notify_task_reminders !== undefined) { notifyTaskReminders.value = allSettings.notify_task_reminders !== "false"; } @@ -1193,6 +1198,19 @@ function formatUserDate(iso: string): string { />

Ceiling on titles surfaced at once (1–10).

+
+ +

+ Checks the file Claude is about to write or edit against your recorded + snippets — what's already kept at that path, and what resembles the code + being written — so a helper you already have is offered before it's + rewritten. Uses the same threshold and ceiling above, and never blocks the + edit. Off = prior art surfaces only on your own prompts. +

+