Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/resources/extensions/gsd/guided-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."],
Expand Down Expand Up @@ -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."],
Expand Down
39 changes: 39 additions & 0 deletions src/resources/extensions/gsd/tests/smart-entry-complete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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 });
}
});
Loading