gsd-pi 2.70.1 → 2.71.0

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 (149) hide show
  1. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +129 -30
  2. package/dist/resources/extensions/get-secrets-from-user.js +17 -1
  3. package/dist/resources/extensions/gsd/auto-start.js +3 -11
  4. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  5. package/dist/resources/extensions/gsd/custom-workflow-engine.js +16 -12
  6. package/dist/resources/extensions/gsd/file-lock.js +60 -0
  7. package/dist/resources/extensions/gsd/guided-flow.js +12 -10
  8. package/dist/resources/extensions/gsd/init-wizard.js +3 -11
  9. package/dist/resources/extensions/gsd/prompts/discuss.md +31 -13
  10. package/dist/resources/extensions/gsd/state.js +234 -332
  11. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +34 -0
  12. package/dist/resources/extensions/gsd/workflow-events.js +25 -13
  13. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +56 -0
  14. package/dist/resources/extensions/gsd/workflow-mcp.js +1 -1
  15. package/dist/web/standalone/.next/BUILD_ID +1 -1
  16. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  17. package/dist/web/standalone/.next/build-manifest.json +3 -3
  18. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  19. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  20. package/dist/web/standalone/.next/required-server-files.json +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.html +1 -1
  38. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  45. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  47. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  48. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  49. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  50. package/dist/web/standalone/.next/static/chunks/2826.dd3dc8bbd3025fa5.js +9 -0
  51. package/dist/web/standalone/.next/static/chunks/{webpack-6e4d7e9a4f57bed4.js → webpack-b868033a5834586d.js} +1 -1
  52. package/dist/web/standalone/server.js +1 -1
  53. package/package.json +1 -1
  54. package/packages/mcp-server/dist/env-writer.d.ts +39 -0
  55. package/packages/mcp-server/dist/env-writer.d.ts.map +1 -0
  56. package/packages/mcp-server/dist/env-writer.js +158 -0
  57. package/packages/mcp-server/dist/env-writer.js.map +1 -0
  58. package/packages/mcp-server/dist/server.d.ts +11 -2
  59. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  60. package/packages/mcp-server/dist/server.js +102 -2
  61. package/packages/mcp-server/dist/server.js.map +1 -1
  62. package/packages/mcp-server/src/env-writer.test.ts +280 -0
  63. package/packages/mcp-server/src/env-writer.ts +183 -0
  64. package/packages/mcp-server/src/secure-env-collect.test.ts +265 -0
  65. package/packages/mcp-server/src/server.ts +137 -3
  66. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts +2 -0
  67. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.d.ts.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +388 -0
  69. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -0
  70. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +2 -0
  71. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  72. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts +19 -2
  74. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js +50 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -0
  78. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +1 -0
  80. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +168 -23
  83. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +1 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +6 -0
  88. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +58 -2
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  92. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +1 -0
  94. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  96. package/packages/pi-coding-agent/package.json +1 -1
  97. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +468 -0
  98. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  99. package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.ts +58 -2
  100. package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +2 -0
  101. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +198 -29
  102. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +1 -0
  103. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +66 -2
  104. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +1 -1
  105. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +1 -0
  106. package/packages/pi-tui/dist/components/__tests__/input.test.js +9 -0
  107. package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
  108. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts +2 -0
  109. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.d.ts.map +1 -0
  110. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +66 -0
  111. package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -0
  112. package/packages/pi-tui/dist/components/input.d.ts +2 -0
  113. package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
  114. package/packages/pi-tui/dist/components/input.js +7 -4
  115. package/packages/pi-tui/dist/components/input.js.map +1 -1
  116. package/packages/pi-tui/dist/components/markdown.d.ts +3 -0
  117. package/packages/pi-tui/dist/components/markdown.d.ts.map +1 -1
  118. package/packages/pi-tui/dist/components/markdown.js +17 -1
  119. package/packages/pi-tui/dist/components/markdown.js.map +1 -1
  120. package/packages/pi-tui/src/components/__tests__/input.test.ts +11 -0
  121. package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +75 -0
  122. package/packages/pi-tui/src/components/input.ts +7 -4
  123. package/packages/pi-tui/src/components/markdown.ts +22 -1
  124. package/pkg/package.json +1 -1
  125. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +166 -31
  126. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +145 -0
  127. package/src/resources/extensions/get-secrets-from-user.ts +24 -1
  128. package/src/resources/extensions/gsd/auto-start.ts +3 -13
  129. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  130. package/src/resources/extensions/gsd/custom-workflow-engine.ts +19 -14
  131. package/src/resources/extensions/gsd/file-lock.ts +59 -0
  132. package/src/resources/extensions/gsd/guided-flow.ts +12 -9
  133. package/src/resources/extensions/gsd/init-wizard.ts +3 -13
  134. package/src/resources/extensions/gsd/prompts/discuss.md +31 -13
  135. package/src/resources/extensions/gsd/state.ts +274 -344
  136. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +436 -0
  137. package/src/resources/extensions/gsd/tests/discuss-incremental-persistence.test.ts +9 -0
  138. package/src/resources/extensions/gsd/tests/file-lock.test.ts +103 -0
  139. package/src/resources/extensions/gsd/tests/secure-env-collect.test.ts +45 -0
  140. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +76 -0
  141. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +155 -1
  142. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +22 -0
  143. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +60 -25
  144. package/src/resources/extensions/gsd/workflow-events.ts +34 -25
  145. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +76 -0
  146. package/src/resources/extensions/gsd/workflow-mcp.ts +1 -1
  147. package/dist/web/standalone/.next/static/chunks/2826.821e01b07d92e948.js +0 -9
  148. /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → nPky_WQC28aBD77eZsRAB}/_buildManifest.js +0 -0
  149. /package/dist/web/standalone/.next/static/{9pw9EXtXjdM7EFrCXUEPf → nPky_WQC28aBD77eZsRAB}/_ssgManifest.js +0 -0
