gsd-pi 2.76.0-dev.4c866b677 → 2.76.0-dev.7218806ab

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 (187) hide show
  1. package/dist/claude-cli-check.js +32 -3
  2. package/dist/mcp-server.d.ts +7 -0
  3. package/dist/mcp-server.js +35 -1
  4. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  6. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  7. package/dist/resources/extensions/gsd/auto-start.js +11 -15
  8. package/dist/resources/extensions/gsd/auto.js +13 -17
  9. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  11. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  12. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  15. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  16. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  17. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  18. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  19. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  20. package/dist/resources/extensions/gsd/gsd-db.js +3 -1
  21. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  22. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  23. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  24. package/dist/resources/extensions/gsd/model-router.js +36 -3
  25. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  26. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  27. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  28. package/dist/resources/extensions/gsd/preferences.js +17 -17
  29. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  30. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  31. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  32. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  33. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  34. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  35. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  36. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  37. package/dist/web/standalone/.next/BUILD_ID +1 -1
  38. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  39. package/dist/web/standalone/.next/build-manifest.json +2 -2
  40. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  41. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.html +1 -1
  58. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  65. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +1 -1
  71. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  72. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  73. package/packages/mcp-server/dist/remote-questions.js +732 -0
  74. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  75. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  76. package/packages/mcp-server/dist/server.js +18 -1
  77. package/packages/mcp-server/dist/server.js.map +1 -1
  78. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  79. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  80. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  81. package/packages/mcp-server/package.json +2 -1
  82. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  83. package/packages/mcp-server/src/remote-questions.ts +916 -0
  84. package/packages/mcp-server/src/server.ts +19 -1
  85. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  86. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  87. package/packages/mcp-server/tsconfig.test.json +19 -0
  88. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  89. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  91. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  92. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  93. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  95. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  96. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  97. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  98. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  99. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  102. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  105. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  107. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  109. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  111. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  113. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  114. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  116. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  118. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  123. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  124. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  125. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  126. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  127. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  128. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  129. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  130. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  131. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  132. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  133. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  134. package/src/resources/extensions/gsd/auto-post-unit.ts +0 -1
  135. package/src/resources/extensions/gsd/auto-start.ts +13 -16
  136. package/src/resources/extensions/gsd/auto.ts +12 -17
  137. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  138. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  139. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  140. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  141. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  142. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  143. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  144. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  145. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  146. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  147. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  148. package/src/resources/extensions/gsd/gsd-db.ts +3 -1
  149. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  150. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  151. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  152. package/src/resources/extensions/gsd/model-router.ts +42 -1
  153. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  154. package/src/resources/extensions/gsd/preferences-types.ts +38 -0
  155. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  156. package/src/resources/extensions/gsd/preferences.ts +17 -17
  157. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  158. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  159. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  160. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  161. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  162. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  163. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +64 -0
  164. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  165. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  166. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  167. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  168. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  169. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  170. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  171. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  172. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  173. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  174. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  175. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  176. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  177. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  178. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  179. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  180. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  181. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  182. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  183. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  184. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  185. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  186. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_buildManifest.js +0 -0
  187. /package/dist/web/standalone/.next/static/{jDqWYbuP_CG6Kjc-uKwkN → 5qAwYhcU5Fs2VOq_R8lOc}/_ssgManifest.js +0 -0
@@ -130,8 +130,20 @@ interface PendingAutoStartEntry {
130
130
  milestoneId: string; // the milestone being discussed
131
131
  step?: boolean; // preserve step mode through discuss → auto transition
132
132
  createdAt: number; // timestamp for staleness detection (#3274)
133
+ // #4573: counter for how many times the LLM emitted the ready phrase
134
+ // without writing the required artifacts. Cleared on entry delete/recreate.
135
+ readyRejectCount?: number;
133
136
  }
134
137
 
138
+ // #4573: cap for how many times we nudge the LLM after a premature ready
139
+ // phrase before giving up and asking the user to re-run /gsd.
140
+ const MAX_READY_REJECTS = 2;
141
+
142
+ // #4573: matches the canonical ready phrase the discuss prompt asks the LLM
143
+ // to emit. Accepts any M-prefixed milestone ID (three digits + optional
144
+ // suffix) with optional trailing punctuation.
145
+ const READY_PHRASE_RE = /\bMilestone\s+M\d{3}[A-Z0-9-]*\s+ready\.?/i;
146
+
135
147
  const pendingAutoStartMap = new Map<string, PendingAutoStartEntry>();
