diff --git a/src/resources/extensions/gsd/guided-flow.ts b/src/resources/extensions/gsd/guided-flow.ts index 91d307579c..2138f450c4 100644 --- a/src/resources/extensions/gsd/guided-flow.ts +++ b/src/resources/extensions/gsd/guided-flow.ts @@ -191,6 +191,11 @@ async function runQuickTaskChoice(ctx: ExtensionCommandContext, pi: ExtensionAPI await handleQuick(task, ctx, pi); } +function isNonInteractiveContext(ctx: ExtensionCommandContext): boolean { + if (!ctx.hasUI) return true; + return process.env.GSD_HEADLESS === "1" || process.env.GSD_WEB_BRIDGE_TUI === "1"; +} + /** * Scope-based overload of isGhostMilestone. * Binds basePath and milestoneId from the scope, ensuring path resolution @@ -2217,6 +2222,10 @@ export async function showSmartEntry( basePath ), "gsd-run", ctx, "discuss-milestone", { basePath }); } else { + if (isNonInteractiveContext(ctx)) { + ctx.ui.notify(`Auto-mode stopped — ${state.nextAction || "No active milestone."}`, "info"); + return; + } const choice = await showNextAction(ctx, { title: "GSD — Get Shit Done", summary: ["No active milestone."], @@ -2273,6 +2282,10 @@ export async function showSmartEntry( // ── All milestones complete → New milestone ────────────────────────── if (state.phase === "complete") { + if (isNonInteractiveContext(ctx)) { + ctx.ui.notify("Auto-mode stopped — all milestones complete.", "info"); + return; + } const choice = await showNextAction(ctx, { title: `GSD — ${milestoneId}: ${milestoneTitle}`, summary: ["All milestones complete."], diff --git a/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts b/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts index effa8b64cf..f444e7bed1 100644 --- a/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +++ b/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts @@ -7,6 +7,8 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { deriveState } from "../state.js"; +import { showSmartEntry } from "../guided-flow.js"; +import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.js"; function writeCompleteMilestone(base: string): void { const milestoneDir = join(base, ".gsd", "milestones", "M001"); @@ -35,3 +37,40 @@ test("deriveState reports the last completed milestone when all milestone slices rmSync(base, { recursive: true, force: true }); } }); + +test("showSmartEntry stops instead of opening next-action choices when complete and non-interactive", async () => { + const base = mkdtempSync(join(tmpdir(), "gsd-smart-entry-complete-")); + const notifications: Array<{ message: string; level: string }> = []; + try { + writeCompleteMilestone(base); + openDatabase(join(base, ".gsd", "gsd.db")); + insertMilestone({ id: "M001", title: "Complete Milestone", status: "complete" }); + insertSlice({ id: "S01", milestoneId: "M001", title: "Done slice", status: "complete", risk: "low", depends: [] }); + + await showSmartEntry( + { + hasUI: false, + ui: { + notify: (message: string, level: string) => notifications.push({ message, level }), + setStatus: () => {}, + }, + } as any, + { + sendMessage: () => { + throw new Error("complete non-interactive smart entry must not dispatch a prompt"); + }, + getActiveTools: () => [], + setActiveTools: () => {}, + } as any, + base, + ); + + assert.deepEqual(notifications.at(-1), { + message: "Auto-mode stopped — All milestones complete.", + level: "info", + }); + } finally { + closeDatabase(); + rmSync(base, { recursive: true, force: true }); + } +});