Merge pull request #461 from deseltrus/feat/multi-milestone-enforcement

feat(discuss): three-layer enforcement for multi-milestone discussions
This commit is contained in:
TÂCHES 2026-03-15 10:02:00 -06:00 committed by GitHub
commit 65acf67762
4 changed files with 108 additions and 5 deletions

View file

@ -23,6 +23,7 @@ const BASELINE_PATTERNS = [
".gsd/metrics.json",
".gsd/completed-units.json",
".gsd/STATE.md",
".gsd/DISCUSSION-MANIFEST.json",
// ── OS junk ──
".DS_Store",

View file

@ -50,13 +50,76 @@ 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 */ }
}
// Gate 4: Discussion manifest process verification (multi-milestone only)
// The LLM writes DISCUSSION-MANIFEST.json after each Phase 3 gate decision.
// If the manifest exists but gates_completed < total, the LLM hasn't finished
// presenting all readiness gates to the user — block auto-start.
const manifestPath = join(basePath, ".gsd", "DISCUSSION-MANIFEST.json");
if (existsSync(manifestPath)) {
try {
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
const total = typeof manifest.total === "number" ? manifest.total : 0;
const completed = typeof manifest.gates_completed === "number" ? manifest.gates_completed : 0;
if (total > 1 && completed < total) {
// Discussion not complete — block auto-start until all gates are done
return false;
}
// Cross-check manifest milestones against PROJECT.md if available
if (projectFile) {
const projectContent = readFileSync(projectFile, "utf-8");
const projectIds = parseMilestoneSequenceFromProject(projectContent);
const manifestIds = Object.keys(manifest.milestones ?? {});
const untracked = projectIds.filter(id => !manifestIds.includes(id));
if (untracked.length > 0) {
ctx.ui.notify(
`Discussion manifest missing gates for: ${untracked.join(", ")}`,
"warning",
);
}
}
} catch { /* malformed manifest — warn but don't block */ }
}
// 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 {
@ -64,11 +127,28 @@ export function checkAutoStartAfterDiscuss(): boolean {
if (draftFile) unlinkSync(draftFile);
} catch { /* non-fatal — stale draft doesn't break anything, CONTEXT.md wins */ }
// Cleanup: remove discussion manifest after auto-start (only needed during discussion)
try { unlinkSync(manifestPath); } catch { /* may not exist for single-milestone */ }
pendingAutoStart = null;
startAuto(ctx, pi, basePath, false, { step }).catch(() => {});
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;

View file

@ -227,6 +227,27 @@ For each remaining milestone **one at a time, in sequence**, use `ask_user_quest
Each context file (full or draft) should be rich enough that a future agent encountering it fresh — with no memory of this conversation — can understand the intent, constraints, dependencies, what this milestone unlocks, and what "done" looks like.
#### Milestone Gate Tracking (MANDATORY for multi-milestone)
After EVERY Phase 3 gate decision, immediately write or update `.gsd/DISCUSSION-MANIFEST.json` with the cumulative state. This file is mechanically validated by the system before auto-mode starts — if gates are incomplete, auto-mode will NOT start.
```json
{
"primary": "M001",
"milestones": {
"M001": { "gate": "discussed", "context": "full" },
"M002": { "gate": "discussed", "context": "full" },
"M003": { "gate": "queued", "context": "none" }
},
"total": 3,
"gates_completed": 3
}
```
Write this file AFTER each gate decision, not just at the end. Update `gates_completed` incrementally. The system reads this file and BLOCKS auto-start if `gates_completed < total`.
For single-milestone projects, do NOT write this file — it is only for multi-milestone discussions.
#### Phase 4: Finalize
7. Update `.gsd/STATE.md`

View file

@ -145,7 +145,8 @@ const guidedFlowSource = readFileSync(
);
const checkFnIdx = guidedFlowSource.indexOf("checkAutoStartAfterDiscuss");
const checkFnChunk = guidedFlowSource.slice(checkFnIdx, checkFnIdx + 1200);
const checkFnEnd = guidedFlowSource.indexOf("\nexport ", checkFnIdx + 1);
const checkFnChunk = guidedFlowSource.slice(checkFnIdx, checkFnEnd > checkFnIdx ? checkFnEnd : checkFnIdx + 5000);
assert(
checkFnChunk.includes("CONTEXT-DRAFT"),