136
148
 
137
149
  /**
@@ -279,6 +291,215 @@ export function checkAutoStartAfterDiscuss(): boolean {
279
291
  return true;
280
292
  }
281
293
 
294
+ /**
295
+ * Extract the concatenated text content from an assistant message, whether it
296
+ * stores content as a string or as an array of text blocks.
297
+ */
298
+ function extractAssistantText(msg: any): string {
299
+ if (!msg) return "";
300
+ const content = msg.content;
301
+ if (typeof content === "string") return content;
302
+ if (!Array.isArray(content)) return "";
303
+ const parts: string[] = [];
304
+ for (const block of content) {
305
+ if (!block || typeof block !== "object") continue;
306
+ if (block.type === "text" && typeof block.text === "string") parts.push(block.text);
307
+ }
308
+ return parts.join("\n");
309
+ }
310
+
311
+ /**
312
+ * Return true if the assistant message contains any tool-use block.
313
+ */
314
+ function hasToolUse(msg: any): boolean {
315
+ if (!msg) return false;
316
+ const content = msg.content;
317
+ if (!Array.isArray(content)) return false;
318
+ return content.some((b: any) => b && typeof b === "object" && (b.type === "tool_use" || b.type === "tool-use"));
319
+ }
320
+
321
+ /**
322
+ * #4573 — Detect and recover from the "ready phrase without files" failure mode.
323
+ *
324
+ * When the LLM emits "Milestone {{id}} ready." but has not written CONTEXT.md
325
+ * or ROADMAP.md, `checkAutoStartAfterDiscuss()` silently returns false and the
326
+ * next /gsd invocation loops into the "All milestones complete" warning.
327
+ *
328
+ * This function, called from `handleAgentEnd` after `checkAutoStartAfterDiscuss`
329
+ * returns false, pattern-matches the ready phrase on the last assistant message.
330
+ * If it fired AND neither CONTEXT.md nor ROADMAP.md exists, it:
331
+ * 1. Notifies the user that the signal was rejected.
332
+ * 2. Injects a system message via `pi.sendMessage(..., {triggerTurn:true})`
333
+ * telling the LLM the signal was premature and to emit the writes now.
334
+ * 3. Caps at `MAX_READY_REJECTS` per-entry; beyond that, gives up and asks
335
+ * the user to re-run /gsd.
336
+ *
337
+ * Returns true when a nudge (or give-up) was emitted, signaling the caller to
338
+ * skip `resolveAgentEnd`.
339
+ */
340
+ export function maybeHandleReadyPhraseWithoutFiles(event: { messages: any[] }): boolean {
341
+ const entry = _getPendingAutoStart();
342
+ if (!entry) return false;
343
+ const { ctx, pi, basePath, milestoneId } = entry;
344
+
345
+ // Gate: last assistant message must contain the ready phrase
346
+ const lastMsg = event.messages[event.messages.length - 1];
347
+ const text = extractAssistantText(lastMsg);
348
+ if (!READY_PHRASE_RE.test(text)) return false;
349
+
350
+ // Gate: artifacts must still be missing — if they exist, the happy path
351
+ // already fired and we have nothing to do.
352
+ const contextFile = resolveMilestoneFile(basePath, milestoneId, "CONTEXT");
353
+ const roadmapFile = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
354
+ if (contextFile || roadmapFile) return false;
355
+
356
+ entry.readyRejectCount = (entry.readyRejectCount ?? 0) + 1;
357
+
358
+ if (entry.readyRejectCount > MAX_READY_REJECTS) {
359
+ // Give up: clear state and tell the user to re-run /gsd. Avoids an
360
+ // infinite nudge loop when the LLM never produces the writes.
361
+ pendingAutoStartMap.delete(basePath);
362
+ ctx.ui.notify(
363
+ `Milestone ${milestoneId}: LLM signaled "ready" ${entry.readyRejectCount} times without writing files. ` +
364
+ `Stopping auto-nudge. Run /gsd to try again.`,
365
+ "error",
366
+ );
367
+ return true;
368
+ }
369
+
370
+ ctx.ui.notify(
371
+ `Milestone ${milestoneId}: "ready" signal rejected — CONTEXT.md and ROADMAP.md are missing. Asking the LLM to complete the writes.`,
372
+ "warning",
373
+ );
374
+
375
+ const nudge =
376
+ `You emitted "Milestone ${milestoneId} ready." but neither ` +
377
+ `.gsd/milestones/${milestoneId}/${milestoneId}-CONTEXT.md nor ` +
378
+ `.gsd/milestones/${milestoneId}/${milestoneId}-ROADMAP.md exists on disk. ` +
379
+ `The ready phrase is a POST-WRITE signal and has been rejected. ` +
380
+ `In this turn: (1) write PROJECT.md, REQUIREMENTS.md, and the milestone ` +
381
+ `CONTEXT.md, (2) call gsd_plan_milestone, then (3) emit the ready phrase. ` +
382
+ `Do not describe these steps — execute them as tool calls. ` +
383
+ `This is retry ${entry.readyRejectCount}/${MAX_READY_REJECTS}; further ` +
384
+ `premature signals will clear the session.`;
385
+
386
+ try {
387
+ pi.sendMessage(
388
+ { customType: "gsd-ready-no-files", content: nudge, display: false },
389
+ { triggerTurn: true },
390
+ );
391
+ } catch (e) {
392
+ logWarning("guided", `ready-phrase nudge sendMessage failed: ${(e as Error).message}`);
393
+ return false;
394
+ }
395
+ return true;
396
+ }
397
+
398
+ /**
399
+ * #4573 — Detect and recover from the "announces tool, never calls it" stall.
400
+ *
401
+ * The LLM emits text like "I'll now write the CONTEXT.md file" but the turn
402
+ * ends with zero tool-use blocks. The harness has no post-turn tool-call
403
+ * validation, so the unit promise resolves and the user sees a stalled state.
404
+ *
405
+ * This function, called from `handleAgentEnd`, inspects the last assistant
406
+ * message. If ALL of the following are true, it injects a recovery message:
407
+ * - Text-only (no tool-use blocks)
408
+ * - Contains a commit-intent phrase ("I'll write", "I'll call", etc.)
409
+ * - Auto-mode is active OR a discussion autostart is pending
410
+ * - `emptyTurnRetryCount` is under the cap
411
+ *
412
+ * Per-handler state is held on the `PendingAutoStartEntry` when present, and
413
+ * on a module-level map otherwise. The counter resets on any successful
414
+ * tool-use turn via `resetEmptyTurnCounter`.
415
+ */
416
+ const emptyTurnCounterByBase = new Map<string, number>();
417
+ const MAX_EMPTY_TURN_RETRIES = 2;
418
+
419
+ // Phrases that indicate the LLM is about to do something but has not yet.
420
+ // Kept tight to avoid flagging legitimate narration like "I'll wait for your answer."
421
+ const COMMIT_INTENT_RE =
422
+ /\b(?:I['’]ll|I will|Next,? I['’]ll|Now I['’]ll|Let me|I['’]m going to|I am going to)\s+(?:now\s+)?(?:write|create|call|invoke|update|add|make|run|execute|generate|produce|emit|compose|implement|save|apply|commit)\b/i;
423
+
424
+ /**
425
+ * Reset the empty-turn counter for a basePath after a successful tool-use turn.
426
+ * Called from handleAgentEnd when the last message contains tool_use blocks.
427
+ */
428
+ export function resetEmptyTurnCounter(basePath?: string): void {
429
+ if (basePath) emptyTurnCounterByBase.delete(basePath);
430
+ else emptyTurnCounterByBase.clear();
431
+ }
432
+
433
+ export function maybeHandleEmptyIntentTurn(
434
+ event: { messages: any[] },
435
+ isAuto: boolean,
436
+ ): boolean {
437
+ // Gate: only fire when there is system-driven work in flight. Interactive
438
+ // /gsd discuss (user-driven) produces legitimate text-only turns.
439
+ if (!isAuto && pendingAutoStartMap.size === 0) return false;
440
+
441
+ const lastMsg = event.messages[event.messages.length - 1];
442
+ if (!lastMsg) return false;
443
+ if (hasToolUse(lastMsg)) return false;
444
+
445
+ const text = extractAssistantText(lastMsg).trim();
446
+ if (!text) return false;
447
+
448
+ // Skip if the LLM is emitting the ready phrase — that is the ready-no-files
449
+ // path, handled by maybeHandleReadyPhraseWithoutFiles.
450
+ if (READY_PHRASE_RE.test(text)) return false;
451
+
452
+ // Skip if the LLM is clearly handing back to the user. Keep the heuristic
453
+ // tight: a trailing question mark on the last non-empty line is the common
454
+ // signal for "I asked the user a question and stopped."
455
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
456
+ const lastLine = lines[lines.length - 1] ?? "";
457
+ if (lastLine.endsWith("?")) return false;
458
+
459
+ // Must contain a commit-intent phrase — this is the stall we care about.
460
+ if (!COMMIT_INTENT_RE.test(text)) return false;
461
+
462
+ // Resolve the target basePath + pi for injection. Prefer the pending
463
+ // autostart entry (discuss flow); otherwise we cannot inject.
464
+ const entry = _getPendingAutoStart();
465
+ if (!entry) return false;
466
+ const { ctx, pi, basePath } = entry;
467
+
468
+ const count = (emptyTurnCounterByBase.get(basePath) ?? 0) + 1;
469
+ emptyTurnCounterByBase.set(basePath, count);
470
+
471
+ if (count > MAX_EMPTY_TURN_RETRIES) {
472
+ ctx.ui.notify(
473
+ `Empty-turn recovery: LLM announced intent ${count} times without calling any tool. ` +
474
+ `Stopping auto-nudge.`,
475
+ "error",
476
+ );
477
+ return false; // let the normal flow resolve/pause the unit
478
+ }
479
+
480
+ ctx.ui.notify(
481
+ `Empty-turn detected: LLM announced intent but called no tool. Prompting it to execute.`,
482
+ "info",
483
+ );
484
+
485
+ const nudge =
486
+ `Your last turn announced an action (e.g. "I'll write…" or "Let me call…") ` +
487
+ `but contained no tool call. The system records zero tool-use blocks for ` +
488
+ `that turn. Execute the announced action NOW as a tool call in this turn. ` +
489
+ `Do not describe it again. Retry ${count}/${MAX_EMPTY_TURN_RETRIES}.`;
490
+
491
+ try {
492
+ pi.sendMessage(
493
+ { customType: "gsd-empty-turn-recovery", content: nudge, display: false },
494
+ { triggerTurn: true },
495
+ );
496
+ } catch (e) {
497
+ logWarning("guided", `empty-turn nudge sendMessage failed: ${(e as Error).message}`);
498
+ return false;
499
+ }
500
+ return true;
501
+ }
502
+
282
503
  /**
283
504
  * Extract milestone IDs from PROJECT.md milestone sequence table.
284
505
  * Looks for rows like "| M001 | Name | Status |" and extracts the ID column.
@@ -108,6 +108,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
108
108
  let data = initialData;
109
109
  let cachedLines: string[] | undefined;
110
110
  let refreshInFlight = false;
111
+ let isDisposed = false;
111
112
 
112
113
  const refresh = async () => {
113
114
  if (refreshInFlight) return;
@@ -115,7 +116,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
115
116
  try {
116
117
  data = loadHealthWidgetData(basePath);
117
118
  cachedLines = undefined;
118
- _tui.requestRender();
119
+ if (!isDisposed) _tui.requestRender();
119
120
  } catch { /* non-fatal */ } finally {
120
121
  refreshInFlight = false;
121
122
  }
