gsd-pi 2.73.1-dev.47ed32a → 2.73.1-dev.49397e5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/dist/cli-web-branch.d.ts +4 -3
  2. package/dist/cli-web-branch.js +10 -7
  3. package/dist/cli.js +99 -206
  4. package/dist/headless-query.js +4 -1
  5. package/dist/logo.d.ts +1 -1
  6. package/dist/logo.js +1 -1
  7. package/dist/onboarding.js +59 -53
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +59 -1
  9. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  10. package/dist/resources/extensions/gsd/auto/phases.js +60 -10
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +11 -3
  12. package/dist/resources/extensions/gsd/auto-post-unit.js +93 -57
  13. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +13 -0
  14. package/dist/resources/extensions/gsd/auto-verification.js +88 -3
  15. package/dist/resources/extensions/gsd/auto.js +29 -10
  16. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  17. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  18. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  19. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  20. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  21. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  22. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  23. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  24. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  25. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  26. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  27. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  28. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  29. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  30. package/dist/resources/extensions/gsd/index.js +15 -2
  31. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  32. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  33. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  34. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  35. package/dist/resources/extensions/gsd/notification-widget.js +2 -2
  36. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  37. package/dist/resources/extensions/gsd/state.js +66 -15
  38. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  39. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  40. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  41. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  42. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  43. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  44. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  45. package/dist/web/standalone/.next/BUILD_ID +1 -1
  46. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  47. package/dist/web/standalone/.next/build-manifest.json +2 -2
  48. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  49. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.html +1 -1
  66. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  73. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  75. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  76. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  77. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  78. package/package.json +3 -3
  79. package/packages/mcp-server/tsconfig.json +1 -0
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  81. package/packages/native/tsconfig.tsbuildinfo +1 -0
  82. package/packages/pi-agent-core/tsconfig.json +1 -0
  83. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  84. package/packages/pi-ai/tsconfig.json +1 -0
  85. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +10 -2
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +2 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +9 -3
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +52 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +5 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/package.json +1 -1
  101. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +12 -6
  102. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +73 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +9 -3
  104. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -1
  105. package/packages/pi-coding-agent/tsconfig.json +1 -0
  106. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  107. package/packages/pi-tui/dist/__tests__/tui.test.js +30 -0
  108. package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
  109. package/packages/pi-tui/dist/tui.d.ts +1 -0
  110. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  111. package/packages/pi-tui/dist/tui.js +22 -3
  112. package/packages/pi-tui/dist/tui.js.map +1 -1
  113. package/packages/pi-tui/src/__tests__/tui.test.ts +38 -0
  114. package/packages/pi-tui/src/tui.ts +20 -3
  115. package/packages/pi-tui/tsconfig.json +1 -0
  116. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  117. package/packages/rpc-client/tsconfig.json +1 -0
  118. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  119. package/pkg/package.json +1 -1
  120. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +95 -1
  121. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +88 -0
  122. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  123. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  124. package/src/resources/extensions/gsd/auto/phases.ts +90 -10
  125. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -4
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +107 -58
  127. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +17 -0
  128. package/src/resources/extensions/gsd/auto-verification.ts +98 -3
  129. package/src/resources/extensions/gsd/auto.ts +33 -14
  130. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  131. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  132. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  133. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  134. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  135. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  136. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  137. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  138. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  139. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  140. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  141. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  142. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
  143. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  144. package/src/resources/extensions/gsd/index.ts +18 -2
  145. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  146. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  147. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  149. package/src/resources/extensions/gsd/notification-widget.ts +2 -2
  150. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  151. package/src/resources/extensions/gsd/state.ts +80 -17
  152. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -2
  153. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +53 -0
  154. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  155. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  156. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  157. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  158. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  159. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  160. package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +142 -0
  161. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +42 -0
  162. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -2
  163. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +3 -2
  164. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +68 -8
  165. package/src/resources/extensions/gsd/tests/derive-state.test.ts +3 -3
  166. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  167. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  168. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  169. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  170. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  171. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +5 -7
  172. package/src/resources/extensions/gsd/tests/token-profile.test.ts +1 -1
  173. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +179 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  175. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  176. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  177. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  178. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  179. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  180. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  181. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{WawMb3TMcYLkGJLTg_x1u → tAsSblRKK1lG7MVOwsO92}/_ssgManifest.js +0 -0
@@ -71,8 +71,8 @@ const OTHER_PROVIDERS = [
71
71
  ];
72
72
  // ─── Dynamic imports ──────────────────────────────────────────────────────────
