From 8fec87b6f26f2e41bbf9640d7463faa041f0eeb1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 11:03:04 +0000 Subject: [PATCH 1/2] fix(gsd): notify users what to do next after /gsd step finishes In step mode, /gsd would run one unit and then silently exit the auto loop, leaving users with no hint that they should /clear and /gsd again to run the next step. Emit an info notify before returning "step-wizard" from postUnitPostVerification so the TUI surfaces the next unit label and the /clear + /gsd guidance (or /gsd auto to switch to auto mode). Falls back to a generic message if deriveState throws, and handles the milestone-complete case with a dedicated review message. https://claude.ai/code/session_015yrPQbZTyJPqTsM654Ym3s --- .../extensions/gsd/auto-post-unit.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index dc710830f..c3a29264f 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"; @@ -1025,8 +1026,32 @@ 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); + if (nextState.phase === "complete") { + ctx.ui.notify( + "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.", + "info", + ); + } else { + const next = describeNextUnit(nextState); + ctx.ui.notify( + `Step complete. Next: ${next.label}\n` + + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`, + "info", + ); + } + } catch (e) { + debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) }); + ctx.ui.notify( + "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).", + "info", + ); + } return "step-wizard"; } From 2cec5a10145eeac8c9c519b5b80a7b60dbf432ff Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 14 Apr 2026 06:11:53 -0500 Subject: [PATCH 2/2] test(gsd): cover step-mode completion message helper Extracts the step-complete notification text into buildStepCompleteMessage and STEP_COMPLETE_FALLBACK_MESSAGE so the copy can be unit-tested directly (milestone complete, mid-flight with next unit, unknown phase, and deriveState-failure fallback). Resolves require-tests CI failure on PR #4173. --- .../extensions/gsd/auto-post-unit.ts | 31 +++++------ .../tests/auto-post-unit-step-message.test.ts | 53 +++++++++++++++++++ 2 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts diff --git a/src/resources/extensions/gsd/auto-post-unit.ts b/src/resources/extensions/gsd/auto-post-unit.ts index c3a29264f..cb146679d 100644 --- a/src/resources/extensions/gsd/auto-post-unit.ts +++ b/src/resources/extensions/gsd/auto-post-unit.ts @@ -234,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; @@ -1032,25 +1044,10 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<" if (s.stepMode) { try { const nextState = await deriveState(s.basePath); - if (nextState.phase === "complete") { - ctx.ui.notify( - "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.", - "info", - ); - } else { - const next = describeNextUnit(nextState); - ctx.ui.notify( - `Step complete. Next: ${next.label}\n` - + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`, - "info", - ); - } + ctx.ui.notify(buildStepCompleteMessage(nextState), "info"); } catch (e) { debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) }); - ctx.ui.notify( - "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).", - "info", - ); + 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/); +});