Merge pull request 'Plan gate default 0.80, and an "Approval requested" reply section' (#160) from dev into main
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Failing after 15s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m34s
CI & Build / Build & push image (push) Successful in 14s

This commit was merged in pull request #160.
This commit is contained in:
2026-09-15 15:23:01 -04:00
5 changed files with 37 additions and 13 deletions
+2 -2
View File
@@ -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));
+1 -1
View File
@@ -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"
},
+9 -1
View File
@@ -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 · 24 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.
+15 -9
View File
@@ -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:
+10
View File
@@ -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."""