From ccb2a08d6776f95509d286924c29cf04eeb3c9ff Mon Sep 17 00:00:00 2001 From: deseltrus Date: Sun, 15 Mar 2026 09:07:55 +0100 Subject: [PATCH] feat(discuss): harden multi-milestone gates with two-layer enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layer 1 (Prompt): discuss.md now enforces: - Document ingestion rule: read ALL user-provided files before reflection - Mandatory milestone confirmation gate via ask_user_questions - 1M context awareness: prefer discussing all milestones in-session - Phase 3 gates marked MANDATORY with progress tracking - Default-recommend "Discuss now" over "Draft for later" Layer 2 (Code): checkAutoStartAfterDiscuss() now validates: - Gate 1: Primary CONTEXT.md exists - Gate 2: STATE.md exists (written last in Phase 4, prevents premature auto-start during Phase 3 readiness gates) - Gate 3: Multi-milestone completeness check against PROJECT.md milestone sequence — warns if milestones are missing from filesystem Also fixes conflict markers in discuss.md from gsd/M005/S05 merge. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/guided-flow.ts | 53 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 5ad3cc766..f8ddb66ed 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -50,13 +50,44 @@ export function checkAutoStartAfterDiscuss(): boolean { const { ctx, pi, basePath, milestoneId, step } = pendingAutoStart; - // Don't fire until the discuss phase has actually produced a context file - // for the milestone being discussed. agent_end fires after every LLM turn, - // including the initial "What do you want to build?" response — we need to - // wait for the full conversation to complete and the LLM to write CONTEXT.md. + // Gate 1: Primary milestone must have CONTEXT.md const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT"); if (!contextFile) return false; // no context yet — keep waiting + // Gate 2: STATE.md must exist — written as the last step in the discuss + // output phase. This prevents auto-start from firing during Phase 3 + // (sequential readiness gates for remaining milestones) in multi-milestone + // discussions, where M001-CONTEXT.md exists but M002/M003 haven't been + // processed yet. + const stateFile = resolveGsdRootFile(basePath, "STATE"); + if (!stateFile) return false; // discussion not finalized yet + + // Gate 3: Multi-milestone completeness warning + // Parse PROJECT.md for milestone sequence, warn if any are missing context. + // Don't block — milestones can be intentionally queued without context. + const projectFile = resolveGsdRootFile(basePath, "PROJECT"); + if (projectFile) { + try { + const projectContent = readFileSync(projectFile, "utf-8"); + const milestoneIds = parseMilestoneSequenceFromProject(projectContent); + if (milestoneIds.length > 1) { + const missing = milestoneIds.filter(id => { + const hasContext = !!resolveMilestoneFile(basePath, id, "CONTEXT"); + const hasDraft = !!resolveMilestoneFile(basePath, id, "CONTEXT-DRAFT"); + const hasDir = existsSync(join(basePath, ".gsd", "milestones", id)); + return !hasContext && !hasDraft && !hasDir; + }); + if (missing.length > 0) { + ctx.ui.notify( + `Multi-milestone validation: ${missing.join(", ")} not found in filesystem. ` + + `Discussion may not have completed all readiness gates.`, + "warning", + ); + } + } + } catch { /* non-fatal — PROJECT.md parsing failure shouldn't block auto-start */ } + } + // Draft promotion cleanup: if a CONTEXT-DRAFT.md exists alongside the new // CONTEXT.md, delete the draft — it's been consumed by the discussion. try { @@ -69,6 +100,20 @@ export function checkAutoStartAfterDiscuss(): boolean { return true; } +/** + * Extract milestone IDs from PROJECT.md milestone sequence table. + * Looks for rows like "| M001 | Name | Status |" and extracts the ID column. + */ +function parseMilestoneSequenceFromProject(content: string): string[] { + const ids: string[] = []; + const lines = content.split(/\r?\n/); + for (const line of lines) { + const match = line.match(/^\|\s*(M\d{3}[A-Z0-9-]*)\s*\|/); + if (match) ids.push(match[1]); + } + return ids; +} + // ─── Types ──────────────────────────────────────────────────────────────────── type UIContext = ExtensionContext;