73
73
  /**
74
- * Dynamically import @clack/prompts and picocolors.
75
- * Dynamic import with fallback so the module doesn't crash if they're missing.
74
+ * Dynamically import @clack/prompts.
75
+ * Dynamic import with fallback so the module doesn't crash if it's missing.
76
76
  */
77
77
  async function loadClack() {
78
78
  try {
@@ -82,10 +82,23 @@ async function loadClack() {
82
82
  throw new Error('[gsd] @clack/prompts not found — onboarding wizard requires this dependency');
83
83
  }
84
84
  }
85
+ /**
86
+ * Build the PicoModule color surface from chalk. Chalk is already a
87
+ * dependency of the CLI; this adapter keeps the onboarding call sites stable
88
+ * while removing the redundant picocolors dep.
89
+ */
85
90
  async function loadPico() {
86
91
  try {
87
- const mod = await import('picocolors');
88
- return mod.default ?? mod;
92
+ const { default: chalk } = await import('chalk');
93
+ return {
94
+ cyan: (s) => chalk.cyan(s),
95
+ green: (s) => chalk.green(s),
96
+ yellow: (s) => chalk.yellow(s),
97
+ dim: (s) => chalk.dim(s),
98
+ bold: (s) => chalk.bold(s),
99
+ red: (s) => chalk.red(s),
100
+ reset: (s) => chalk.reset(s),
101
+ };
89
102
  }
90
103
  catch {
91
104
  // Fallback: return identity functions
@@ -105,9 +118,29 @@ function openBrowser(url) {
105
118
  execFile(cmd, [url], () => { });
106
119
  }
107
120
  }
108
- /** Check if an error is a clack cancel signal */
109
- function isCancelError(p, err) {
110
- return p.isCancel(err);
121
+ /** Sentinel returned by runStep when the user cancels tells the caller
122
+ * to abort the entire wizard. */
123
+ const STEP_CANCELLED = Symbol('step-cancelled');
124
+ /**
125
+ * Run a single onboarding step with shared error handling:
126
+ * - user cancel (Ctrl+C) → p.cancel(cancelMessage), returns STEP_CANCELLED
127
+ * - other error → p.log.warn + optional info follow-up, returns null
128
+ * - success → the step's return value
129
+ */
130
+ async function runStep(p, warnLabel, fn, opts = {}) {
131
+ try {
132
+ return await fn();
133
+ }
134
+ catch (err) {
135
+ if (p.isCancel(err)) {
136
+ p.cancel(opts.cancelMessage ?? 'Setup cancelled.');
137
+ return STEP_CANCELLED;
138
+ }
139
+ p.log.warn(`${warnLabel}: ${err instanceof Error ? err.message : String(err)}`);
140
+ if (opts.errorInfo)
141
+ p.log.info(opts.errorInfo);
142
+ return null;
143
+ }
111
144
  }
112
145
  // ─── Public API ───────────────────────────────────────────────────────────────
113
146
  /**
@@ -160,55 +193,28 @@ export async function runOnboarding(authStorage) {
160
193
  process.stderr.write(renderLogo(pc.cyan));
161
194
  p.intro(pc.bold('Welcome to GSD — let\'s get you set up'));
162
195
  // ── LLM Provider Selection ────────────────────────────────────────────────
163
- let llmConfigured = false;
164
- try {
165
- llmConfigured = await runLlmStep(p, pc, authStorage);
166
- }
167
- catch (err) {
168
- // User cancelled (Ctrl+C in clack throws) or unexpected error
169
- if (isCancelError(p, err)) {
170
- p.cancel('Setup cancelled — you can run /login inside GSD later.');
171
- return;
172
- }
173
- p.log.warn(`LLM setup failed: ${err instanceof Error ? err.message : String(err)}`);
174
- p.log.info('You can configure your LLM provider later with /login inside GSD.');
175
- }
196
+ const llmResult = await runStep(p, 'LLM setup failed', () => runLlmStep(p, pc, authStorage), {
197
+ cancelMessage: 'Setup cancelled — you can run /login inside GSD later.',
198
+ errorInfo: 'You can configure your LLM provider later with /login inside GSD.',
199
+ });
200
+ if (llmResult === STEP_CANCELLED)
201
+ return;
202
+ const llmConfigured = llmResult ?? false;
176
203
  // ── Web Search Provider ──────────────────────────────────────────────────
177
- let searchConfigured = null;
178
- try {
179
- searchConfigured = await runWebSearchStep(p, pc, authStorage, llmConfigured);
180
- }
181
- catch (err) {
182
- if (isCancelError(p, err)) {
183
- p.cancel('Setup cancelled.');
184
- return;
185
- }
186
- p.log.warn(`Web search setup failed: ${err instanceof Error ? err.message : String(err)}`);
187
- }
204
+ const searchResult = await runStep(p, 'Web search setup failed', () => runWebSearchStep(p, pc, authStorage, llmConfigured));
205
+ if (searchResult === STEP_CANCELLED)
206
+ return;
207
+ const searchConfigured = searchResult;
188
208
  // ── Remote Questions ─────────────────────────────────────────────────────
189
- let remoteConfigured = null;
190
- try {
191
- remoteConfigured = await runRemoteQuestionsStep(p, pc, authStorage);
192
- }
193
- catch (err) {
194
- if (isCancelError(p, err)) {
195
- p.cancel('Setup cancelled.');
196
- return;
197
- }
198
- p.log.warn(`Remote questions setup failed: ${err instanceof Error ? err.message : String(err)}`);
199
- }
209
+ const remoteResult = await runStep(p, 'Remote questions setup failed', () => runRemoteQuestionsStep(p, pc, authStorage));
210
+ if (remoteResult === STEP_CANCELLED)
211
+ return;
212
+ const remoteConfigured = remoteResult;
200
213
  // ── Tool API Keys ─────────────────────────────────────────────────────────
201
- let toolKeyCount = 0;
202
- try {
203
- toolKeyCount = await runToolKeysStep(p, pc, authStorage);
204
- }
205
- catch (err) {
206
- if (isCancelError(p, err)) {
207
- p.cancel('Setup cancelled.');
208
- return;
209
- }
210
- p.log.warn(`Tool key setup failed: ${err instanceof Error ? err.message : String(err)}`);
211
- }
214
+ const toolResult = await runStep(p, 'Tool key setup failed', () => runToolKeysStep(p, pc, authStorage));
215
+ if (toolResult === STEP_CANCELLED)
216
+ return;
217
+ const toolKeyCount = toolResult ?? 0;
212
218
  // ── Summary ───────────────────────────────────────────────────────────────
213
219
  const summaryLines = [];
214
220
  if (llmConfigured) {
@@ -123,6 +123,63 @@ export function buildPromptFromContext(context) {
123
123
  }
124
124
  return parts.join("\n\n");
125
125
  }
126
+ function stripDataUriPrefix(value) {
127
+ const commaIndex = value.indexOf(",");
128
+ if (value.startsWith("data:") && commaIndex !== -1) {
129
+ return value.slice(commaIndex + 1);
130
+ }
131
+ return value;
132
+ }
133
+ function inferMimeTypeFromDataUri(value) {
134
+ const match = /^data:([^;,]+);base64,/.exec(value);
135
+ return match?.[1] ?? null;
136
+ }
137
+ export function extractImageBlocksFromContext(context) {
138
+ const imageBlocks = [];
139
+ for (const msg of context.messages) {
140
+ if (msg.role !== "user" || !Array.isArray(msg.content))
141
+ continue;
142
+ for (const part of msg.content) {
143
+ if (!part || typeof part !== "object")
144
+ continue;
145
+ const block = part;
146
+ if (block.type !== "image" || typeof block.data !== "string")
147
+ continue;
148
+ const mimeType = typeof block.mimeType === "string" && block.mimeType.length > 0
149
+ ? block.mimeType
150
+ : inferMimeTypeFromDataUri(block.data);
151
+ if (!mimeType)
152
+ continue;
153
+ imageBlocks.push({
154
+ type: "image",
155
+ source: {
156
+ type: "base64",
157
+ media_type: mimeType,
158
+ data: stripDataUriPrefix(block.data),
159
+ },
160
+ });
161
+ }
162
+ }
163
+ return imageBlocks;
164
+ }
165
+ export function buildSdkQueryPrompt(context, textPrompt = buildPromptFromContext(context)) {
166
+ const imageBlocks = extractImageBlocksFromContext(context);
167
+ if (imageBlocks.length === 0) {
168
+ return textPrompt;
169
+ }
170
+ const content = [...imageBlocks];
171
+ if (textPrompt) {
172
+ content.push({ type: "text", text: textPrompt });
173
+ }
174
+ const sdkMessage = {
175
+ type: "user",
176
+ message: { role: "user", content },
177
+ parent_tool_use_id: null,
178
+ };
179
+ return (async function* () {
180
+ yield sdkMessage;
181
+ })();
182
+ }
126
183
  // ---------------------------------------------------------------------------
127
184
  // Error helper
128
185
  // ---------------------------------------------------------------------------
@@ -622,6 +679,7 @@ async function pumpSdkMessages(model, context, options, stream) {
622
679
  options.signal.addEventListener("abort", () => controller.abort(), { once: true });
623
680
  }
624
681
  const prompt = buildPromptFromContext(context);
682
+ const queryPrompt = buildSdkQueryPrompt(context, prompt);
625
683
  const permissionMode = await resolveClaudePermissionMode();
626
684
  const sdkOpts = buildSdkOptions(modelId, prompt, { permissionMode }, typeof options?.extensionUIContext === "object"
627
685
  ? {
@@ -630,7 +688,7 @@ async function pumpSdkMessages(model, context, options, stream) {
630
688
  }
631
689
  : { reasoning: options?.reasoning });
632
690
  const queryResult = sdk.query({
633
- prompt,
691
+ prompt: queryPrompt,
634
692
  options: {
635
693
  ...sdkOpts,
636
694
  abortController: controller,
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Leaf node in the import DAG.
5
5
  */
6
+ import { summarizeLogs } from "../workflow-logger.js";
6
7
  /**
7
8
  * Pattern matching ENOENT errors with a file path.
8
9
  * Matches: "ENOENT: no such file or directory, access '/path/to/file'"
@@ -22,13 +23,19 @@ const ENOENT_PATH_RE = /ENOENT[^']*'([^']+)'/;
22
23
  export function detectStuck(window) {
23
24
  if (window.length < 2)
24
25
  return null;
26
+ // Peek (not drain) the workflow-logger buffer so stuck reasons can surface
27
+ // the underlying diagnostic context (projection failures, DB degradations,
28
+ // reconcile warnings) that usually explains *why* the loop is stuck. The
29
+ // auto-loop's finalize step owns the buffer lifecycle — this is read-only.
30
+ const loggerSummary = summarizeLogs();
31
+ const suffix = loggerSummary ? ` — ${loggerSummary}` : "";
25
32
  const last = window[window.length - 1];
26
33
  const prev = window[window.length - 2];
27
34
  // Rule 1: Same error repeated consecutively
28
35
  if (last.error && prev.error && last.error === prev.error) {
29
36
  return {
30
37
  stuck: true,
31
- reason: `Same error repeated: ${last.error.slice(0, 200)}`,
38
+ reason: `Same error repeated: ${last.error.slice(0, 200)}${suffix}`,
32
39
  };
33
40
  }
34
41
  // Rule 2: Same unit 3+ consecutive times
@@ -37,7 +44,7 @@ export function detectStuck(window) {
37
44
  if (lastThree.every((u) => u.key === last.key)) {
38
45
  return {
39
46
  stuck: true,
40
- reason: `${last.key} derived 3 consecutive times without progress`,
47
+ reason: `${last.key} derived 3 consecutive times without progress${suffix}`,
41
48
  };
42
49
  }
43
50
  }
@@ -49,7 +56,7 @@ export function detectStuck(window) {
49
56
  w[0].key !== w[1].key) {
50
57
  return {
51
58
  stuck: true,
52
- reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}`,
59
+ reason: `Oscillation detected: ${w[0].key} ↔ ${w[1].key}${suffix}`,
53
60
  };
54
61
  }
55
62
  }
@@ -67,7 +74,7 @@ export function detectStuck(window) {
67
74
  if (count >= 2) {
68
75
  return {
69
76
  stuck: true,
70
- reason: `Missing file referenced twice: ${filePath} (ENOENT)`,
77
+ reason: `Missing file referenced twice: ${filePath} (ENOENT)${suffix}`,
71
78
  };
72
79
  }
73
80
  enoentPaths.set(filePath, count);
@@ -16,7 +16,7 @@ import { MergeConflictError } from "../git-service.js";
16
16
  import { setCurrentPhase, clearCurrentPhase } from "../../shared/gsd-phase-state.js";
17
17
  import { join, basename, dirname, parse as parsePath } from "node:path";
18
18
  import { existsSync, cpSync, readdirSync } from "node:fs";
19
- import { logWarning, logError } from "../workflow-logger.js";
19
+ import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
20
20
  import { gsdRoot } from "../paths.js";
21
21
  import { atomicWriteSync } from "../atomic-write.js";
22
22
  import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
@@ -100,6 +100,22 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
100
100
  }
101
101
  await deps.stopAuto(ctx, pi, reason);
102
102
  }
103
+ async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
104
+ ic.deps.emitJournalEvent({
105
+ ts: new Date().toISOString(),
106
+ flowId: ic.flowId,
107
+ seq: ic.nextSeq(),
108
+ eventType: "unit-end",
109
+ data: {
110
+ unitType,
111
+ unitId,
112
+ status: "cancelled",
113
+ artifactVerified: false,
114
+ ...(errorContext ? { errorContext } : {}),
115
+ },
116
+ causedBy: { flowId: ic.flowId, seq: unitStartSeq },
117
+ });
118
+ }
103
119
  // ─── runPreDispatch ───────────────────────────────────────────────────────────
104
120
  /**
105
121
  * Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
@@ -320,10 +336,13 @@ export async function runPreDispatch(ic, loopState) {
320
336
  }
321
337
  else if (state.phase === "blocked") {
322
338
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
323
- await deps.stopAuto(ctx, pi, blockerMsg);
324
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
325
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
326
- deps.logCmuxEvent(prefs, blockerMsg, "error");
339
+ // Pause instead of hard-stop so the session is resumable with `/gsd auto`.
340
+ // Hard-stop here was causing premature termination when slice dependencies
341
+ // were temporarily unresolvable (e.g. after reassessment added new slices).
342
+ await deps.pauseAuto(ctx, pi);
343
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
344
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
345
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
327
346
  }
328
347
  else {
329
348
  const ids = incomplete.map((m) => m.id).join(", ");
@@ -392,13 +411,16 @@ export async function runPreDispatch(ic, loopState) {
392
411
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
393
412
  return { action: "break", reason: "milestone-complete" };
394
413
  }
395
- // Terminal: blocked
414
+ // Terminal: blocked — pause instead of hard-stop so the session is resumable.
396
415
  if (state.phase === "blocked") {
397
416
  const blockerMsg = `Blocked: ${state.blockers.join(", ")}`;
398
- await closeoutAndStop(ctx, pi, s, deps, blockerMsg);
399
- ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto.`, "warning");
400
- deps.sendDesktopNotification("GSD", blockerMsg, "error", "attention", basename(s.originalBasePath || s.basePath));
401
- deps.logCmuxEvent(prefs, blockerMsg, "error");
417
+ if (s.currentUnit) {
418
+ await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
419
+ }
420
+ await deps.pauseAuto(ctx, pi);
421
+ ctx.ui.notify(`${blockerMsg}. Fix and run /gsd auto to resume.`, "warning");
422
+ deps.sendDesktopNotification("GSD", blockerMsg, "warning", "attention", basename(s.originalBasePath || s.basePath));
423
+ deps.logCmuxEvent(prefs, blockerMsg, "warning");
402
424
  debugLog("autoLoop", { phase: "exit", reason: "blocked" });
403
425
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
404
426
  return { action: "break", reason: "blocked" };
@@ -770,6 +792,10 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
770
792
  s.currentUnit.type === unitType &&
771
793
  s.currentUnit.id === unitId);
772
794
  const previousTier = s.currentUnitRouting?.tier;
795
+ // Scope workflow-logger buffer to this unit so post-finalize drains are
796
+ // per-unit. Without this, the module-level _buffer accumulates across every
797
+ // unit in the same Node process (see workflow-logger.ts module header).
798
+ _resetLogs();
773
799
  s.currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
774
800
  setCurrentPhase(unitType);
775
801
  s.lastToolInvocationError = null; // #2883: clear stale error from previous unit
@@ -969,6 +995,7 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
969
995
  // Provider-error pause: pauseAuto already handled cleanup and scheduled
970
996
  // recovery. Don't hard-stop — just break out of the loop (#2762).
971
997
  if (unitResult.errorContext?.category === "provider") {
998
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
972
999
  debugLog("autoLoop", { phase: "exit", reason: "provider-pause", isTransient: unitResult.errorContext.isTransient });
973
1000
  return { action: "break", reason: "provider-pause" };
974
1001
  }
@@ -982,9 +1009,16 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
982
1009
  ctx.ui.notify(`Session creation timed out for ${unitType} ${unitId}. Pausing auto-mode (recoverable).`, "warning");
983
1010
  debugLog("autoLoop", { phase: "session-timeout-pause", unitType, unitId });
984
1011
  await deps.pauseAuto(ctx, pi);
1012
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1013
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
985
1014
  return { action: "break", reason: "session-timeout" };
986
1015
  }
987
1016
  // All other cancelled states (structural errors, non-transient failures): hard stop
1017
+ if (s.currentUnit) {
1018
+ await deps.closeoutUnit(ctx, s.basePath, unitType, unitId, s.currentUnit.startedAt, deps.buildSnapshotOpts(unitType, unitId));
1019
+ }
1020
+ await deps.autoCommitUnit?.(s.basePath, unitType, unitId, ctx);
1021
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, unitResult.errorContext);
988
1022
  ctx.ui.notify(`Session creation failed for ${unitType} ${unitId}: ${unitResult.errorContext?.message ?? "unknown"}. Stopping auto-mode.`, "warning");
989
1023
  await deps.stopAuto(ctx, pi, `Session creation failed: ${unitResult.errorContext?.message ?? "unknown"}`);
990
1024
  debugLog("autoLoop", { phase: "exit", reason: "session-failed" });
@@ -1118,6 +1152,9 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1118
1152
  // cannot mutate state for the next unit (#3757).
1119
1153
  s.currentUnit = null;
1120
1154
  clearCurrentPhase();
1155
+ // Drop any logger entries from the timed-out unit so they don't bleed
1156
+ // into the next iteration's drain.
1157
+ drainLogs();
1121
1158
  loopState.consecutiveFinalizeTimeouts++;
1122
1159
  debugLog("autoLoop", {
1123
1160
  phase: "pre-verification-timeout",
@@ -1193,6 +1230,9 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1193
1230
  // cannot mutate state for the next unit (#3757).
1194
1231
  s.currentUnit = null;
1195
1232
  clearCurrentPhase();
1233
+ // Drop any logger entries from the timed-out unit so they don't bleed
1234
+ // into the next iteration's drain.
1235
+ drainLogs();
1196
1236
  loopState.consecutiveFinalizeTimeouts++;
1197
1237
  debugLog("autoLoop", {
1198
1238
  phase: "post-verification-timeout",
@@ -1224,5 +1264,15 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
1224
1264
  }
1225
1265
  // Both pre and post verification completed without timeout — reset counter
1226
1266
  loopState.consecutiveFinalizeTimeouts = 0;
1267
+ // Surface accumulated workflow-logger issues for this unit to the user.
1268
+ // Warnings/errors logged during the unit are buffered in the logger and
1269
+ // drained here so the user sees a single consolidated post-unit alert.
1270
+ if (hasAnyIssues()) {
1271
+ const { logs } = drainAndSummarize();
1272
+ if (logs.length > 0) {
1273
+ const severity = logs.some((e) => e.severity === "error") ? "error" : "warning";
1274
+ ctx.ui.notify(formatForNotification(logs), severity);
1275
+ }
1276
+ }
1227
1277
  return { action: "next", data: undefined };
1228
1278
  }
@@ -216,7 +216,12 @@ export const DISPATCH_RULES = [
216
216
  {
217
217
  name: "reassess-roadmap (post-completion)",
218
218
  match: async ({ state, mid, midTitle, basePath, prefs }) => {
219
- if (prefs?.phases?.skip_reassess || !prefs?.phases?.reassess_after_slice)
219
+ if (prefs?.phases?.skip_reassess)
220
+ return null;
221
+ // Default reassess_after_slice to true — reassessment after slice completion
222
+ // is essential for roadmap integrity. Opt-out via explicit `false`.
223
+ const reassessEnabled = prefs?.phases?.reassess_after_slice ?? true;
224
+ if (!reassessEnabled)
220
225
  return null;
221
226
  const needsReassess = await checkNeedsReassessment(basePath, mid, state);
222
227
  if (!needsReassess)
@@ -710,11 +715,14 @@ export async function resolveDispatch(ctx) {
710
715
  return result;
711
716
  }
712
717
  }
713
- // No rule matched — unhandled phase
718
+ // No rule matched — unhandled phase.
719
+ // Use level "warning" so the loop pauses (resumable) instead of hard-stopping.
720
+ // Hard-stop here was causing premature termination for transient phase gaps
721
+ // (e.g. after reassessment modifies the roadmap and state needs re-derivation).
714
722
  return {
715
723
  action: "stop",
716
724
  reason: `Unhandled phase "${ctx.state.phase}" — run /gsd doctor to diagnose.`,
717
- level: "info",
725
+ level: "warning",
718
726
  matchedRule: "<no-match>",
719
727
  };
720
728
  }
@@ -67,6 +67,7 @@ const LIFECYCLE_ONLY_UNITS = new Set([
67
67
  "replan-slice", "complete-slice", "run-uat",
68
68
  "reassess-roadmap", "rewrite-docs",
69
69
  ]);
70
+ import { describeNextUnit, } from "./auto-dashboard.js";
70
71
  import { existsSync, unlinkSync } from "node:fs";
71
72
  import { join } from "node:path";
72
73
  import { _resetHasChangesCache } from "./native-git-bridge.js";
@@ -179,6 +180,66 @@ export function detectRogueFileWrites(unitType, unitId, basePath) {
179
180
  }
180
181
  return rogues;
181
182
  }
183
+ export const STEP_COMPLETE_FALLBACK_MESSAGE = "Step complete. Run /clear, then /gsd to continue (or /gsd auto to run continuously).";
184
+ export function buildStepCompleteMessage(nextState) {
185
+ if (nextState.phase === "complete") {
186
+ return "Step complete — milestone finished. Run /gsd status to review, or start the next milestone.";
187
+ }
188
+ const next = describeNextUnit(nextState);
189
+ return `Step complete. Next: ${next.label}\n`
190
+ + `Run /clear, then /gsd to continue (or /gsd auto to run continuously).`;
191
+ }
192
+ export async function autoCommitUnit(basePath, unitType, unitId, ctx) {
193
+ try {
194
+ let taskContext;
195
+ if (unitType === "execute-task") {
196
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
197
+ if (mid && sid && tid) {
198
+ const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
199
+ if (summaryPath) {
200
+ try {
201
+ const summaryContent = await loadFile(summaryPath);
202
+ if (summaryContent) {
203
+ const summary = parseSummary(summaryContent);
204
+ let ghIssueNumber;
205
+ try {
206
+ const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
207
+ ghIssueNumber = getTaskIssueNumberForCommit(basePath, mid, sid, tid) ?? undefined;
208
+ }
209
+ catch (err) {
210
+ logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
211
+ }
212
+ taskContext = {
213
+ taskId: `${sid}/${tid}`,
214
+ taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
215
+ oneLiner: summary.oneLiner || undefined,
216
+ keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
217
+ issueNumber: ghIssueNumber,
218
+ };
219
+ }
220
+ }
221
+ catch (e) {
222
+ debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
223
+ }
224
+ }
225
+ }
226
+ }
227
+ _resetHasChangesCache();
228
+ if (LIFECYCLE_ONLY_UNITS.has(unitType)) {
229
+ return null;
230
+ }
231
+ const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId, taskContext);
232
+ if (commitMsg) {
233
+ ctx?.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
234
+ }
235
+ return commitMsg;
236
+ }
237
+ catch (e) {
238
+ debugLog("postUnit", { phase: "auto-commit", error: String(e) });
239
+ ctx?.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
240
+ return null;
241
+ }
242
+ }
182
243
  /**
183
244
  * Pre-verification processing: parallel worker signal check, cache invalidation,
184
245
  * auto-commit, doctor run, state rebuild, worktree sync, artifact verification.
@@ -214,62 +275,7 @@ export async function postUnitPreVerification(pctx, opts) {
214
275
  // Auto-commit
215
276
  if (s.currentUnit) {
216
277
  const unit = s.currentUnit;
217
- try {
218
- let taskContext;
219
- if (s.currentUnit.type === "execute-task") {
220
- const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
221
- if (mid && sid && tid) {
222
- const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
223
- if (summaryPath) {
224
- try {
225
- const summaryContent = await loadFile(summaryPath);
226
- if (summaryContent) {
227
- const summary = parseSummary(summaryContent);
228
- // Look up GitHub issue number for commit linking
229
- let ghIssueNumber;
230
- try {
231
- const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
232
- ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
233
- }
234
- catch (err) {
235
- // GitHub sync not available — skip
236
- logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
237
- }
238
- taskContext = {
239
- taskId: `${sid}/${tid}`,
240
- taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
241
- oneLiner: summary.oneLiner || undefined,
242
- keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
243
- issueNumber: ghIssueNumber,
244
- };
245
- }
246
- }
247
- catch (e) {
248
- debugLog("postUnit", { phase: "task-summary-parse", error: String(e) });
249
- }
250
- }
251
- }
252
- }
253
- // Invalidate the nativeHasChanges cache before auto-commit (#1853).
254
- // The cache has a 10-second TTL and is keyed by basePath. A stale
255
- // `false` result causes autoCommit to skip staging entirely, leaving
256
- // code files only in the working tree where they are destroyed by
257
- // `git worktree remove --force` during teardown.
258
- _resetHasChangesCache();
259
- // Skip auto-commit for lifecycle-only units (#2553) — they only touch
260
- // `.gsd/` internal state files. Those files are picked up by the next
261
- // actual task commit via smartStage().
262
- if (!LIFECYCLE_ONLY_UNITS.has(s.currentUnit.type)) {
263
- const commitMsg = autoCommitCurrentBranch(s.basePath, s.currentUnit.type, s.currentUnit.id, taskContext);
264
- if (commitMsg) {
265
- ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
266
- }
267
- }
268
- }
269
- catch (e) {
270
- debugLog("postUnit", { phase: "auto-commit", error: String(e) });
271
- ctx.ui.notify(`Auto-commit failed: ${String(e).split("\n")[0]}`, "warning");
272
- }
278
+ await autoCommitUnit(s.basePath, unit.type, unit.id, ctx);
273
279
  // GitHub sync (non-blocking, opt-in)
274
280
  await runSafely("postUnit", "github-sync", async () => {
275
281
  const { runGitHubSync } = await import("../github-sync/sync.js");
@@ -509,6 +515,26 @@ export async function postUnitPreVerification(pctx, opts) {
509
515
  const attempt = (s.verificationRetryCount.get(retryKey) ?? 0) + 1;
510
516
  s.verificationRetryCount.set(retryKey, attempt);
511
517
  if (attempt > MAX_VERIFICATION_RETRIES) {
518
+ // #4175: For complete-milestone, a blocker placeholder is harmful —
519
+ // the stub SUMMARY has no recovery value (milestone is terminal),
520
+ // it does not update DB status (so deriveState never advances),
521
+ // and it fools stopAuto's presence check into merging a milestone
522
+ // that was never legitimately completed. Pause auto-mode with a
523
+ // clear single failure signal and preserve the worktree branch.
524
+ if (s.currentUnit.type === "complete-milestone") {
525
+ debugLog("postUnit", {
526
+ phase: "artifact-verify-pause-complete-milestone",
527
+ unitType: s.currentUnit.type,
528
+ unitId: s.currentUnit.id,
529
+ attempt,
530
+ maxRetries: MAX_VERIFICATION_RETRIES,
531
+ });
532
+ s.verificationRetryCount.delete(retryKey);
533
+ s.pendingVerificationRetry = null;
534
+ ctx.ui.notify(`Milestone ${s.currentUnit.id} verification failed after ${MAX_VERIFICATION_RETRIES} retries — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
535
+ await pauseAuto(ctx, pi);
536
+ return "dispatched";
537
+ }
512
538
  // Retries exhausted — write a blocker placeholder so the pipeline
513
539
  // can advance past this stuck unit (#2653).
514
540
  debugLog("postUnit", {
@@ -836,8 +862,18 @@ export async function postUnitPostVerification(pctx) {
836
862
  debugLog("postUnit", { phase: "quick-task-dispatch", error: String(e) });
837
863
  }
838
864
  }
839
- // Step mode → show wizard instead of dispatch
865
+ // Step mode → show wizard instead of dispatch.
866
+ // Without this notify(), /gsd in step mode finishes a unit and silently
867
+ // exits the loop, leaving the user with no hint to /clear and /gsd again.
840
868
  if (s.stepMode) {
869
+ try {
870
+ const nextState = await deriveState(s.basePath);
871
+ ctx.ui.notify(buildStepCompleteMessage(nextState), "info");
872
+ }
873
+ catch (e) {
874
+ debugLog("postUnit", { phase: "step-wizard-notify", error: String(e) });
875
+ ctx.ui.notify(STEP_COMPLETE_FALLBACK_MESSAGE, "info");
876
+ }
841
877
  return "step-wizard";
842
878
  }
843
879
  return "continue";
@@ -156,6 +156,19 @@ export async function recoverTimedOutUnit(ctx, pi, unitType, unitId, reason, rct
156
156
  ctx.ui.notify(`${reason === "idle" ? "Idle" : "Timeout"} recovery: steering ${unitType} ${unitId} to produce ${expected} (attempt ${attemptNumber}, session ${recoveryAttempts + 1}/${maxRecoveryAttempts}).`, "warning");
157
157
  return "recovered";
158
158
  }
159
+ // #4175: For complete-milestone, never write a blocker placeholder — a stub
160
+ // SUMMARY has no recovery value (milestone is terminal), it does not update
161
+ // DB status, and downstream merge paths can treat the stub as a legitimate
162
+ // completion signal. Pause instead so the worktree branch is preserved.
163
+ if (unitType === "complete-milestone") {
164
+ writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnitStartedAt, {
165
+ phase: "paused",
166
+ recoveryAttempts: recoveryAttempts + 1,
167
+ lastRecoveryReason: reason,
168
+ });
169
+ ctx.ui.notify(`Milestone ${unitId} ${reason}-recovery exhausted ${maxRecoveryAttempts} attempt(s) — worktree branch preserved. Re-run /gsd auto once blockers are resolved.`, "error");
170
+ return "paused";
171
+ }
159
172
  // Retries exhausted — write a blocker placeholder and advance the pipeline
160
173
  // instead of silently stalling.
161
174
  const placeholder = writeBlockerPlaceholder(unitType, unitId, basePath, `${reason} recovery exhausted ${maxRecoveryAttempts} attempts without producing the artifact.`);