diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index dc710830f..cb146679d 100644 --- a/src/resources/extensions/gsd/auto-post-unit.ts +++ b/src/resources/extensions/gsd/auto-post-unit.ts @@ -104,6 +104,7 @@ import { updateSliceProgressCache, unitVerb, hideFooter, + describeNextUnit, } from "./auto-dashboard.js"; import { existsSync, unlinkSync } from "node:fs"; import { join } from "node:path"; @@ -233,6 +234,18 @@ export function detectRogueFileWrites( return rogues; } +export const STEP_COMPLETE_FALLBACK_MESSAGE = + "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously)."; + +export function buildStepCompleteMessage(nextState: import("./types.js").GSDState): string { + if (nextState.phase === "complete") { + return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone."; + } + const next = describeNextUnit(nextState); + return `Step complete. Next: ${next.label}\n` + + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`; +} + export interface PreVerificationOpts { skipSettleDelay?: boolean; skipWorktreeSync?: boolean; @@ -1025,8 +1038,17 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<" } } - // Step mode → show wizard instead of dispatch + // Step mode → show wizard instead of dispatch. + // Without this notify(), /gsd in step mode finishes a unit and silently + // exits the loop, leaving the user with no hint to /clear and /gsd again. if (s.stepMode) { + try { + const nextState = await deriveState(s.basePath); + ctx.ui.notify(buildStepCompleteMessage(nextState), "info"); + } catch (e) { + debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) }); + ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info"); + } return "step-wizard"; } diff --git a/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts b/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts new file mode 100644 index 000000000..b1b057570 --- /dev/null +++ b/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts @@ -0,0 +1,53 @@ +// GSD-2 — Tests for step-mode completion messages in auto-post-unit + +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildStepCompleteMessage, STEP_COMPLETE_FALLBACK_MESSAGE } from "../auto-post-unit.ts"; +import type { GSDState } from "../types.ts"; + +function makeState(overrides: Partial): GSDState { + return { + activeMilestone: null, + activeSlice: null, + activeTask: null, + phase: "executing", + recentDecisions: [], + blockers: [], + nextAction: "", + registry: [], + ...overrides, + }; +} + +test("buildStepCompleteMessage: milestone complete surfaces review guidance", () => { + const msg = buildStepCompleteMessage(makeState({ phase: "complete" })); + assert.match(msg, /milestone finished/); + assert.match(msg, /\/gsd status/); + assert.doesNotMatch(msg, /Next:/); +}); + +test("buildStepCompleteMessage: mid-flight step includes next unit label and /clear hint", () => { + const state = makeState({ + phase: "executing", + activeSlice: { id: "S01", title: "Core" }, + activeTask: { id: "T03", title: "Wire notify" }, + }); + const msg = buildStepCompleteMessage(state); + assert.match(msg, /Next: Execute T03: Wire notify/); + assert.match(msg, /\/clear/); + assert.match(msg, /\/gsd to continue/); +}); + +test("buildStepCompleteMessage: unknown phase falls back to generic continue label", () => { + // Cast to bypass Phase union so we exercise the default branch of describeNextUnit. + const state = makeState({ phase: "totally-unknown" as unknown as GSDState["phase"] }); + const msg = buildStepCompleteMessage(state); + assert.match(msg, /Next: Continue/); + assert.match(msg, /\/clear/); +}); + +test("STEP_COMPLETE_FALLBACK_MESSAGE: used when deriveState throws, still points users at /clear + /gsd", () => { + assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/clear/); + assert.match(STEP_COMPLETE_FALLBACK_MESSAGE, /\/gsd/); +});