From 369bd8aeb9173fafd309310afb6cf9963a746181 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 16 Mar 2026 11:01:14 -0400 Subject: [PATCH] fix: auto mode re-derives state after discussion fallthrough (#609) (#629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/resources/extensions/gsd/auto.ts | 53 +++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/auto.ts b/src/resources/extensions/gsd/auto.ts index afa824d95..8872863da 100644 --- a/src/resources/extensions/gsd/auto.ts +++ b/src/resources/extensions/gsd/auto.ts @@ -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) {