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:
parent
b2fb12813f
commit
e94bda817f
5 changed files with 52 additions and 11 deletions
|
|
@ -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}`;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue