fix: auto mode re-derives state after discussion fallthrough (#609) (#629)

When /gsd auto is called with no milestone, it delegates to the
discussion flow (showSmartEntry). Previously, if the LLM didn't follow
the discussion protocol — e.g. for simple tasks where it judged the
ceremony overkill and started editing directly — auto mode never
activated. The function returned after showSmartEntry with no retry
or notification, leaving the user in a loop.

Fix: After showSmartEntry returns in both the no-milestone and
pre-planning paths, re-derive state from disk. If the LLM produced
enough artifacts (CONTEXT.md, ROADMAP.md, or advanced the phase),
auto mode proceeds instead of returning. If not, a clear warning
tells the user what happened and what to do next.

This handles the case where the LLM writes files but doesn't follow
the exact discussion → CONTEXT.md → checkAutoStartAfterDiscuss flow.
This commit is contained in:
Tom Boucher 2026-03-16 11:01:14 -04:00 committed by GitHub
parent 330e5200bc
commit 369bd8aeb9

View file

@ -725,25 +725,68 @@ export async function startAuto(
clearLock(base);
}
const state = await deriveState(base);
let state = await deriveState(base);
// No active work at all — start a new milestone via the discuss flow.
// After discussion completes, checkAutoStartAfterDiscuss() (fired from
// agent_end) will detect the new CONTEXT.md and restart auto mode.
// If the LLM didn't follow the discussion protocol (e.g. started editing
// files directly for a simple task), we re-derive state and either proceed
// with what was created or notify the user clearly (#609).
if (!state.activeMilestone || state.phase === "complete") {
const { showSmartEntry } = await import("./guided-flow.js");
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
return;
// Re-derive state after discussion — the LLM may have created artifacts
// even if it didn't follow the full protocol.
invalidateAllCaches();
const postState = await deriveState(base);
if (postState.activeMilestone && postState.phase !== "complete" && postState.phase !== "pre-planning") {
// Discussion produced enough artifacts to proceed — fall through
// to auto mode activation below instead of returning.
state = postState;
} else if (postState.activeMilestone && postState.phase === "pre-planning") {
// Milestone directory exists but no context — check if context was written
const contextFile = resolveMilestoneFile(base, postState.activeMilestone.id, "CONTEXT");
const hasContext = !!(contextFile && await loadFile(contextFile));
if (hasContext) {
state = postState;
// Fall through — auto mode will research + plan it
} else {
ctx.ui.notify(
"Discussion completed but no milestone context was written. Run /gsd to try the discussion again, or /gsd auto after creating the milestone manually.",
"warning",
);
return;
}
} else {
return;
}
}
// Active milestone exists but has no roadmap — check if context exists.
// If context was pre-written (multi-milestone planning), auto-mode can
// research and plan it. If no context either, need user discussion.
if (state.phase === "pre-planning") {
const contextFile = resolveMilestoneFile(base, state.activeMilestone.id, "CONTEXT");
const mid = state.activeMilestone!.id;
const contextFile = resolveMilestoneFile(base, mid, "CONTEXT");
const hasContext = !!(contextFile && await loadFile(contextFile));
if (!hasContext) {
const { showSmartEntry } = await import("./guided-flow.js");
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
return;
// Same re-derive pattern as above
invalidateAllCaches();
const postState = await deriveState(base);
if (postState.activeMilestone && postState.phase !== "pre-planning") {
state = postState;
} else {
ctx.ui.notify(
"Discussion completed but milestone context is still missing. Run /gsd to try again.",
"warning",
);
return;
}
}
// Has context, no roadmap — auto-mode will research + plan it
}
@ -846,7 +889,7 @@ export async function startAuto(
ctx.ui.notify(`${modeLabel} started. ${scopeMsg}`, "info");
// Secrets collection gate — collect pending secrets before first dispatch
const mid = state.activeMilestone.id;
const mid = state.activeMilestone!.id;
try {
const manifestStatus = await getManifestStatus(base, mid);
if (manifestStatus && manifestStatus.pending.length > 0) {