diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 2647c35..73fa61c 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -105,7 +105,7 @@ const kbDupThresholdSnippet = ref("0.82"); const kbDupThresholdNote = ref("0.93"); const kbDupThresholdTask = ref("0.93"); // PLAN_MATCH_DEFAULT_THRESHOLD in services/dedup.py. -const kbPlanMatchThreshold = ref("0.90"); +const kbPlanMatchThreshold = ref("0.80"); const savingKbInject = ref(false); const kbInjectSaved = ref(false); @@ -159,7 +159,7 @@ async function saveKbInject() { const dupTask = Math.min(1, Math.max(0, Number(kbDupThresholdTask.value) || 0.93)); // Same `|| default` guard: a floor of 0 would hand back an existing plan // for every new one, and no plan could be started without force. - const planT = Math.min(1, Math.max(0, Number(kbPlanMatchThreshold.value) || 0.9)); + const planT = Math.min(1, Math.max(0, Number(kbPlanMatchThreshold.value) || 0.8)); // Same `|| default` reasoning: falling back to 0 would surface every // snippet in the corpus on every edit, which is the failure this knob fixes. const wpT = Math.min(1, Math.max(0, Number(kbWritePathThreshold.value) || 0.68)); diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index 0db0f27..e67ef7b 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe for Claude Code: connects the scribe MCP server, adds the hooks that deliver live project state and relevant records at the right moment, ships the shared client-neutral Scribe skills (using-scribe, writing-plans, reporting-back, systematic-debugging, verification, brainstorming, reusing-code, shape-accounting), and syncs your saved Scribe Processes as skills (/scribe:sync).", - "version": "2026.09.15.1744", + "version": "2026.09.15.1921", "author": { "name": "Bryan Van Deusen" }, diff --git a/plugin/skills/reporting-back/SKILL.md b/plugin/skills/reporting-back/SKILL.md index 90d91b5..a2f1288 100644 --- a/plugin/skills/reporting-back/SKILL.md +++ b/plugin/skills/reporting-back/SKILL.md @@ -23,6 +23,12 @@ when one is short — "**Needs you:** nothing" is an answer they were looking fo plain. - **End with the ask, in bold** — the one thing they need to decide or do. If there is nothing, say so. +- **A request for approval gets its own section, headed "Approval requested".** + Whenever you are holding an action until the operator says yes — a permission + prompt their client raised, something hard to undo, a change you have + prepared and not made — put it there, near the top, whatever kind of reply + this is. Inside a progress or completion list it reads as a status line, and + the operator does not see that you are waiting on them. - **Plain words.** Use the operator's vocabulary, not the names you coined while working. If a term has to appear, explain it once. - **Place the work in Scribe.** Name the task, issue or milestone it belongs to, @@ -76,6 +82,7 @@ default differ, the preference is what they asked for. | **Decision** | The question first · 2–4 options, each with what it changes · recommendation first | | **Clarification** | "My reading is X · the gap is Y · unless you say otherwise I'll do Z" | | **Handoff** (only they can do it) | The action · why it needs them · what it unblocks · what you'll do after · any way to skip it | +| **Approval** (you are ready to act and holding for a yes) | **Approval requested:** exactly what happens once they approve, one numbered item per change so they can approve part · why it needs their yes · how it can be undone · what you'll do after | | **Conflict** (what you're about to do clashes with a rule, a plan or an earlier decision) | What it says · what you were about to do · where they clash · A or B? | Before asking, check whether you can find the answer yourself — something that @@ -128,7 +135,8 @@ Notes on each section: verified**. If something could not be verified, say what and why here rather than letting it read as passed. - **Needs you** — an action, an approval, a decision, or "nothing". If it's an - action, give the reason with it. + action, give the reason with it. An approval you are holding for also gets + its own **Approval requested** section, and this line points at it. - **Next** — from `placement.next`, or say the milestone is finished. If you found something you didn't fix, the offer to fix it goes here. diff --git a/src/scribe/services/dedup.py b/src/scribe/services/dedup.py index dfa0cc9..ce93e8d 100644 --- a/src/scribe/services/dedup.py +++ b/src/scribe/services/dedup.py @@ -739,16 +739,22 @@ async def find_duplicate_rule( # project is exactly what adding steps to its existing plan needs, so a caller # who reaches this gate can act on whatever it returns. # -# Its own threshold, as a setting (rule 25). The note gate's 0.90 is where it -# starts, because it asks the same question ("the same thing, reworded") with -# the same embedder. Plan documents are shaped differently, though: a milestone -# is embedded as title, description and plan (embeddings.milestone_document), -# while the candidate usually has no description and carries its steps -# instead. That difference has not been measured, and a gate that blocks on -# noise teaches sessions to pass force=true every time, which is worse than no -# gate. So the default is conservative and the operator can lower it. +# Its own threshold, as a setting (rule 25), and lower than the note gate's +# 0.90 because plan documents are shaped differently: a milestone is embedded +# as title, description and plan (embeddings.milestone_document), while the +# candidate usually has no description and carries its steps instead, so even +# a faithful rewording never scores like a copy. +# +# Measured after the first deploy (2026-09-15, bge-small-en-v1.5, #4079): three +# plans reworded from existing active milestones scored 0.83, 0.85 and 0.87 +# against the milestone they restated; the nearest DIFFERENT plan for each, and +# a distinct plan on a neighbouring topic, scored 0.72-0.77. At 0.90 the +# semantic arm matched nothing, leaving only the title arm. 0.80 sits in the +# gap. A gate that blocks on noise teaches sessions to pass force=true every +# time, which is why it is not lower; four samples is thin, which is why the +# operator can move it. Retune alongside the embedder, not the corpus. PLAN_MATCH_THRESHOLD_KEY = "kb_plan_match_threshold" -PLAN_MATCH_DEFAULT_THRESHOLD = 0.90 +PLAN_MATCH_DEFAULT_THRESHOLD = 0.80 async def get_plan_match_threshold(user_id: int) -> float: diff --git a/tests/test_reporting_back_skill.py b/tests/test_reporting_back_skill.py index 7cdce13..da10576 100644 --- a/tests/test_reporting_back_skill.py +++ b/tests/test_reporting_back_skill.py @@ -38,6 +38,16 @@ def test_the_completion_report_keeps_its_sections(): assert section in text, f"the completion report lost its {section!r} section" +def test_a_request_for_approval_has_its_own_named_section(): + """The operator, 2026-09-15 (#4084): a session's go-ahead request sat inside + a completion list as "Blocked by the permission check", and read as a + fault rather than a question waiting on them. The heading is what they + scan for, so it is pinned by name.""" + text = _text() + assert "Approval requested" in text + assert "| **Approval**" in text, "the Asks table lost its Approval row" + + def test_placement_comes_from_the_record(): """The failure this milestone started from: a placement written from memory reads exactly like a real one when it is wrong."""