@@ -12,6 +12,7 @@ import { PartialMessageBuilder, ZERO_USAGE, mapUsage } from "./partial-builder.j
12
12
  import { buildWorkflowMcpServers } from "../gsd/workflow-mcp.js";
13
13
  import { showInterviewRound } from "../shared/tui.js";
14
14
  const OTHER_OPTION_LABEL = "None of the above";
15
+ const SENSITIVE_FIELD_PATTERN = /(password|passphrase|secret|token|api[_\s-]*key|private[_\s-]*key|credential)/i;
15
16
  // ---------------------------------------------------------------------------
16
17
  // Stream factory
17
18
  // ---------------------------------------------------------------------------
@@ -182,6 +183,56 @@ export function parseAskUserQuestionsElicitation(request) {
182
183
  }
183
184
  return questions.length > 0 ? questions : null;
184
185
  }
186
+ function isSecureElicitationField(requestMessage, fieldId, field) {
187
+ if (field.format === "password")
188
+ return true;
189
+ if (field.writeOnly === true)
190
+ return true;
191
+ const rawField = field;
192
+ if (rawField.sensitive === true || rawField["x-sensitive"] === true)
193
+ return true;
194
+ const haystack = [
195
+ requestMessage,
196
+ fieldId.replace(/[_-]+/g, " "),
197
+ typeof field.title === "string" ? field.title : "",
198
+ typeof field.description === "string" ? field.description : "",
199
+ ]
200
+ .join(" ")
201
+ .toLowerCase();
202
+ return SENSITIVE_FIELD_PATTERN.test(haystack);
203
+ }
204
+ export function parseTextInputElicitation(request) {
205
+ if (request.mode && request.mode !== "form")
206
+ return null;
207
+ const schema = request.requestedSchema;
208
+ const fieldsSource = schema?.properties && typeof schema.properties === "object"
209
+ ? schema.properties
210
+ : schema?.keys && typeof schema.keys === "object"
211
+ ? schema.keys
212
+ : undefined;
213
+ if (!fieldsSource)
214
+ return null;
215
+ const requiredSet = new Set(Array.isArray(request.requestedSchema?.required)
216
+ ? request.requestedSchema.required.filter((value) => typeof value === "string")
217
+ : []);
218
+ const fields = [];
219
+ for (const [fieldId, field] of Object.entries(fieldsSource)) {
220
+ if (!field || typeof field !== "object")
221
+ continue;
222
+ if (field.type !== "string")
223
+ continue;
224
+ if (Array.isArray(field.oneOf) && field.oneOf.length > 0)
225
+ continue;
226
+ fields.push({
227
+ id: fieldId,
228
+ title: typeof field.title === "string" && field.title.length > 0 ? field.title : fieldId,
229
+ description: typeof field.description === "string" ? field.description : "",
230
+ required: requiredSet.has(fieldId),
231
+ secure: isSecureElicitationField(request.message, fieldId, field),
232
+ });
233
+ }
234
+ return fields.length > 0 ? fields : null;
235
+ }
185
236
  export function roundResultToElicitationContent(questions, result) {
186
237
  const content = {};
187
238
  for (const question of questions) {
@@ -246,6 +297,38 @@ async function promptElicitationWithDialogs(request, questions, ui, signal) {
246
297
  }
247
298
  return { action: "accept", content };
248
299
  }
300
+ function buildTextInputPromptTitle(request, field) {
301
+ const parts = [
302
+ request.serverName ? `[${request.serverName}]` : "",
303
+ field.title,
304
+ field.description,
305
+ ].filter((part) => typeof part === "string" && part.trim().length > 0);
306
+ return parts.join("\n\n");
307
+ }
308
+ function buildTextInputPlaceholder(field) {
309
+ const desc = field.description.trim();
310
+ if (!desc)
311
+ return field.required ? "Required" : "Leave empty to skip";
312
+ const formatLine = desc
313
+ .split(/\r?\n/)
314
+ .map((line) => line.trim())
315
+ .find((line) => /^format:/i.test(line));
316
+ if (!formatLine)
317
+ return field.required ? "Required" : "Leave empty to skip";
318
+ const hint = formatLine.replace(/^format:\s*/i, "").trim();
319
+ return hint.length > 0 ? hint : field.required ? "Required" : "Leave empty to skip";
320
+ }
321
+ async function promptTextInputElicitation(request, fields, ui, signal) {
322
+ const content = {};
323
+ for (const field of fields) {
324
+ const value = await ui.input(buildTextInputPromptTitle(request, field), buildTextInputPlaceholder(field), { signal, ...(field.secure ? { secure: true } : {}) });
325
+ if (value === undefined) {
326
+ return { action: "cancel" };
327
+ }
328
+ content[field.id] = value;
329
+ }
330
+ return { action: "accept", content };
331
+ }
249
332
  export function createClaudeCodeElicitationHandler(ui) {
250
333
  if (!ui)
251
334
  return undefined;
@@ -254,17 +337,21 @@ export function createClaudeCodeElicitationHandler(ui) {
254
337
  return { action: "decline" };
255
338
  }
256
339
  const questions = parseAskUserQuestionsElicitation(request);
257
- if (!questions) {
258
- return { action: "decline" };
340
+ if (questions) {
341
+ const interviewResult = await showInterviewRound(questions, { signal }, { ui }).catch(() => undefined);
342
+ if (interviewResult && Object.keys(interviewResult.answers).length > 0) {
343
+ return {
344
+ action: "accept",
345
+ content: roundResultToElicitationContent(questions, interviewResult),
346
+ };
347
+ }
348
+ return promptElicitationWithDialogs(request, questions, ui, signal);
259
349
  }
260
- const interviewResult = await showInterviewRound(questions, { signal }, { ui }).catch(() => undefined);
261
- if (interviewResult && Object.keys(interviewResult.answers).length > 0) {
262
- return {
263
- action: "accept",
264
- content: roundResultToElicitationContent(questions, interviewResult),
265
- };
350
+ const textFields = parseTextInputElicitation(request);
351
+ if (textFields) {
352
+ return promptTextInputElicitation(request, textFields, ui, signal);
266
353
  }
267
- return promptElicitationWithDialogs(request, questions, ui, signal);
354
+ return { action: "decline" };
268
355
  };
269
356
  }
270
357
  // ---------------------------------------------------------------------------
@@ -278,6 +365,7 @@ export function createClaudeCodeElicitationHandler(ui) {
278
365
  */
279
366
  export function buildSdkOptions(modelId, prompt, extraOptions = {}) {
280
367
  const mcpServers = buildWorkflowMcpServers();
368
+ const disallowedTools = ["AskUserQuestion"];
281
369
  return {
282
370
  pathToClaudeCodeExecutable: getClaudePath(),
283
371
  model: modelId,
@@ -288,6 +376,7 @@ export function buildSdkOptions(modelId, prompt, extraOptions = {}) {
288
376
  allowDangerouslySkipPermissions: true,
289
377
  settingSources: ["project"],
290
378
  systemPrompt: { type: "preset", preset: "claude_code" },
379
+ disallowedTools,
291
380
  ...(mcpServers ? { mcpServers } : {}),
292
381
  betas: modelId.includes("sonnet") ? ["context-1m-2025-08-07"] : [],
293
382
  ...extraOptions,
@@ -371,9 +460,9 @@ export function extractToolResultsFromSdkUserMessage(message) {
371
460
  }
372
461
  return extracted;
373
462
  }
374
- function attachExternalResultsToToolCalls(toolCalls, toolResultsById) {
375
- for (const block of toolCalls) {
376
- if (block.type !== "toolCall")
463
+ function attachExternalResultsToToolBlocks(toolBlocks, toolResultsById) {
464
+ for (const block of toolBlocks) {
465
+ if (block.type !== "toolCall" && block.type !== "serverToolUse")
377
466
  continue;
378
467
  const externalResult = toolResultsById.get(block.id);
379
468
  if (!externalResult)
@@ -402,8 +491,8 @@ async function pumpSdkMessages(model, context, options, stream) {
402
491
  /** Track the last text content seen across all assistant turns for the final message. */
403
492
  let lastTextContent = "";
404
493
  let lastThinkingContent = "";
405
- /** Collect tool calls from intermediate SDK turns for tool_execution events. */
406
- const intermediateToolCalls = [];
494
+ /** Collect tool blocks from intermediate SDK turns for tool execution rendering. */
495
+ const intermediateToolBlocks = [];
407
496
  /** Preserve real external tool results from Claude Code's synthetic user messages. */
408
497
  const toolResultsById = new Map();
409
498
  try {
@@ -491,9 +580,9 @@ async function pumpSdkMessages(model, context, options, stream) {
491
580
  else if (block.type === "thinking" && block.thinking) {
492
581
  lastThinkingContent = block.thinking;
493
582
  }
494
- else if (block.type === "toolCall") {
495
- // Collect tool calls for externalToolExecution rendering
496
- intermediateToolCalls.push(block);
583
+ else if (block.type === "toolCall" || block.type === "serverToolUse") {
584
+ // Collect tool blocks for externalToolExecution rendering
585
+ intermediateToolBlocks.push(block);
497
586
  }
498
587
  }
499
588
  }
@@ -502,25 +591,35 @@ async function pumpSdkMessages(model, context, options, stream) {
502
591
  for (const { toolUseId, result } of extractToolResultsFromSdkUserMessage(msg)) {
503
592
  toolResultsById.set(toolUseId, result);
504
593
  }
505
- attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
594
+ attachExternalResultsToToolBlocks(intermediateToolBlocks, toolResultsById);
506
595
  // Push a synthetic toolcall_end for each tool call from this turn
507
596
  // so the TUI can render tool results in real-time during the SDK
508
597
  // session instead of waiting until the entire session completes.
509
598
  if (builder) {
510
599
  for (const block of builder.message.content) {
511
- if (block.type !== "toolCall")
512
- continue;
513
600
  const extResult = block.externalResult;
514
601
  if (!extResult)
515
602
  continue;
516
- // Push a toolcall_end with result attached so the chat-controller
517
- // can call updateResult on the pending ToolExecutionComponent.
518
- stream.push({
519
- type: "toolcall_end",
520
- contentIndex: builder.message.content.indexOf(block),
521
- toolCall: block,
522
- partial: builder.message,
523
- });
603
+ const contentIndex = builder.message.content.indexOf(block);
604
+ if (contentIndex < 0)
605
+ continue;
606
+ // Push synthetic completion events with result attached so the
607
+ // chat-controller can update pending ToolExecutionComponents.
608
+ if (block.type === "toolCall") {
609
+ stream.push({
610
+ type: "toolcall_end",
611
+ contentIndex,
612
+ toolCall: block,
613
+ partial: builder.message,
614
+ });
615
+ }
616
+ else if (block.type === "serverToolUse") {
617
+ stream.push({
618
+ type: "server_tool_use",
619
+ contentIndex,
620
+ partial: builder.message,
621
+ });
622
+ }
524
623
  }
525
624
  }
526
625
  builder = null;
@@ -534,8 +633,8 @@ async function pumpSdkMessages(model, context, options, stream) {
534
633
  // events for proper TUI rendering, followed by the text response.
535
634
  const finalContent = [];
536
635
  // Add tool calls from intermediate turns first (renders above text)
537
- attachExternalResultsToToolCalls(intermediateToolCalls, toolResultsById);
538
- finalContent.push(...intermediateToolCalls);
636
+ attachExternalResultsToToolBlocks(intermediateToolBlocks, toolResultsById);
637
+ finalContent.push(...intermediateToolBlocks);
539
638
  // Add text/thinking from the last turn
540
639
  if (builder && builder.message.content.length > 0) {
541
640
  for (const block of builder.message.content) {
@@ -93,7 +93,7 @@ export function detectDestination(basePath) {
93
93
  async function collectOneSecret(ctx, pageIndex, totalPages, keyName, hint, guidance) {
94
94
  if (!ctx.hasUI)
95
95
  return null;
96
- return ctx.ui.custom((tui, theme, _kb, done) => {
96
+ const customResult = await ctx.ui.custom((tui, theme, _kb, done) => {
97
97
  let value = "";
98
98
  let cachedLines;
99
99
  const editorTheme = {
@@ -178,6 +178,22 @@ async function collectOneSecret(ctx, pageIndex, totalPages, keyName, hint, guida
178
178
  handleInput,
179
179
  };
180
180
  });
181
+ // RPC/web surfaces may not implement ctx.ui.custom(). Fall back to a
182
+ // standard input prompt so users can still provide the secret.
183
+ if (customResult !== undefined) {
184
+ return customResult;
185
+ }
186
+ if (typeof ctx.ui?.input !== "function") {
187
+ return null;
188
+ }
189
+ const inputTitle = `Secure value for ${keyName} (${pageIndex + 1}/${totalPages})`;
190
+ const inputPlaceholder = hint || "Enter secret value";
191
+ const inputResult = await ctx.ui.input(inputTitle, inputPlaceholder, { secure: true });
192
+ if (typeof inputResult !== "string") {
193
+ return null;
194
+ }
195
+ const trimmed = inputResult.trim();
196
+ return trimmed.length > 0 ? trimmed : null;
181
197
  }
182
198
  /**
183
199
  * Exported wrapper around collectOneSecret for testing.
@@ -248,17 +248,9 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
248
248
  logWarning("engine", `mkdir failed: ${err instanceof Error ? err.message : String(err)}`);
249
249
  }
250
250
  }
251
- if (ctx.model?.provider === "claude-code") {
252
- try {
253
- const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
254
- const result = ensureProjectWorkflowMcpConfig(base);
255
- if (result.status !== "unchanged") {
256
- ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
257
- }
258
- }
259
- catch (err) {
260
- ctx.ui.notify(`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
261
- }
251
+ {
252
+ const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
253
+ prepareWorkflowMcpForProject(ctx, base);
262
254
  }
263
255
  // Initialize GitServiceImpl
264
256
  s.gitService = new GitServiceImpl(s.basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
@@ -39,6 +39,8 @@ export function registerHooks(pi) {
39
39
  resetToolCallLoopGuard();
40
40
  resetAskUserQuestionsCache();
41
41
  await syncServiceTierStatus(ctx);
42
+ const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
43
+ prepareWorkflowMcpForProject(ctx, process.cwd());
42
44
  // Apply show_token_cost preference (#1515)
43
45
  try {
44
46
  const { loadEffectiveGSDPreferences } = await import("../preferences.js");
@@ -78,6 +80,8 @@ export function registerHooks(pi) {
78
80
  resetAskUserQuestionsCache();
79
81
  clearDiscussionFlowState();
80
82
  await syncServiceTierStatus(ctx);
83
+ const { prepareWorkflowMcpForProject } = await import("../workflow-mcp-auto-prep.js");
84
+ prepareWorkflowMcpForProject(ctx, process.cwd());
81
85
  loadToolApiKeys();
82
86
  });
83
87
  pi.on("before_agent_start", async (event, ctx) => {
@@ -17,6 +17,7 @@ import { parse } from "yaml";
17
17
  import { readGraph, writeGraph, getNextPendingStep, markStepComplete, expandIteration, } from "./graph.js";
18
18
  import { injectContext } from "./context-injector.js";
19
19
  import { parseUnitId } from "./unit-id.js";
20
+ import { withFileLock } from "./file-lock.js";
20
21
  /** Read and parse the frozen DEFINITION.yaml from a run directory. */
21
22
  export function readFrozenDefinition(runDir) {
22
23
  const defPath = join(runDir, "DEFINITION.yaml");
@@ -135,18 +136,21 @@ export class CustomWorkflowEngine {
135
136
  * Returns "milestone-complete" when all steps are now done, "continue" otherwise.
136
137
  */
137
138
  async reconcile(state, completedStep) {
138
- // Re-read the graph from disk so we do not overwrite concurrent
139
- // workflow edits with a stale in-memory snapshot from deriveState().
140
- const graph = readGraph(this.runDir);
141
- // Extract stepId from "<workflowName>/<stepId>"
142
- const { milestone, slice, task } = parseUnitId(completedStep.unitId);
143
- const stepId = task ?? slice ?? milestone;
144
- const updatedGraph = markStepComplete(graph, stepId);
145
- writeGraph(this.runDir, updatedGraph);
146
- const allDone = updatedGraph.steps.every((s) => s.status === "complete" || s.status === "expanded");
147
- return {
148
- outcome: allDone ? "milestone-complete" : "continue",
149
- };
139
+ const graphPath = join(this.runDir, "GRAPH.yaml");
140
+ return await withFileLock(graphPath, () => {
141
+ // Re-read the graph from disk so we do not overwrite concurrent
142
+ // workflow edits with a stale in-memory snapshot from deriveState().
143
+ const graph = readGraph(this.runDir);
144
+ // Extract stepId from "<workflowName>/<stepId>"
145
+ const { milestone, slice, task } = parseUnitId(completedStep.unitId);
146
+ const stepId = task ?? slice ?? milestone;
147
+ const updatedGraph = markStepComplete(graph, stepId);
148
+ writeGraph(this.runDir, updatedGraph);
149
+ const allDone = updatedGraph.steps.every((s) => s.status === "complete" || s.status === "expanded");
150
+ return {
151
+ outcome: allDone ? "milestone-complete" : "continue",
152
+ };
153
+ });
150
154
  }
151
155
  /**
152
156
  * Return UI-facing metadata for progress display.
@@ -0,0 +1,60 @@
1
+ import { existsSync } from "node:fs";
2
+ function _require(name) {
3
+ try {
4
+ return require(name);
5
+ }
6
+ catch {
7
+ try {
8
+ const gsdPiRequire = require("module").createRequire(require("path").join(process.cwd(), "node_modules", "gsd-pi", "index.js"));
9
+ return gsdPiRequire(name);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ }
16
+ export function withFileLockSync(filePath, fn) {
17
+ const lockfile = _require("proper-lockfile");
18
+ if (!lockfile)
19
+ return fn();
20
+ if (!existsSync(filePath))
21
+ return fn();
22
+ try {
23
+ const release = lockfile.lockSync(filePath, { retries: 5, stale: 10000 });
24
+ try {
25
+ return fn();
26
+ }
27
+ finally {
28
+ release();
29
+ }
30
+ }
31
+ catch (err) {
32
+ if (err.code === "ELOCKED") {
33
+ // Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
34
+ return fn();
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+ export async function withFileLock(filePath, fn) {
40
+ const lockfile = _require("proper-lockfile");
41
+ if (!lockfile)
42
+ return await fn();
43
+ if (!existsSync(filePath))
44
+ return await fn();
45
+ try {
46
+ const release = await lockfile.lock(filePath, { retries: 5, stale: 10000 });
47
+ try {
48
+ return await fn();
49
+ }
50
+ finally {
51
+ await release();
52
+ }
53
+ }
54
+ catch (err) {
55
+ if (err.code === "ELOCKED") {
56
+ return await fn();
57
+ }
58
+ throw err;
59
+ }
60
+ }
@@ -334,8 +334,9 @@ function resolveAvailableModel(modelId, availableModels, currentProvider) {
334
334
  * Build the discuss-and-plan prompt for a new milestone.
335
335
  * Used by all three "new milestone" paths (first ever, no active, all complete).
336
336
  */
337
- function buildDiscussPrompt(nextId, preamble, _basePath, preparationContext) {
337
+ function buildDiscussPrompt(nextId, preamble, _basePath, pi, ctx, preparationContext) {
338
338
  const milestoneRel = `.gsd/milestones/${nextId}`;
339
+ const structuredQuestionsAvailable = getStructuredQuestionsAvailability(pi, ctx);
339
340
  const inlinedTemplates = [
340
341
  inlineTemplate("project", "Project"),
341
342
  inlineTemplate("requirements", "Requirements"),
@@ -347,6 +348,7 @@ function buildDiscussPrompt(nextId, preamble, _basePath, preparationContext) {
347
348
  milestoneId: nextId,
348
349
  preamble,
349
350
  preparationContext: preparationContext ?? "",
351
+ structuredQuestionsAvailable,
350
352
  contextPath: `${milestoneRel}/${nextId}-CONTEXT.md`,
351
353
  roadmapPath: `${milestoneRel}/${nextId}-ROADMAP.md`,
352
354
  inlinedTemplates,
@@ -390,7 +392,7 @@ function buildHeadlessDiscussPrompt(nextId, seedContext, _basePath) {
390
392
  * @param basePath - Root directory of the project
391
393
  * @returns The discuss prompt string
392
394
  */
393
- async function prepareAndBuildDiscussPrompt(ctx, nextId, preamble, basePath) {
395
+ async function prepareAndBuildDiscussPrompt(ctx, pi, nextId, preamble, basePath) {
394
396
  const prefs = loadEffectiveGSDPreferences()?.preferences ?? {};
395
397
  // Run preparation if enabled (default: true) — results are injected as
396
398
  // supplementary context into the standard discuss prompt, NOT as a
@@ -421,7 +423,7 @@ async function prepareAndBuildDiscussPrompt(ctx, nextId, preamble, basePath) {
421
423
  logWarning("guided", `preparation failed, proceeding without context: ${err.message}`);
422
424
  }
423
425
  }
424
- return buildDiscussPrompt(nextId, preamble, basePath, preparationContext);
426
+ return buildDiscussPrompt(nextId, preamble, basePath, pi, ctx, preparationContext);
425
427
  }
426
428
  /**
427
429
  * Bootstrap a .gsd/ project from scratch for headless use.
@@ -638,7 +640,7 @@ export async function showDiscuss(ctx, pi, basePath) {
638
640
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
639
641
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
640
642
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: false, createdAt: Date.now() });
641
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
643
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
642
644
  }
643
645
  return;
644
646
  }
@@ -994,7 +996,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
994
996
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
995
997
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
996
998
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
997
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
999
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
998
1000
  return true;
999
1001
  }
1000
1002
  // "back" or null
@@ -1161,7 +1163,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1161
1163
  if (isFirst) {
1162
1164
  // First ever — skip wizard, just ask directly
1163
1165
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1164
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "discuss-milestone");
1166
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "discuss-milestone");
1165
1167
  }
1166
1168
  else {
1167
1169
  const choice = await showNextAction(ctx, {
@@ -1179,7 +1181,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1179
1181
  });
1180
1182
  if (choice === "new_milestone") {
1181
1183
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1182
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1184
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1183
1185
  }
1184
1186
  }
1185
1187
  return;
@@ -1211,7 +1213,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1211
1213
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1212
1214
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
1213
1215
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1214
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1216
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1215
1217
  }
1216
1218
  else if (choice === "status") {
1217
1219
  const { fireStatusViaCommand } = await import("./commands.js");
@@ -1275,7 +1277,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1275
1277
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1276
1278
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
1277
1279
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1278
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1280
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1279
1281
  }
1280
1282
  return;
1281
1283
  }
@@ -1365,7 +1367,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
1365
1367
  const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
1366
1368
  const nextId = nextMilestoneIdReserved(milestoneIds, uniqueMilestoneIds);
1367
1369
  pendingAutoStartMap.set(basePath, { ctx, pi, basePath, milestoneId: nextId, step: stepMode, createdAt: Date.now() });
1368
- await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1370
+ await dispatchWorkflow(pi, await prepareAndBuildDiscussPrompt(ctx, pi, nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "discuss-milestone");
1369
1371
  }
1370
1372
  else if (choice === "discard_milestone") {
1371
1373
  const confirmed = await showConfirm(ctx, {
@@ -225,17 +225,9 @@ export async function showProjectInit(ctx, pi, basePath, detection) {
225
225
  catch {
226
226
  // Non-fatal — STATE.md will be regenerated on next /gsd invocation
227
227
  }
228
- if (ctx.model?.provider === "claude-code") {
229
- try {
230
- const { ensureProjectWorkflowMcpConfig } = await import("./mcp-project-config.js");
231
- const result = ensureProjectWorkflowMcpConfig(basePath);
232
- if (result.status !== "unchanged") {
233
- ctx.ui.notify(`Claude Code MCP prepared at ${result.configPath}`, "info");
234
- }
235
- }
236
- catch (err) {
237
- ctx.ui.notify(`Claude Code MCP prep failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
238
- }
228
+ {
229
+ const { prepareWorkflowMcpForProject } = await import("./workflow-mcp-auto-prep.js");
230
+ prepareWorkflowMcpForProject(ctx, basePath);
239
231
  }
240
232
  ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
241
233
  return { completed: true, bootstrapped: true };
@@ -49,6 +49,26 @@ This happens ONCE, before the first round. The goal: your first questions should
49
49
 
50
50
  For subsequent rounds, continue investigating between rounds — check docs, search, or scout as needed to make each round's questions smarter. But the first-round investigation is mandatory and explicit. Distribute searches across turns rather than clustering them in one turn.
51
51
 
52
+ ## Question Rounds
53
+
54
+ Ask **1–3 questions per round**. Keep each round tightly focused on one or two of the depth checklist dimensions — do not try to cover all six in one round.
55
+
56
+ **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions. **IMPORTANT: Call `ask_user_questions` exactly once per turn. Never make multiple calls with the same or overlapping questions — wait for the user's response before asking the next round.**
57
+
58
+ **If `{{structuredQuestionsAvailable}}` is `false`:** ask questions in plain text. Keep each round to 1–3 focused questions. Wait for answers before asking the next round.
59
+
60
+ After each answer set, investigate further if any answer opens a new unknown, then ask the next round.
61
+
62
+ ### Round cadence
63
+
64
+ After each round of answers, decide whether you already have enough depth to write strong output.
65
+
66
+ - **Incremental persistence:** After every 2 question rounds, silently save a `{{milestoneId}}-CONTEXT-DRAFT.md` using `gsd_summary_save` with `artifact_type: "CONTEXT-DRAFT"` and `milestone_id: "{{milestoneId}}"`. This protects confirmed work against session crashes. Do NOT mention this save to the user.
67
+ - If not ready, continue to the next round immediately. Do **not** ask a meta "ready to wrap up?" question after every round.
68
+ - **Depth-matching rule:** Simple, well-defined work needs fewer rounds — maybe 1–2. Large, ambiguous visions need more — maybe 4+. Do not pad rounds to hit a number. Stop when the Depth Enforcement checklist below is fully satisfied.
69
+ - Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
70
+ - When you genuinely believe the depth checklist is satisfied, move to the Depth Verification step below. Do not ask a separate "ready to wrap up?" gate — the depth verification IS the gate.
71
+
52
72
  ## Questioning Philosophy
53
73
 
54
74
  You are a thinking partner, not an interviewer.
@@ -94,29 +114,27 @@ Do NOT offer to proceed until ALL of the following are satisfied. Track these in
94
114
 
95
115
  Before offering to proceed, demonstrate absorption: reference specific things the user emphasized, specific terminology they used, specific nuance they sharpened — and show how those shaped your understanding. Synthesize, don't recite. "Your emphasis on X led me to prioritize Y over Z" is good. "You said X, you said Y, you said Z" is not. The user should feel heard in the specifics, not just acknowledged in the abstract.
96
116
 
97
- **Questioning depth should match scope.** Simple, well-defined work needs fewer rounds — maybe 1-2. Large, ambiguous visions need more — maybe 4+. Don't pad rounds to hit a number. Stop when the depth checklist is satisfied and you genuinely understand the work.
98
-
99
- Do not count the reflection step as a question round. Rounds start after reflection is confirmed.
100
-
101
117
  ## Depth Verification
102
118
 
103
119
  Before moving to the wrap-up gate, present a structured depth summary as a checkpoint.
104
120
 
105
121
  **Print the summary as normal chat text first** — this is where the formatting renders properly. Structure the summary across the depth checklist dimensions using the user's own terminology and framing. Cover: what you understood them to be building, what shaped your understanding most (their emphasis, constraints, concerns), and any areas where you're least confident in your understanding.
106
122
 
107
- **Then** use `ask_user_questions` with a short confirmation question — NOT the summary itself. The question field is designed for single sentences, not multi-paragraph summaries.
123
+ **Then confirm:**
108
124
 
109
- **Convention:** The question ID must contain `depth_verification` (e.g., `depth_verification_confirm`). This naming convention enables downstream mechanical detection of this step.
125
+ **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` with:
126
+ - header: "Depth Check"
127
+ - question: "Did I capture the depth right?"
128
+ - options: "Yes, you got it (Recommended)", "Not quite — let me clarify"
129
+ - **The question ID must contain `depth_verification`** (e.g., `depth_verification_confirm`) — this naming convention enables downstream mechanical detection and the write-gate.
110
130
 
111
- Example flow:
112
- 1. Print in chat: the full depth summary with markdown formatting (headers, bold, bullets)
113
- 2. Call `ask_user_questions` with: header "Depth Check", question "Did I capture the depth right?", options "Yes, you got it (Recommended)" and "Not quite — let me clarify"
131
+ **If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text: "Did I capture that correctly? If not, tell me what I missed." Wait for explicit confirmation before proceeding. **The same non-bypassable gate applies to the plain-text path** — if the user does not respond, gives an ambiguous answer, or does not explicitly confirm, you MUST re-ask. Never rationalize past a missing confirmation.
114
132
 
115
133
  If they clarify, absorb the correction and re-verify.
116
134
 
117
135
  The depth verification is the required write-gate. Do **not** add another meta "ready to proceed?" checkpoint immediately after it unless there is still material ambiguity.
118
136
 
119
- **CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option. If the user declines, cancels, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
137
+ **CRITICAL — Non-bypassable gate:** The system mechanically blocks CONTEXT.md writes until the user selects the "(Recommended)" option (structured path) or explicitly confirms (plain-text path). If the user declines, cancels, does not respond, or the tool fails, you MUST re-ask — never rationalize past the block ("tool not responding, I'll proceed" is forbidden). The gate exists to protect the user's work; treat a block as an instruction, not an obstacle to work around.
120
138
 
121
139
  ## Wrap-up Gate
122
140
 
@@ -244,7 +262,7 @@ If a milestone has no dependencies, omit the frontmatter. The dependency chain f
244
262
 
245
263
  #### Phase 3: Sequential readiness gate for remaining milestones
246
264
 
247
- For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then use `ask_user_questions` to let the user correct that recommendation. **Non-bypassable:** If `ask_user_questions` fails, errors, returns no response, or the user's response does not match a provided option, you MUST re-ask — never rationalize past the block or auto-select a readiness mode. Present three options:
265
+ For each remaining milestone **one at a time, in sequence**, decide the most likely readiness mode from the evidence you already have, then present the three options below to the user. **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. **If `{{structuredQuestionsAvailable}}` is `false`:** present the options as a plain-text numbered list and ask the user to type their choice. **Non-bypassable:** If the user does not respond, gives an ambiguous answer, or the tool fails, you MUST re-ask — never rationalize past the block or auto-select a readiness mode. Present three options:
248
266
 
249
267
  - **"Discuss now"** — The user wants to conduct a focused discussion for this milestone in the current session, while the context from the broader discussion is still fresh. Proceed with a focused discussion for this milestone (reflection → investigation → questioning → depth verification). When the discussion concludes, write a full `CONTEXT.md`. Then move to the gate for the next milestone.
250
268
  - **"Write draft for later"** — This milestone has seed material from the current conversation but needs its own dedicated discussion in a future session. Write a `CONTEXT-DRAFT.md` capturing the seed material (what was discussed, key ideas, provisional scope, open questions). Mark it clearly as a draft, not a finalized context. **What happens downstream:** When auto-mode reaches this milestone, it pauses and notifies the user: "M00x has draft context — needs discussion. Run /gsd." The `/gsd` wizard shows a "Discuss from draft" option that seeds the new discussion with this draft, so nothing from the current conversation is lost. After the dedicated discussion produces a full CONTEXT.md, the draft file is automatically deleted.
@@ -256,9 +274,9 @@ Before writing each milestone's CONTEXT.md (whether primary or secondary), you M
256
274
 
257
275
  1. **Read the actual code** for every file or module you reference. Confirm APIs exist, check what functions actually do, identify phantom capabilities (code that exists but isn't wired up).
258
276
  2. **Check for stale assumptions** — the codebase changes. Verify referenced modules still work as described.
259
- 3. **Present findings** — use `ask_user_questions` with a question ID containing BOTH `depth_verification` AND the milestone ID (e.g., `depth_verification_M002`). Present: what you're about to write, key technical findings from investigation, risks the code review surfaced.
277
+ 3. **Present findings** — **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` with a question ID containing BOTH `depth_verification` AND the milestone ID (e.g., `depth_verification_M002`). Present: what you're about to write, key technical findings from investigation, risks the code review surfaced. **If `{{structuredQuestionsAvailable}}` is `false`:** present the same findings in plain text and ask for explicit confirmation before proceeding.
260
278
 
261
- **The system mechanically blocks CONTEXT.md writes until the per-milestone depth verification passes.** Each milestone needs its own verification — one global verification does not unlock all milestones.
279
+ **The system mechanically blocks CONTEXT.md writes until the per-milestone depth verification passes** (structured path: user selects "(Recommended)" option; plain-text path: user explicitly confirms). Each milestone needs its own verification — one global verification does not unlock all milestones.
262
280
 
263
281
  **Why sequential, not batch:** After writing the primary milestone's context and roadmap, the agent still has context window capacity. Asking one milestone at a time lets the user decide per-milestone whether to invest that remaining capacity in a focused discussion now, or defer to a future session. A batch question ("Ready/Draft/Queue for M002, M003, M004?") forces the user to decide everything upfront without knowing how much session capacity remains.
264
282