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 kbDupThresholdNote = ref("0.93");
const kbDupThresholdTask = ref("0.93"); const kbDupThresholdTask = ref("0.93");
// PLAN_MATCH_DEFAULT_THRESHOLD in services/dedup.py. // PLAN_MATCH_DEFAULT_THRESHOLD in services/dedup.py.
const kbPlanMatchThreshold = ref("0.90"); const kbPlanMatchThreshold = ref("0.80");
const savingKbInject = ref(false); const savingKbInject = ref(false);
const kbInjectSaved = 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)); 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 // 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. // 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 // Same `|| default` reasoning: falling back to 0 would surface every
// snippet in the corpus on every edit, which is the failure this knob fixes. // 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)); const wpT = Math.min(1, Math.max(0, Number(kbWritePathThreshold.value) || 0.68));
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "scribe", "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).", "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": { "author": {
"name": "Bryan Van Deusen" "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. plain.
- **End with the ask, in bold** — the one thing they need to decide or do. If - **End with the ask, in bold** — the one thing they need to decide or do. If
there is nothing, say so. 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 - **Plain words.** Use the operator's vocabulary, not the names you coined while
working. If a term has to appear, explain it once. working. If a term has to appear, explain it once.
- **Place the work in Scribe.** Name the task, issue or milestone it belongs to, - **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 | | **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" | | **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 | | **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? | | **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 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 verified**. If something could not be verified, say what and why here rather
than letting it read as passed. than letting it read as passed.
- **Needs you** — an action, an approval, a decision, or "nothing". If it's an - **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 - **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. 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 # project is exactly what adding steps to its existing plan needs, so a caller
# who reaches this gate can act on whatever it returns. # 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 # Its own threshold, as a setting (rule 25), and lower than the note gate's
# starts, because it asks the same question ("the same thing, reworded") with # 0.90 because plan documents are shaped differently: a milestone is embedded
# the same embedder. Plan documents are shaped differently, though: a milestone # as title, description and plan (embeddings.milestone_document), while the
# is embedded as title, description and plan (embeddings.milestone_document), # candidate usually has no description and carries its steps instead, so even
# while the candidate usually has no description and carries its steps # a faithful rewording never scores like a copy.
# 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 # Measured after the first deploy (2026-09-15, bge-small-en-v1.5, #4079): three
# gate. So the default is conservative and the operator can lower it. # 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_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: 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" 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(): def test_placement_comes_from_the_record():
"""The failure this milestone started from: a placement written from memory """The failure this milestone started from: a placement written from memory
reads exactly like a real one when it is wrong.""" reads exactly like a real one when it is wrong."""