feat(discuss): harden multi-milestone gates with two-layer enforcement

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) <noreply@anthropic.com>
This commit is contained in:
deseltrus 2026-03-15 09:07:55 +01:00
parent 3bfa444809
commit ccb2a08d67

View file

@ -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;