@@ -140,6 +141,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
140
141
  },
141
142
  invalidate(): void { cachedLines = undefined; cachedWidth = undefined; },
142
143
  dispose(): void {
144
+ isDisposed = true;
143
145
  clearInterval(refreshTimer);
144
146
  },
145
147
  };
@@ -35,6 +35,12 @@ export interface ProviderInfo {
35
35
  export const PROVIDER_REGISTRY: ProviderInfo[] = [
36
36
  // LLM Providers
37
37
  { id: "anthropic", label: "Anthropic (Claude)", category: "llm", envVar: "ANTHROPIC_API_KEY", prefixes: ["sk-ant-"], hasOAuth: true, dashboardUrl: "console.anthropic.com" },
38
+ // Claude Code CLI: routes through the local `claude` binary — no API key,
39
+ // authentication is handled by the CLI's own OAuth flow.
40
+ // Referenced by doctor-providers.ts, auto-model-selection.ts, and others;
41
+ // must be in the canonical registry so all consumers see the same catalog.
42
+ // See: https://github.com/gsd-build/gsd-2/issues/4541
43
+ { id: "claude-code", label: "Claude Code CLI", category: "llm", hasOAuth: true },
38
44
  { id: "openai", label: "OpenAI", category: "llm", envVar: "OPENAI_API_KEY", prefixes: ["sk-"], dashboardUrl: "platform.openai.com/api-keys" },
39
45
  { id: "github-copilot", label: "GitHub Copilot", category: "llm", envVar: "GITHUB_TOKEN", hasOAuth: true },
40
46
  { id: "openai-codex", label: "ChatGPT Plus/Pro (Codex)",category: "llm", hasOAuth: true },
@@ -561,6 +561,23 @@ function bareModelId(modelId: string): string {
561
561
  return modelId.includes("/") ? modelId.split("/").pop()! : modelId;
562
562
  }
563
563
 
564
+ // ─── Provider-specific Tool Limits ─────────────────────────────────────────
565
+
566
+ /**
567
+ * Groq enforces a hard limit of 128 tools per request.
568
+ * Requests exceeding this limit receive a 400 error:
569
+ * "maximum number of items is 128"
570
+ * @see https://console.groq.com/docs/tool-use
571
+ */
572
+ export const GROQ_MAX_TOOLS = 128;
573
+
574
+ /**
575
+ * Provider IDs that map to the Groq API backend.
576
+ * Used to detect Groq at the GSD routing layer where only the provider string
577
+ * is available (the pi-ai openai-completions adapter is shared across providers).
578
+ */
579
+ const GROQ_PROVIDER_IDS = new Set(["groq"]);
580
+
564
581
  // ─── Tool Compatibility Filter (ADR-005 Phase 3) ───────────────────────────
565
582
 
566
583
  /**
@@ -588,10 +605,17 @@ export function isToolCompatibleWithProvider(
588
605
  /**
589
606
  * Filter a list of tool names to only those compatible with a provider.
590
607
  * Used by the routing pipeline to adjust tool sets when switching providers.
608
+ *
609
+ * @param toolNames - The full list of active tool names to filter.
610
+ * @param providerApi - The pi-ai API string (e.g. "openai-completions").
611
+ * @param provider - Optional provider ID (e.g. "groq"). Used to apply
612
+ * provider-specific limits that can't be expressed as API-level capabilities
613
+ * (e.g. Groq's 128-tool hard limit on the shared openai-completions adapter).
591
614
  */
592
615
  export function filterToolsForProvider(
593
616
  toolNames: string[],
594
617
  providerApi: string,
618
+ provider?: string,
595
619
  ): { compatible: string[]; filtered: string[] } {
596
620
  const providerCaps = getProviderCapabilities(providerApi);
597
621
 
@@ -611,6 +635,17 @@ export function filterToolsForProvider(
611
635
  }
612
636
  }
613
637
 
638
+ // Groq enforces a hard limit of 128 tools per request (#4376).
639
+ // Trim the compatible list to GROQ_MAX_TOOLS and move the excess to filtered.
640
+ if (provider && GROQ_PROVIDER_IDS.has(provider) && compatible.length > GROQ_MAX_TOOLS) {
641
+ const trimmed = compatible.splice(GROQ_MAX_TOOLS);
642
+ filtered.push(...trimmed);
643
+ console.warn(
644
+ `[gsd] Groq tool limit: ${compatible.length + trimmed.length} tools active but Groq allows at most ${GROQ_MAX_TOOLS}. ` +
645
+ `Trimming to the first ${GROQ_MAX_TOOLS} tools. Removed: ${trimmed.join(", ")}`,
646
+ );
647
+ }
648
+
614
649
  return { compatible, filtered };
615
650
  }
616
651
 
@@ -620,11 +655,17 @@ export function filterToolsForProvider(
620
655
  *
621
656
  * This is a hard filter only — it removes tools that would fail at the
622
657
  * provider level. It does NOT remove tools based on soft heuristics.
658
+ *
659
+ * @param activeToolNames - The full list of currently active tool names.
660
+ * @param selectedModelApi - The pi-ai API string for the selected model.
661
+ * @param provider - Optional provider ID (e.g. "groq") for provider-specific
662
+ * limits beyond what the API-level capability profile expresses.
623
663
  */
624
664
  export function adjustToolSet(
625
665
  activeToolNames: string[],
626
666
  selectedModelApi: string,
667
+ provider?: string,
627
668
  ): { toolNames: string[]; removedTools: string[] } {
628
- const { compatible, filtered } = filterToolsForProvider(activeToolNames, selectedModelApi);
669
+ const { compatible, filtered } = filterToolsForProvider(activeToolNames, selectedModelApi, provider);
629
670
  return { toolNames: compatible, removedTools: filtered };
630
671
  }
@@ -91,8 +91,13 @@ export function extractPackageReferences(description: string): string[] {
91
91
  }
92
92
  }
93
93
 
94
- // require('pkg') or import from 'pkg' in code blocks
95
- const importPattern = /(?:require\s*\(\s*['"]|from\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
94
+ // require('pkg') or `import ... from 'pkg'` in code blocks.
95
+ // The `from\s+['"]` branch MUST be preceded by an `import` keyword so that
96
+ // natural-language prose like `from "What's Next"` or `from 'master'` does
97
+ // not produce false package-existence failures. Requiring the leading import
98
+ // keyword anchors the match to JavaScript/TypeScript syntax.
99
+ // See: https://github.com/gsd-build/gsd-2/issues/4388
100
+ const importPattern = /(?:require\s*\(\s*['"]|import\b[\s\S]*?\bfrom\s+['"])([a-zA-Z0-9@/_-]+)['"\)]/g;
96
101
  let importMatch: RegExpExecArray | null;
97
102
  while ((importMatch = importPattern.exec(description)) !== null) {
98
103
  // Skip relative imports and node builtins
@@ -325,7 +330,12 @@ function extractPathFromAnnotation(raw: string): string {
325
330
 
326
331
  const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/);
327
332
  if (annotatedMatch) {
328
- return annotatedMatch[1].trim();
333
+ const prefix = annotatedMatch[1].trim();
334
+ const prefixBacktickMatch = prefix.match(/`([^`]+)`/);
335
+ if (prefixBacktickMatch && looksLikePathOrUrl(prefixBacktickMatch[1].trim())) {
336
+ return prefixBacktickMatch[1].trim();
337
+ }
338
+ return prefix.replace(/`/g, "").trim();
329
339
  }
330
340
 
331
341
  // Fallback: scan all backticked tokens and return the first one that looks
@@ -388,13 +398,19 @@ function containsGlobPattern(candidate: string): boolean {
388
398
 
389
399
  /**
390
400
  * Build a set of files that will be created by tasks up to (but not including) taskIndex.
401
+ * Also includes outputs of completed tasks at any position — a completed task has already
402
+ * run and its outputs are available regardless of sequence position or disk state (#4071).
391
403
  * All paths are normalized for consistent comparison.
392
404
  */
393
405
  function getExpectedOutputsUpTo(tasks: TaskRow[], taskIndex: number): Set<string> {
394
406
  const outputs = new Set<string>();
395
- for (let i = 0; i < taskIndex; i++) {
396
- for (const file of tasks[i].expected_output) {
397
- outputs.add(normalizeFilePath(file));
407
+ for (let i = 0; i < tasks.length; i++) {
408
+ const task = tasks[i];
409
+ // Include prior tasks (i < taskIndex) OR completed tasks at any position
410
+ if (i < taskIndex || task.status === "completed") {
411
+ for (const file of task.expected_output) {
412
+ outputs.add(normalizeFilePath(file));
413
+ }
398
414
  }
399
415
  }
400
416
  return outputs;
@@ -481,13 +497,19 @@ export function checkTaskOrdering(
481
497
  const results: PreExecutionCheckJSON[] = [];
482
498
 
483
499
  // Build map: normalized file → task index that creates it
484
- const fileCreators = new Map<string, { taskId: string; index: number; originalPath: string }>();
500
+ const fileCreators = new Map<string, { taskId: string; index: number; originalPath: string; completed: boolean }>();
485
501
  for (let i = 0; i < tasks.length; i++) {
486
502
  const task = tasks[i];
487
503
  for (const file of task.expected_output) {
488
504
  const normalizedFile = normalizeFilePath(file);
489
- if (!fileCreators.has(normalizedFile)) {
490
- fileCreators.set(normalizedFile, { taskId: task.id, index: i, originalPath: file });
505
+ const existing = fileCreators.get(normalizedFile);
506
+ if (!existing || (!existing.completed && task.status === "completed")) {
507
+ fileCreators.set(normalizedFile, {
508
+ taskId: task.id,
509
+ index: i,
510
+ originalPath: file,
511
+ completed: task.status === "completed",
512
+ });
491
513
  }
492
514
  }
493
515
  }
@@ -511,7 +533,11 @@ export function checkTaskOrdering(
511
533
  const creator = fileCreators.get(normalizedFile);
512
534
  const absolutePath = resolve(basePath, normalizedFile);
513
535
  const existsOnDisk = existsSync(absolutePath);
514
- if (creator && creator.index > i && !existsOnDisk) {
536
+ // Skip if the creating task has already completed its output is available
537
+ // regardless of disk state (e.g. file was a temp artifact cleaned up after
538
+ // the task ran, or a replan introduced a new earlier-sequence task that
539
+ // reads this pre-execution output). (#4071)
540
+ if (creator && creator.index > i && !existsOnDisk && !creator.completed) {
515
541
  // Task reads file that is created later — impossible ordering
516
542
  results.push({
517
543
  category: "file",
@@ -28,6 +28,37 @@ export interface ContextManagementConfig {
28
28
  compaction_threshold_percent?: number; // default: 0.70, range: 0.5-0.95
29
29
  tool_result_max_chars?: number; // default: 800, range: 200-10000
30
30
  }
31
+
32
+ /**
33
+ * Opt-in tool-output sandboxing for sub-sessions. When enabled, the gsd_exec
34
+ * MCP tool runs scripts in an isolated subprocess and returns only a short
35
+ * digest to the calling agent's context window; full stdout/stderr persist
36
+ * in the project memory store and can be retrieved by id later.
37
+ *
38
+ * Inspired by mksglu/context-mode (Elastic License 2.0). This is an
39
+ * independent implementation — no upstream code is incorporated.
40
+ */
41
+ export interface ContextModeConfig {
42
+ /** Master switch. Default: true (opt-out via `enabled: false`). */
43
+ enabled?: boolean;
44
+ /** Per-invocation timeout in milliseconds. Default: 30_000. Range: 1_000–600_000. */
45
+ exec_timeout_ms?: number;
46
+ /** Cap on persisted stdout bytes per invocation. Default: 1_048_576 (1 MiB). Range: 4_096–16_777_216. */
47
+ exec_stdout_cap_bytes?: number;
48
+ /** Number of trailing stdout characters returned in the digest. Default: 300. Range: 0–4_000. */
49
+ exec_digest_chars?: number;
50
+ /** Environment variables forwarded to sandboxed processes (case-sensitive names). PATH and HOME are always forwarded. */
51
+ exec_env_allowlist?: string[];
52
+ }
53
+
54
+ /**
55
+ * Resolve whether context-mode features (gsd_exec sandbox + compaction
56
+ * snapshot) should be active. Default is ON: missing config or missing
57
+ * `enabled` is treated as true. Only `enabled: false` disables.
58
+ */
59
+ export function isContextModeEnabled(prefs: { context_mode?: ContextModeConfig } | null | undefined): boolean {
60
+ return prefs?.context_mode?.enabled !== false;
61
+ }
31
62
  import type { GitHubSyncConfig } from "../github-sync/types.js";
32
63
 
33
64
  // ─── Workflow Modes ──────────────────────────────────────────────────────────
@@ -117,6 +148,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
117
148
  "flat_rate_providers",
118
149
  "language",
119
150
  "context_window_override",
151
+ "context_mode",
120
152
  ]);
121
153
 
122
154
  /** Canonical list of all dispatch unit types. */
@@ -300,6 +332,12 @@ export interface GSDPreferences {
300
332
  */
301
333
  context_window_override?: number;
302
334
  context_management?: ContextManagementConfig;
335
+ /**
336
+ * Tool-output sandboxing via gsd_exec. Keeps sub-session context windows
337
+ * clean by running scripts in a subprocess and only surfacing a short
338
+ * digest. See `ContextModeConfig`. Default: disabled.
339
+ */
340
+ context_mode?: ContextModeConfig;
303
341
  token_profile?: TokenProfile;
304
342
  phases?: PhaseSkipPreferences;
305
343
  auto_visualize?: boolean;
@@ -644,6 +644,50 @@ export function validatePreferences(preferences: GSDPreferences): {
644
644
  }
645
645
  }
646
646
 
647
+ // ─── Context Mode (gsd_exec sandbox) ────────────────────────────────────
648
+ if (preferences.context_mode !== undefined) {
649
+ if (typeof preferences.context_mode === "object" && preferences.context_mode !== null) {
650
+ const cmode = preferences.context_mode as unknown as Record<string, unknown>;
651
+ const validCmode: Record<string, unknown> = {};
652
+
653
+ if (cmode.enabled !== undefined) {
654
+ if (typeof cmode.enabled === "boolean") validCmode.enabled = cmode.enabled;
655
+ else errors.push("context_mode.enabled must be a boolean");
656
+ }
657
+ if (cmode.exec_timeout_ms !== undefined) {
658
+ const t = cmode.exec_timeout_ms;
659
+ if (typeof t === "number" && t >= 1000 && t <= 600_000) validCmode.exec_timeout_ms = Math.floor(t);
660
+ else errors.push("context_mode.exec_timeout_ms must be a number between 1000 and 600000");
661
+ }
662
+ if (cmode.exec_stdout_cap_bytes !== undefined) {
663
+ const b = cmode.exec_stdout_cap_bytes;
664
+ if (typeof b === "number" && b >= 4096 && b <= 16_777_216) validCmode.exec_stdout_cap_bytes = Math.floor(b);
665
+ else errors.push("context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216");
666
+ }
667
+ if (cmode.exec_digest_chars !== undefined) {
668
+ const c = cmode.exec_digest_chars;
669
+ if (typeof c === "number" && c >= 0 && c <= 4000) validCmode.exec_digest_chars = Math.floor(c);
670
+ else errors.push("context_mode.exec_digest_chars must be a number between 0 and 4000");
671
+ }
672
+ if (cmode.exec_env_allowlist !== undefined) {
673
+ if (
674
+ Array.isArray(cmode.exec_env_allowlist) &&
675
+ cmode.exec_env_allowlist.every((v) => typeof v === "string" && /^[A-Z_][A-Z0-9_]*$/i.test(v))
676
+ ) {
677
+ validCmode.exec_env_allowlist = cmode.exec_env_allowlist;
678
+ } else {
679
+ errors.push("context_mode.exec_env_allowlist must be an array of valid env var names");
680
+ }
681
+ }
682
+
683
+ if (Object.keys(validCmode).length > 0) {
684
+ validated.context_mode = validCmode as any;
685
+ }
686
+ } else {
687
+ errors.push("context_mode must be an object");
688
+ }
689
+ }
690
+
647
691
  // ─── Parallel Config ────────────────────────────────────────────────────
648
692
  if (preferences.parallel && typeof preferences.parallel === "object") {
649
693
  const p = preferences.parallel as unknown as Record<string, unknown>;
@@ -697,6 +741,41 @@ export function validatePreferences(preferences: GSDPreferences): {
697
741
  }
698
742
  }
699
743
 
744
+ // ─── Slice Parallel Config ───────────────────────────────────────────────
745
+ if (preferences.slice_parallel !== undefined) {
746
+ if (typeof preferences.slice_parallel === "object" && preferences.slice_parallel !== null) {
747
+ const sp = preferences.slice_parallel as Record<string, unknown>;
748
+ const validSp: NonNullable<GSDPreferences["slice_parallel"]> = {};
749
+
750
+ if (sp.enabled !== undefined) {
751
+ if (typeof sp.enabled === "boolean") validSp.enabled = sp.enabled;
752
+ else errors.push("slice_parallel.enabled must be a boolean");
753
+ }
754
+
755
+ if (sp.max_workers !== undefined) {
756
+ const maxWorkers = typeof sp.max_workers === "number" ? sp.max_workers : Number(sp.max_workers);
757
+ if (Number.isFinite(maxWorkers) && maxWorkers >= 1 && maxWorkers <= 8) {
758
+ validSp.max_workers = Math.floor(maxWorkers);
759
+ } else {
760
+ errors.push("slice_parallel.max_workers must be a number between 1 and 8");
761
+ }
762
+ }
763
+
764
+ const knownSliceParallelKeys = new Set(["enabled", "max_workers"]);
765
+ for (const key of Object.keys(sp)) {
766
+ if (!knownSliceParallelKeys.has(key)) {
767
+ warnings.push(`unknown slice_parallel key "${key}" — ignored`);
768
+ }
769
+ }
770
+
771
+ if (Object.keys(validSp).length > 0) {
772
+ validated.slice_parallel = validSp;
773
+ }
774
+ } else {
775
+ errors.push("slice_parallel must be an object");
776
+ }
777
+ }
778
+
700
779
  // ─── Reactive Execution ─────────────────────────────────────────────────
701
780
  if (preferences.reactive_execution !== undefined) {
702
781
  if (typeof preferences.reactive_execution === "object" && preferences.reactive_execution !== null) {