feat(planning): start_planning hands back the active plan that already covers the work (#4079)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 1m10s
CI & Build / TypeScript typecheck (push) Successful in 1m15s
CI & Build / Python tests (push) Failing after 1m22s
CI & Build / Build & push image (push) Skipped

Step 4 of milestone 415 "An existing plan is found before a new one is made".
A session that could not see an existing plan made a second one beside it.
start_planning and create_milestone now ask first: an ACTIVE milestone in the
project with the same title, or one that reads as the same plan (title, design
and steps against milestone embeddings), is returned with its progress and a
pointer to create_records(milestone_id=...). Nothing is created; force=true
bypasses.

- dedup.find_matching_plan / plan_gate / plan_match_response; access-checked
  before either arm (rule 78), fail-open like the other gates.
- Done milestones never block; the semantic arm needs 200+ chars of candidate.
- kb_plan_match_threshold (default 0.90) is a setting, in the Settings view,
  and pinned against the Python default by test_settings_defaults_agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-15 13:43:18 -04:00
co-authored by Claude Opus 5
parent 3a501c2cac
commit 59407728e6
11 changed files with 468 additions and 22 deletions
+31
View File
@@ -104,6 +104,8 @@ const kbPromptRuleThreshold = ref("0.72");
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 savingKbInject = ref(false);
const kbInjectSaved = ref(false);
@@ -155,6 +157,9 @@ async function saveKbInject() {
const dupSnip = Math.min(1, Math.max(0, Number(kbDupThresholdSnippet.value) || 0.82));
const dupNote = Math.min(1, Math.max(0, Number(kbDupThresholdNote.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
// 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));
// 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));
@@ -171,6 +176,7 @@ async function saveKbInject() {
kbDupThresholdSnippet.value = String(dupSnip);
kbDupThresholdNote.value = String(dupNote);
kbDupThresholdTask.value = String(dupTask);
kbPlanMatchThreshold.value = String(planT);
kbWritePathThreshold.value = String(wpT);
kbRuleHintThreshold.value = String(rhT);
kbToolRuleThreshold.value = String(trT);
@@ -199,6 +205,7 @@ async function saveKbInject() {
kb_duplicate_threshold_snippet: String(dupSnip),
kb_duplicate_threshold_note: String(dupNote),
kb_duplicate_threshold_task: String(dupTask),
kb_plan_match_threshold: String(planT),
});
kbInjectSaved.value = true;
setTimeout(() => (kbInjectSaved.value = false), 2000);
@@ -662,6 +669,9 @@ onMounted(async () => {
if (allSettings.kb_duplicate_threshold_task !== undefined) {
kbDupThresholdTask.value = allSettings.kb_duplicate_threshold_task;
}
if (allSettings.kb_plan_match_threshold !== undefined) {
kbPlanMatchThreshold.value = allSettings.kb_plan_match_threshold;
}
if (allSettings.notify_task_reminders !== undefined) {
notifyTaskReminders.value = allSettings.notify_task_reminders !== "false";
}
@@ -1623,6 +1633,27 @@ async function deleteUser(userId: number) {
stays strict to keep the report pointed at work opened twice.
</p>
</div>
<div class="field">
<label for="kb-plan-match-threshold">Existing-plan match threshold</label>
<input
id="kb-plan-match-threshold"
v-model="kbPlanMatchThreshold"
type="number"
min="0"
max="1"
step="0.01"
class="fs-input input"
style="max-width: 8rem"
/>
<p class="field-hint">
How alike a new plan must be to an active milestone in the same project
before an agent is handed that milestone instead of creating a second
one. A plan with the same title always matches. Lower it if sessions
still open parallel plans for work that already has one; raise it if
they are sent to plans that are only related.
</p>
</div>
<div class="actions">
<button class="btn-primary" @click="saveKbInject" :disabled="savingKbInject">
{{ savingKbInject ? 'Saving' : 'Save' }}