fix: auto-dispatch discussion instead of hard-stopping on needs-discussion phase (#1820)

When a milestone has a CONTEXT-DRAFT.md (from multi-milestone queue creation)
or no CONTEXT.md at all, auto-mode previously hard-stopped with a message
telling the user to 'Run /gsd to discuss.' This created a dead end because:

1. /gsd (bare) routes to startAuto(step:true), which hits the same stop rule
2. /gsd auto also hits the same stop rule
3. Only /gsd discuss works, but the stop message doesn't mention it

This change replaces both hard-stop rules with discuss-milestone dispatch:
- 'needs-discussion → stop' becomes 'needs-discussion → discuss-milestone'
- 'pre-planning (no context) → stop' becomes 'pre-planning (no context) → discuss-milestone'

The new discuss-milestone unit type:
- Uses the guided-discuss-milestone prompt template
- Inlines CONTEXT-DRAFT.md as seed material when present
- Interviews the user and writes CONTEXT.md
- After CONTEXT.md exists, deriveState() returns 'pre-planning' and the
  normal research → plan → execute pipeline continues automatically

Supporting changes:
- auto-prompts.ts: new buildDiscussMilestonePrompt() function
- auto-dashboard.ts: discuss-milestone labels for status display
- complexity-classifier.ts: discuss-milestone classified as 'standard' tier
- auto-recovery.ts: expected artifact = CONTEXT.md for discuss-milestone
This commit is contained in:
deseltrus 2026-03-21 19:38:27 +01:00 committed by GitHub
parent b2fb12813f
commit e94bda817f
5 changed files with 52 additions and 11 deletions

View file

@ -67,6 +67,7 @@ export interface AutoDashboardData {
export function unitVerb(unitType: string): string {
if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
switch (unitType) {
case "discuss-milestone": return "discussing";
case "research-milestone":
case "research-slice": return "researching";
case "plan-milestone":
@ -84,6 +85,7 @@ export function unitVerb(unitType: string): string {
export function unitPhaseLabel(unitType: string): string {
if (unitType.startsWith("hook/")) return "HOOK";
switch (unitType) {
case "discuss-milestone": return "DISCUSS";
case "research-milestone": return "RESEARCH";
case "research-slice": return "RESEARCH";
case "plan-milestone": return "PLAN";
@ -108,6 +110,7 @@ function peekNext(unitType: string, state: GSDState): string {
const sid = state.activeSlice?.id ?? "";
if (unitType.startsWith("hook/")) return `continue ${sid}`;
switch (unitType) {
case "discuss-milestone": return "research or plan milestone";
case "research-milestone": return "plan milestone roadmap";
case "plan-milestone": return "plan or execute first slice";
case "research-slice": return `plan ${sid}`;

View file

@ -27,6 +27,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { hasImplementationArtifacts } from "./auto-recovery.js";
import {
buildDiscussMilestonePrompt,
buildResearchMilestonePrompt,
buildPlanMilestonePrompt,
buildResearchSlicePrompt,
@ -210,27 +211,29 @@ const DISPATCH_RULES: DispatchRule[] = [
},
},
{
name: "needs-discussion → stop",
match: async ({ state, mid, midTitle }) => {
name: "needs-discussion → discuss-milestone",
match: async ({ state, mid, midTitle, basePath }) => {
if (state.phase !== "needs-discussion") return null;
return {
action: "stop",
reason: `${mid}: ${midTitle} has draft context from a prior discussion — needs its own discussion before planning.\nRun /gsd to discuss.`,
level: "warning",
action: "dispatch",
unitType: "discuss-milestone",
unitId: mid,
prompt: await buildDiscussMilestonePrompt(mid, midTitle, basePath),
};
},
},
{
name: "pre-planning (no context) → stop",
match: async ({ state, mid, basePath }) => {
name: "pre-planning (no context) → discuss-milestone",
match: async ({ state, mid, midTitle, basePath }) => {
if (state.phase !== "pre-planning") return null;
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
const hasContext = !!(contextFile && (await loadFile(contextFile)));
if (hasContext) return null; // fall through to next rule
return {
action: "stop",
reason: "No context or roadmap yet. Run /gsd to discuss first.",
level: "warning",
action: "dispatch",
unitType: "discuss-milestone",
unitId: mid,
prompt: await buildDiscussMilestonePrompt(mid, midTitle, basePath),
};
},
},

View file

@ -767,6 +767,34 @@ export async function checkNeedsRunUat(
// ─── Prompt Builders ──────────────────────────────────────────────────────
/**
* Build a prompt for the discuss-milestone unit type.
* Loads the guided-discuss-milestone template and inlines the CONTEXT-DRAFT
* as a seed when present. The discussion agent interviews the user, writes
* a full CONTEXT.md, and the phase transitions to pre-planning automatically.
*/
export async function buildDiscussMilestonePrompt(mid: string, midTitle: string, base: string): Promise<string> {
const discussTemplates = inlineTemplate("context", "Context");
const basePrompt = loadPrompt("guided-discuss-milestone", {
milestoneId: mid,
milestoneTitle: midTitle,
inlinedTemplates: discussTemplates,
structuredQuestionsAvailable: "true",
commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally.",
});
// If a CONTEXT-DRAFT.md exists, append it as seed material
const draftPath = resolveMilestoneFile(base, mid, "CONTEXT-DRAFT");
const draftContent = draftPath ? await loadFile(draftPath) : null;
if (draftContent) {
return `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\nThe following draft was captured from a prior multi-milestone discussion. Use it as seed material — the user has already provided this context. Start with a brief reflection on what the draft covers, then probe for any gaps or open questions before writing the full CONTEXT.md.\n\n${draftContent}`;
}
return basePrompt;
}
export async function buildResearchMilestonePrompt(mid: string, midTitle: string, base: string): Promise<string> {
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
const contextRel = relMilestoneFile(base, mid, "CONTEXT");

View file

@ -63,6 +63,10 @@ export function resolveExpectedArtifactPath(
const mid = parts[0]!;
const sid = parts[1];
switch (unitType) {
case "discuss-milestone": {
const dir = resolveMilestonePath(base, mid);
return dir ? join(dir, buildMilestoneFileName(mid, "CONTEXT")) : null;
}
case "research-milestone": {
const dir = resolveMilestonePath(base, mid);
return dir ? join(dir, buildMilestoneFileName(mid, "RESEARCH")) : null;
@ -441,6 +445,8 @@ export function diagnoseExpectedArtifact(
const mid = parts[0];
const sid = parts[1];
switch (unitType) {
case "discuss-milestone":
return `${relMilestoneFile(base, mid!, "CONTEXT")} (milestone context from discussion)`;
case "research-milestone":
return `${relMilestoneFile(base, mid!, "RESEARCH")} (milestone research)`;
case "plan-milestone":

View file

@ -35,7 +35,8 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
"complete-slice": "light",
"run-uat": "light",
// Tier 2 — Standard: research, routine planning
// Tier 2 — Standard: research, routine planning, discussion
"discuss-milestone": "standard",
"research-milestone": "standard",
"research-slice": "standard",
"plan-milestone": "standard",