gsd-pi 2.76.0-dev.82e249f7b → 2.76.0-dev.97807402

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 (236) 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/resource-loader.d.ts +1 -1
  5. package/dist/resource-loader.js +2 -8
  6. package/dist/resources/extensions/claude-code-cli/readiness.js +4 -3
  7. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +77 -17
  8. package/dist/resources/extensions/gsd/auto/phases.js +14 -0
  9. package/dist/resources/extensions/gsd/auto/run-unit.js +27 -0
  10. package/dist/resources/extensions/gsd/auto-model-selection.js +1 -1
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  12. package/dist/resources/extensions/gsd/auto-recovery.js +13 -0
  13. package/dist/resources/extensions/gsd/auto-start.js +27 -18
  14. package/dist/resources/extensions/gsd/auto-worktree.js +30 -48
  15. package/dist/resources/extensions/gsd/auto.js +13 -17
  16. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +17 -1
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +39 -9
  18. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +93 -0
  19. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  20. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +40 -4
  21. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +12 -1
  22. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +968 -23
  23. package/dist/resources/extensions/gsd/compaction-snapshot.js +121 -0
  24. package/dist/resources/extensions/gsd/error-classifier.js +10 -3
  25. package/dist/resources/extensions/gsd/exec-history.js +120 -0
  26. package/dist/resources/extensions/gsd/exec-sandbox.js +258 -0
  27. package/dist/resources/extensions/gsd/gsd-db.js +115 -7
  28. package/dist/resources/extensions/gsd/guided-flow.js +189 -0
  29. package/dist/resources/extensions/gsd/health-widget.js +4 -1
  30. package/dist/resources/extensions/gsd/init-wizard.js +15 -1
  31. package/dist/resources/extensions/gsd/key-manager.js +6 -0
  32. package/dist/resources/extensions/gsd/model-router.js +36 -3
  33. package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -9
  34. package/dist/resources/extensions/gsd/preferences-types.js +9 -0
  35. package/dist/resources/extensions/gsd/preferences-validation.js +83 -0
  36. package/dist/resources/extensions/gsd/preferences.js +17 -17
  37. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  38. package/dist/resources/extensions/gsd/prompts/discuss.md +29 -2
  39. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  40. package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -4
  41. package/dist/resources/extensions/gsd/safety/safety-harness.js +4 -0
  42. package/dist/resources/extensions/gsd/token-counter.js +22 -5
  43. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +59 -0
  44. package/dist/resources/extensions/gsd/tools/exec-tool.js +126 -0
  45. package/dist/resources/extensions/gsd/tools/resume-tool.js +23 -0
  46. package/dist/resources/extensions/gsd/workflow-mcp.js +3 -0
  47. package/dist/resources/skills/verify-before-complete/SKILL.md +2 -1
  48. package/dist/resources/skills/write-docs/SKILL.md +2 -1
  49. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  50. package/dist/web/standalone/.next/BUILD_ID +1 -1
  51. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  52. package/dist/web/standalone/.next/build-manifest.json +2 -2
  53. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  54. package/dist/web/standalone/.next/required-server-files.json +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.html +1 -1
  72. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  79. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  81. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  82. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  83. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  84. package/dist/web/standalone/server.js +1 -1
  85. package/package.json +1 -1
  86. package/packages/mcp-server/dist/remote-questions.d.ts +45 -0
  87. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -0
  88. package/packages/mcp-server/dist/remote-questions.js +732 -0
  89. package/packages/mcp-server/dist/remote-questions.js.map +1 -0
  90. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  91. package/packages/mcp-server/dist/server.js +18 -1
  92. package/packages/mcp-server/dist/server.js.map +1 -1
  93. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  94. package/packages/mcp-server/dist/workflow-tools.js +64 -25
  95. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  96. package/packages/mcp-server/package.json +2 -1
  97. package/packages/mcp-server/src/remote-questions.test.ts +294 -0
  98. package/packages/mcp-server/src/remote-questions.ts +916 -0
  99. package/packages/mcp-server/src/server.ts +19 -1
  100. package/packages/mcp-server/src/workflow-tools.test.ts +146 -1
  101. package/packages/mcp-server/src/workflow-tools.ts +84 -43
  102. package/packages/mcp-server/tsconfig.test.json +19 -0
  103. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  104. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/anthropic-shared.js +2 -0
  106. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/simple-options.d.ts +10 -0
  108. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  109. package/packages/pi-ai/dist/providers/simple-options.js +16 -1
  110. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  111. package/packages/pi-ai/src/providers/anthropic-shared.ts +3 -1
  112. package/packages/pi-ai/src/providers/simple-options.ts +17 -1
  113. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  114. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts +2 -0
  115. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.d.ts.map +1 -0
  116. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js +203 -0
  117. package/packages/pi-coding-agent/dist/core/model-registry-custom-caps.test.js.map +1 -0
  118. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/model-registry.js +14 -0
  120. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts +2 -0
  122. package/packages/pi-coding-agent/dist/core/redact-secrets.d.ts.map +1 -0
  123. package/packages/pi-coding-agent/dist/core/redact-secrets.js +49 -0
  124. package/packages/pi-coding-agent/dist/core/redact-secrets.js.map +1 -0
  125. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/redact-secrets.test.d.ts.map +1 -0
  127. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js +67 -0
  128. package/packages/pi-coding-agent/dist/core/redact-secrets.test.js.map +1 -0
  129. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/session-manager.js +9 -5
  131. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/session-manager.test.js +25 -1
  133. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js +5 -4
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/chat-frame.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts +7 -6
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js +29 -21
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +13 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  145. package/packages/pi-coding-agent/src/core/model-registry-custom-caps.test.ts +245 -0
  146. package/packages/pi-coding-agent/src/core/model-registry.ts +16 -0
  147. package/packages/pi-coding-agent/src/core/redact-secrets.test.ts +86 -0
  148. package/packages/pi-coding-agent/src/core/redact-secrets.ts +58 -0
  149. package/packages/pi-coding-agent/src/core/session-manager.test.ts +36 -1
  150. package/packages/pi-coding-agent/src/core/session-manager.ts +9 -5
  151. package/packages/pi-coding-agent/src/modes/interactive/components/chat-frame.ts +6 -6
  152. package/packages/pi-coding-agent/src/modes/interactive/components/skill-invocation-message.ts +36 -22
  153. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +13 -1
  154. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  155. package/src/resources/extensions/claude-code-cli/readiness.ts +4 -3
  156. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +78 -17
  157. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +149 -5
  158. package/src/resources/extensions/gsd/auto/phases.ts +14 -0
  159. package/src/resources/extensions/gsd/auto/run-unit.ts +29 -0
  160. package/src/resources/extensions/gsd/auto-model-selection.ts +1 -1
  161. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -2
  162. package/src/resources/extensions/gsd/auto-recovery.ts +15 -0
  163. package/src/resources/extensions/gsd/auto-start.ts +29 -19
  164. package/src/resources/extensions/gsd/auto-worktree.ts +34 -52
  165. package/src/resources/extensions/gsd/auto.ts +12 -17
  166. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +23 -1
  167. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +40 -9
  168. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +109 -0
  169. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  170. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +42 -4
  171. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +13 -1
  172. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +898 -32
  173. package/src/resources/extensions/gsd/compaction-snapshot.ts +165 -0
  174. package/src/resources/extensions/gsd/error-classifier.ts +10 -3
  175. package/src/resources/extensions/gsd/exec-history.ts +153 -0
  176. package/src/resources/extensions/gsd/exec-sandbox.ts +326 -0
  177. package/src/resources/extensions/gsd/gsd-db.ts +122 -7
  178. package/src/resources/extensions/gsd/guided-flow.ts +221 -0
  179. package/src/resources/extensions/gsd/health-widget.ts +3 -1
  180. package/src/resources/extensions/gsd/init-wizard.ts +15 -1
  181. package/src/resources/extensions/gsd/journal.ts +2 -1
  182. package/src/resources/extensions/gsd/key-manager.ts +6 -0
  183. package/src/resources/extensions/gsd/model-router.ts +42 -1
  184. package/src/resources/extensions/gsd/pre-execution-checks.ts +36 -10
  185. package/src/resources/extensions/gsd/preferences-types.ts +46 -0
  186. package/src/resources/extensions/gsd/preferences-validation.ts +79 -0
  187. package/src/resources/extensions/gsd/preferences.ts +17 -17
  188. package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -0
  189. package/src/resources/extensions/gsd/prompts/discuss.md +29 -2
  190. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +5 -2
  191. package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -3
  192. package/src/resources/extensions/gsd/safety/safety-harness.ts +6 -0
  193. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +116 -0
  194. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +49 -0
  195. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +123 -0
  196. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +2 -2
  197. package/src/resources/extensions/gsd/tests/complete-task.test.ts +2 -2
  198. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +31 -0
  199. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +1 -1
  200. package/src/resources/extensions/gsd/tests/escalation.test.ts +1 -1
  201. package/src/resources/extensions/gsd/tests/exec-history.test.ts +124 -0
  202. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +210 -0
  203. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +58 -0
  204. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +152 -1
  205. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +27 -0
  206. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +1 -1
  207. package/src/resources/extensions/gsd/tests/issue-4540-regressions.test.ts +288 -0
  208. package/src/resources/extensions/gsd/tests/key-manager.test.ts +7 -0
  209. package/src/resources/extensions/gsd/tests/md-importer.test.ts +1 -1
  210. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  211. package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +19 -0
  212. package/src/resources/extensions/gsd/tests/pre-exec-backtick-strip.test.ts +14 -0
  213. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +234 -0
  214. package/src/resources/extensions/gsd/tests/preferences.test.ts +110 -0
  215. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +44 -0
  216. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +48 -0
  217. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +388 -0
  218. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +9 -3
  219. package/src/resources/extensions/gsd/tests/save-gate-result-render.test.ts +95 -0
  220. package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +32 -40
  221. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +56 -0
  222. package/src/resources/extensions/gsd/tests/token-counter.test.ts +105 -1
  223. package/src/resources/extensions/gsd/tests/tool-compatibility.test.ts +107 -0
  224. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +65 -2
  225. package/src/resources/extensions/gsd/tests/write-gate.test.ts +64 -0
  226. package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +3 -1
  227. package/src/resources/extensions/gsd/token-counter.ts +22 -5
  228. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +81 -0
  229. package/src/resources/extensions/gsd/tools/exec-tool.ts +183 -0
  230. package/src/resources/extensions/gsd/tools/resume-tool.ts +40 -0
  231. package/src/resources/extensions/gsd/workflow-logger.ts +2 -1
  232. package/src/resources/extensions/gsd/workflow-mcp.ts +3 -0
  233. package/src/resources/skills/verify-before-complete/SKILL.md +2 -1
  234. package/src/resources/skills/write-docs/SKILL.md +2 -1
  235. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → pI48IF3dgfs0CBrYi2bh_}/_buildManifest.js +0 -0
  236. /package/dist/web/standalone/.next/static/{ecSsu49rxxcpbNmVP4mLD → pI48IF3dgfs0CBrYi2bh_}/_ssgManifest.js +0 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * GSD-2 / guided-flow — regression tests for #4573
3
+ *
4
+ * Covers two recovery paths:
5
+ * - maybeHandleReadyPhraseWithoutFiles: nudge when LLM emits
6
+ * "Milestone M001 ready." without writing CONTEXT.md / ROADMAP.md
7
+ * - maybeHandleEmptyIntentTurn: nudge when LLM narrates intent but
8
+ * emits no tool-use blocks
9
+ */
10
+
11
+ import { describe, test, beforeEach } from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+
17
+ import {
18
+ setPendingAutoStart,
19
+ clearPendingAutoStart,
20
+ maybeHandleReadyPhraseWithoutFiles,
21
+ maybeHandleEmptyIntentTurn,
22
+ resetEmptyTurnCounter,
23
+ } from "../guided-flow.ts";
24
+
25
+ // ─── Test harness ──────────────────────────────────────────────────────────
26
+
27
+ interface MockCapture {
28
+ notifies: Array<{ msg: string; level: string }>;
29
+ messages: Array<{ payload: any; options: any }>;
30
+ }
31
+
32
+ function mkCapture(): MockCapture {
33
+ return { notifies: [], messages: [] };
34
+ }
35
+
36
+ function mkCtx(cap: MockCapture): any {
37
+ return {
38
+ ui: {
39
+ notify: (msg: string, level: string) => {
40
+ cap.notifies.push({ msg, level });
41
+ },
42
+ },
43
+ };
44
+ }
45
+
46
+ function mkPi(cap: MockCapture, opts: { sendThrows?: boolean } = {}): any {
47
+ return {
48
+ sendMessage: (payload: any, options: any) => {
49
+ if (opts.sendThrows) throw new Error("send failed");
50
+ cap.messages.push({ payload, options });
51
+ },
52
+ setActiveTools: () => undefined,
53
+ getActiveTools: () => [],
54
+ };
55
+ }
56
+
57
+ function mkBase(): string {
58
+ const base = mkdtempSync(join(tmpdir(), "gsd-4573-"));
59
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
60
+ return base;
61
+ }
62
+
63
+ function assistantMsg(text: string, opts: { toolUse?: boolean } = {}): any {
64
+ const content: any[] = [];
65
+ if (text) content.push({ type: "text", text });
66
+ if (opts.toolUse) content.push({ type: "tool_use", name: "whatever", input: {} });
67
+ return { role: "assistant", content };
68
+ }
69
+
70
+ // ─── ready-phrase recovery (Layer 2) ───────────────────────────────────────
71
+
72
+ describe("#4573 maybeHandleReadyPhraseWithoutFiles", () => {
73
+ beforeEach(() => {
74
+ clearPendingAutoStart();
75
+ resetEmptyTurnCounter();
76
+ });
77
+
78
+ test("no pending entry → no-op", () => {
79
+ const cap = mkCapture();
80
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
81
+ const handled = maybeHandleReadyPhraseWithoutFiles(event);
82
+ assert.equal(handled, false);
83
+ assert.equal(cap.messages.length, 0);
84
+ });
85
+
86
+ test("pending entry, ready phrase, no files → notify + sendMessage", () => {
87
+ const base = mkBase();
88
+ try {
89
+ const cap = mkCapture();
90
+ setPendingAutoStart(base, {
91
+ basePath: base,
92
+ milestoneId: "M001",
93
+ ctx: mkCtx(cap),
94
+ pi: mkPi(cap),
95
+ });
96
+ const handled = maybeHandleReadyPhraseWithoutFiles({
97
+ messages: [assistantMsg("Milestone M001 ready.")],
98
+ });
99
+ assert.equal(handled, true);
100
+ assert.equal(cap.messages.length, 1);
101
+ assert.equal(cap.messages[0].payload.customType, "gsd-ready-no-files");
102
+ assert.equal(cap.messages[0].options.triggerTurn, true);
103
+ assert.ok(
104
+ cap.notifies.some((n) => /rejected/.test(n.msg)),
105
+ "user notified about rejection",
106
+ );
107
+ } finally {
108
+ clearPendingAutoStart();
109
+ }
110
+ });
111
+
112
+ test("retry cap — after MAX_READY_REJECTS the nudge stops and entry clears", () => {
113
+ const base = mkBase();
114
+ try {
115
+ const cap = mkCapture();
116
+ setPendingAutoStart(base, {
117
+ basePath: base,
118
+ milestoneId: "M001",
119
+ ctx: mkCtx(cap),
120
+ pi: mkPi(cap),
121
+ });
122
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
123
+
124
+ const first = maybeHandleReadyPhraseWithoutFiles(event);
125
+ const second = maybeHandleReadyPhraseWithoutFiles(event);
126
+ const third = maybeHandleReadyPhraseWithoutFiles(event); // > MAX
127
+
128
+ assert.equal(first, true);
129
+ assert.equal(second, true);
130
+ assert.equal(third, true); // still returns true (handled via give-up)
131
+ assert.equal(cap.messages.length, 2, "only 2 nudges sent (MAX_READY_REJECTS=2)");
132
+ assert.ok(
133
+ cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
134
+ "gives up with error notify",
135
+ );
136
+
137
+ // After giving up, a fresh re-entry starts clean
138
+ const fourth = maybeHandleReadyPhraseWithoutFiles(event);
139
+ assert.equal(fourth, false, "pending entry was cleared — nothing to handle");
140
+ } finally {
141
+ clearPendingAutoStart();
142
+ }
143
+ });
144
+
145
+ test("files present → no nudge (happy path already fired)", () => {
146
+ const base = mkBase();
147
+ try {
148
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# ctx");
149
+ const cap = mkCapture();
150
+ setPendingAutoStart(base, {
151
+ basePath: base,
152
+ milestoneId: "M001",
153
+ ctx: mkCtx(cap),
154
+ pi: mkPi(cap),
155
+ });
156
+ const handled = maybeHandleReadyPhraseWithoutFiles({
157
+ messages: [assistantMsg("Milestone M001 ready.")],
158
+ });
159
+ assert.equal(handled, false);
160
+ assert.equal(cap.messages.length, 0);
161
+ } finally {
162
+ clearPendingAutoStart();
163
+ }
164
+ });
165
+
166
+ test("last message lacks ready phrase → no-op", () => {
167
+ const base = mkBase();
168
+ try {
169
+ const cap = mkCapture();
170
+ setPendingAutoStart(base, {
171
+ basePath: base,
172
+ milestoneId: "M001",
173
+ ctx: mkCtx(cap),
174
+ pi: mkPi(cap),
175
+ });
176
+ const handled = maybeHandleReadyPhraseWithoutFiles({
177
+ messages: [assistantMsg("Let me think about the slices first.")],
178
+ });
179
+ assert.equal(handled, false);
180
+ assert.equal(cap.messages.length, 0);
181
+ } finally {
182
+ clearPendingAutoStart();
183
+ }
184
+ });
185
+
186
+ test("fresh entry after give-up resets counter", () => {
187
+ const base = mkBase();
188
+ try {
189
+ const cap = mkCapture();
190
+ // First cycle: exhaust cap
191
+ setPendingAutoStart(base, {
192
+ basePath: base,
193
+ milestoneId: "M001",
194
+ ctx: mkCtx(cap),
195
+ pi: mkPi(cap),
196
+ });
197
+ const event = { messages: [assistantMsg("Milestone M001 ready.")] };
198
+ maybeHandleReadyPhraseWithoutFiles(event);
199
+ maybeHandleReadyPhraseWithoutFiles(event);
200
+ maybeHandleReadyPhraseWithoutFiles(event); // clears entry
201
+
202
+ // New /gsd run — re-seeds entry; counter must be 0 again
203
+ cap.messages.length = 0;
204
+ setPendingAutoStart(base, {
205
+ basePath: base,
206
+ milestoneId: "M001",
207
+ ctx: mkCtx(cap),
208
+ pi: mkPi(cap),
209
+ });
210
+ const handled = maybeHandleReadyPhraseWithoutFiles(event);
211
+ assert.equal(handled, true);
212
+ assert.equal(cap.messages.length, 1, "fresh entry fires nudge again");
213
+ } finally {
214
+ clearPendingAutoStart();
215
+ }
216
+ });
217
+ });
218
+
219
+ // ─── empty-turn recovery (Layer 3) ────────────────────────────────────────
220
+
221
+ describe("#4573 maybeHandleEmptyIntentTurn", () => {
222
+ beforeEach(() => {
223
+ clearPendingAutoStart();
224
+ resetEmptyTurnCounter();
225
+ });
226
+
227
+ test("no pending entry + isAuto false → no-op (interactive discuss is user-driven)", () => {
228
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md now.")] };
229
+ const handled = maybeHandleEmptyIntentTurn(event, false);
230
+ assert.equal(handled, false);
231
+ });
232
+
233
+ test("text-only turn WITHOUT commit phrase → not flagged (legitimate text)", () => {
234
+ const base = mkBase();
235
+ try {
236
+ const cap = mkCapture();
237
+ setPendingAutoStart(base, {
238
+ basePath: base,
239
+ milestoneId: "M001",
240
+ ctx: mkCtx(cap),
241
+ pi: mkPi(cap),
242
+ });
243
+ const handled = maybeHandleEmptyIntentTurn(
244
+ { messages: [assistantMsg("Here is the roadmap preview — three slices.")] },
245
+ false,
246
+ );
247
+ assert.equal(handled, false);
248
+ assert.equal(cap.messages.length, 0);
249
+ } finally {
250
+ clearPendingAutoStart();
251
+ }
252
+ });
253
+
254
+ test("text-only turn ending in question → treated as user-handoff, not flagged", () => {
255
+ const base = mkBase();
256
+ try {
257
+ const cap = mkCapture();
258
+ setPendingAutoStart(base, {
259
+ basePath: base,
260
+ milestoneId: "M001",
261
+ ctx: mkCtx(cap),
262
+ pi: mkPi(cap),
263
+ });
264
+ const handled = maybeHandleEmptyIntentTurn(
265
+ { messages: [assistantMsg("Ready to write, or want to adjust?")] },
266
+ false,
267
+ );
268
+ assert.equal(handled, false);
269
+ } finally {
270
+ clearPendingAutoStart();
271
+ }
272
+ });
273
+
274
+ test("commit-intent phrase WITHOUT tool call → nudge fires", () => {
275
+ const base = mkBase();
276
+ try {
277
+ const cap = mkCapture();
278
+ setPendingAutoStart(base, {
279
+ basePath: base,
280
+ milestoneId: "M001",
281
+ ctx: mkCtx(cap),
282
+ pi: mkPi(cap),
283
+ });
284
+ const handled = maybeHandleEmptyIntentTurn(
285
+ { messages: [assistantMsg("I'll now write the CONTEXT.md file.")] },
286
+ false,
287
+ );
288
+ assert.equal(handled, true);
289
+ assert.equal(cap.messages.length, 1);
290
+ assert.equal(cap.messages[0].payload.customType, "gsd-empty-turn-recovery");
291
+ } finally {
292
+ clearPendingAutoStart();
293
+ }
294
+ });
295
+
296
+ test("commit-intent WITH tool-use block → not flagged", () => {
297
+ const base = mkBase();
298
+ try {
299
+ const cap = mkCapture();
300
+ setPendingAutoStart(base, {
301
+ basePath: base,
302
+ milestoneId: "M001",
303
+ ctx: mkCtx(cap),
304
+ pi: mkPi(cap),
305
+ });
306
+ const handled = maybeHandleEmptyIntentTurn(
307
+ { messages: [assistantMsg("I'll write the file now.", { toolUse: true })] },
308
+ false,
309
+ );
310
+ assert.equal(handled, false);
311
+ assert.equal(cap.messages.length, 0);
312
+ } finally {
313
+ clearPendingAutoStart();
314
+ }
315
+ });
316
+
317
+ test("ready phrase is NOT treated as empty-turn (handled by other recovery path)", () => {
318
+ const base = mkBase();
319
+ try {
320
+ const cap = mkCapture();
321
+ setPendingAutoStart(base, {
322
+ basePath: base,
323
+ milestoneId: "M001",
324
+ ctx: mkCtx(cap),
325
+ pi: mkPi(cap),
326
+ });
327
+ const handled = maybeHandleEmptyIntentTurn(
328
+ { messages: [assistantMsg("Milestone M001 ready.")] },
329
+ false,
330
+ );
331
+ assert.equal(handled, false);
332
+ } finally {
333
+ clearPendingAutoStart();
334
+ }
335
+ });
336
+
337
+ test("empty-turn retry cap — stops after MAX_EMPTY_TURN_RETRIES", () => {
338
+ const base = mkBase();
339
+ try {
340
+ const cap = mkCapture();
341
+ setPendingAutoStart(base, {
342
+ basePath: base,
343
+ milestoneId: "M001",
344
+ ctx: mkCtx(cap),
345
+ pi: mkPi(cap),
346
+ });
347
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
348
+
349
+ maybeHandleEmptyIntentTurn(event, false); // 1
350
+ maybeHandleEmptyIntentTurn(event, false); // 2
351
+ const third = maybeHandleEmptyIntentTurn(event, false); // > cap
352
+
353
+ assert.equal(cap.messages.length, 2, "only 2 nudges sent");
354
+ assert.equal(third, false, "after cap, no further injection");
355
+ assert.ok(
356
+ cap.notifies.some((n) => /Stopping auto-nudge/.test(n.msg)),
357
+ "user notified of give-up",
358
+ );
359
+ } finally {
360
+ clearPendingAutoStart();
361
+ }
362
+ });
363
+
364
+ test("resetEmptyTurnCounter clears state after a successful tool-use turn", () => {
365
+ const base = mkBase();
366
+ try {
367
+ const cap = mkCapture();
368
+ setPendingAutoStart(base, {
369
+ basePath: base,
370
+ milestoneId: "M001",
371
+ ctx: mkCtx(cap),
372
+ pi: mkPi(cap),
373
+ });
374
+ const event = { messages: [assistantMsg("I'll write the CONTEXT.md file.")] };
375
+
376
+ maybeHandleEmptyIntentTurn(event, false); // 1
377
+ maybeHandleEmptyIntentTurn(event, false); // 2 — at cap
378
+ resetEmptyTurnCounter(); // simulate a successful tool-use turn in between
379
+
380
+ cap.messages.length = 0;
381
+ const after = maybeHandleEmptyIntentTurn(event, false);
382
+ assert.equal(after, true, "counter reset — nudge fires again");
383
+ assert.equal(cap.messages.length, 1);
384
+ } finally {
385
+ clearPendingAutoStart();
386
+ }
387
+ });
388
+ });
@@ -45,9 +45,15 @@ describe('restore tools after discuss flow scoping (#3628)', () => {
45
45
  })
46
46
 
47
47
  it('savedTools is restored after sendMessage', () => {
48
- // Find the sendMessage call
49
- const sendMsg = src.indexOf('triggerTurn: true')
50
- assert.ok(sendMsg !== -1, 'sendMessage with triggerTurn must exist')
48
+ // #4573: guided-flow.ts now contains multiple `triggerTurn: true` calls
49
+ // (ready-phrase and empty-turn recovery paths). The discuss-flow scoping
50
+ // sendMessage is the one that follows `savedTools = currentTools`, so
51
+ // anchor the search there rather than at the first `triggerTurn: true`.
52
+ const savedToolsAssign = src.indexOf('savedTools = currentTools')
53
+ assert.ok(savedToolsAssign !== -1, 'savedTools = currentTools must exist')
54
+
55
+ const sendMsg = src.indexOf('triggerTurn: true', savedToolsAssign)
56
+ assert.ok(sendMsg !== -1, 'discuss-flow sendMessage with triggerTurn must exist after savedTools capture')
51
57
 
52
58
  // After sendMessage, savedTools should be restored via setActiveTools
53
59
  const afterSend = src.slice(sendMsg, sendMsg + 500)
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Regression test suite for save_gate_result renderResult.
3
+ *
4
+ * Verifies that renderResult does not print "undefined: undefined" when
5
+ * `details` is empty, and that the error fallback does not produce a
6
+ * duplicated `Error: Error:` prefix when `content[0].text` already starts
7
+ * with `Error:`.
8
+ */
9
+
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { registerDbTools } from '../bootstrap/db-tools.ts';
13
+
14
+ function makeMockPi() {
15
+ const tools: any[] = [];
16
+ return {
17
+ registerTool: (tool: any) => tools.push(tool),
18
+ tools,
19
+ } as any;
20
+ }
21
+
22
+ const fakeTheme = {
23
+ fg: (_color: string, text: string) => text,
24
+ bold: (text: string) => text,
25
+ };
26
+
27
+ function getSaveGateResultTool() {
28
+ const pi = makeMockPi();
29
+ registerDbTools(pi);
30
+ const tool = pi.tools.find((t: any) => t.name === 'gsd_save_gate_result');
31
+ assert.ok(tool, 'gsd_save_gate_result should be registered');
32
+ return tool;
33
+ }
34
+
35
+ test('save_gate_result renderResult falls back to content text when details is empty', () => {
36
+ const tool = getSaveGateResultTool();
37
+ const result = {
38
+ content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=pass' }],
39
+ details: {},
40
+ isError: false,
41
+ };
42
+ const rendered = tool.renderResult(result, {}, fakeTheme);
43
+ const text = String(rendered.content ?? rendered.text ?? rendered);
44
+ assert.ok(!text.includes('undefined'), `got: ${text}`);
45
+ assert.ok(
46
+ text.includes('Gate Q3') || text.includes('verdict=pass'),
47
+ `expected content summary — got: ${text}`,
48
+ );
49
+ });
50
+
51
+ test('save_gate_result renderResult uses structured details when present', () => {
52
+ const tool = getSaveGateResultTool();
53
+ const result = {
54
+ content: [{ type: 'text', text: 'Gate Q3 result saved: verdict=flag' }],
55
+ details: { operation: 'save_gate_result', gateId: 'Q3', verdict: 'flag' },
56
+ isError: false,
57
+ };
58
+ const rendered = tool.renderResult(result, {}, fakeTheme);
59
+ const text = String(rendered.content ?? rendered.text ?? rendered);
60
+ assert.ok(text.includes('Q3'), `got: ${text}`);
61
+ assert.ok(text.includes('flag'), `got: ${text}`);
62
+ assert.ok(!text.includes('undefined'), `got: ${text}`);
63
+ });
64
+
65
+ test('save_gate_result renderResult shows error from content when details.error is missing', () => {
66
+ const tool = getSaveGateResultTool();
67
+ const result = {
68
+ content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
69
+ details: {},
70
+ isError: true,
71
+ };
72
+ const rendered = tool.renderResult(result, {}, fakeTheme);
73
+ const text = String(rendered.content ?? rendered.text ?? rendered);
74
+ assert.ok(
75
+ text.includes('Invalid gateId') || text.includes('Error'),
76
+ `got: ${text}`,
77
+ );
78
+ assert.ok(!text.includes('undefined'), `got: ${text}`);
79
+ });
80
+
81
+ test('save_gate_result renderResult does not duplicate Error: prefix', () => {
82
+ const tool = getSaveGateResultTool();
83
+ const result = {
84
+ content: [{ type: 'text', text: 'Error: Invalid gateId "Z1"' }],
85
+ details: {},
86
+ isError: true,
87
+ };
88
+ const rendered = tool.renderResult(result, {}, fakeTheme);
89
+ const text = String(rendered.content ?? rendered.text ?? rendered);
90
+ assert.ok(
91
+ !/Error:\s*Error:/i.test(text),
92
+ `expected a single Error: prefix — got: ${text}`,
93
+ );
94
+ assert.ok(text.includes('Invalid gateId'), `got: ${text}`);
95
+ });
@@ -1,19 +1,15 @@
1
1
  /**
2
2
  * session-start-footer.test.ts
3
3
  *
4
- * Verifies that register-hooks.ts suppresses the built-in footer by calling
5
- * ctx.ui.setFooter(hideFooter) in both session_start and session_switch when
6
- * isAutoActive() is true.
4
+ * Verifies that register-hooks.ts suppresses the gsd-health widget (not the
5
+ * built-in footer) when isAutoActive() is true, and that setFooter is never
6
+ * called by the extension in either session_start or session_switch.
7
7
  *
8
8
  * Testing strategy:
9
- * Two layers:
10
- * 1. Source-code regression guard: ensures the guard and setFooter call are
11
- * structurally present in register-hooks.ts for both event handlers.
12
- * (node:test does not support mock.module without --experimental-test-module-mocks,
13
- * so structural analysis is the correct approach here.)
9
+ * 1. Source-code regression guards: structural checks on register-hooks.ts.
14
10
  * 2. Behavioral integration test: fires the live session_start handler with a
15
- * fake ctx when isAutoActive() is false (its default at test time) and
16
- * confirms setFooter is NOT called — verifying the guard is conditional.
11
+ * fake ctx when isAutoActive() is false (default) and confirms neither
12
+ * setFooter nor setWidget("gsd-health") is called.
17
13
  *
18
14
  * Relates to #4314.
19
15
  */
@@ -35,16 +31,14 @@ const HOOKS_SOURCE = readFileSync(
35
31
 
36
32
  // ─── Source-code regression guards ──────────────────────────────────────────
37
33
 
38
- test("register-hooks.ts imports hideFooter from auto-dashboard", () => {
34
+ test("register-hooks.ts does NOT import hideFooter", () => {
39
35
  assert.ok(
40
- HOOKS_SOURCE.includes('import { hideFooter } from "../auto-dashboard.js"') ||
41
- HOOKS_SOURCE.includes("import { hideFooter } from '../auto-dashboard.js'"),
42
- "register-hooks.ts must import hideFooter from auto-dashboard.js",
36
+ !HOOKS_SOURCE.includes("hideFooter"),
37
+ "register-hooks.ts must not reference hideFooter footer is no longer swapped in auto mode",
43
38
  );
44
39
  });
45
40
 
46
- test("session_start handler calls ctx.ui.setFooter(hideFooter) when isAutoActive()", () => {
47
- // Locate the session_start handler body (up to the next pi.on call)
41
+ test("session_start handler guards initHealthWidget with !isAutoActive()", () => {
48
42
  const sessionStartIdx = HOOKS_SOURCE.indexOf('"session_start"');
49
43
  assert.ok(sessionStartIdx > -1, "session_start handler must exist");
50
44
 
@@ -58,20 +52,23 @@ test("session_start handler calls ctx.ui.setFooter(hideFooter) when isAutoActive
58
52
  "session_start handler must call isAutoActive()",
59
53
  );
60
54
  assert.ok(
61
- sessionStartBody.includes("ctx.ui.setFooter(hideFooter)"),
62
- "session_start handler must call ctx.ui.setFooter(hideFooter)",
55
+ sessionStartBody.includes("initHealthWidget"),
56
+ "session_start handler must reference initHealthWidget",
57
+ );
58
+ assert.ok(
59
+ !sessionStartBody.includes("setFooter"),
60
+ "session_start handler must NOT call setFooter",
63
61
  );
64
62
 
65
- // Guard must wrap the setFooter call
66
63
  const guardIdx = sessionStartBody.indexOf("isAutoActive()");
67
- const setFooterIdx = sessionStartBody.indexOf("ctx.ui.setFooter(hideFooter)");
64
+ const healthIdx = sessionStartBody.indexOf("initHealthWidget");
68
65
  assert.ok(
69
- guardIdx < setFooterIdx,
70
- "isAutoActive() guard must appear before ctx.ui.setFooter(hideFooter) in session_start",
66
+ guardIdx < healthIdx,
67
+ "isAutoActive() guard must appear before initHealthWidget in session_start",
71
68
  );
72
69
  });
73
70
 
74
- test("session_switch handler calls ctx.ui.setFooter(hideFooter) when isAutoActive()", () => {
71
+ test("session_switch handler suppresses gsd-health when isAutoActive()", () => {
75
72
  const sessionSwitchIdx = HOOKS_SOURCE.indexOf('"session_switch"');
76
73
  assert.ok(sessionSwitchIdx > -1, "session_switch handler must exist");
77
74
 
@@ -85,21 +82,18 @@ test("session_switch handler calls ctx.ui.setFooter(hideFooter) when isAutoActiv
85
82
  "session_switch handler must call isAutoActive()",
86
83
  );
87
84
  assert.ok(
88
- sessionSwitchBody.includes("ctx.ui.setFooter(hideFooter)"),
89
- "session_switch handler must call ctx.ui.setFooter(hideFooter)",
85
+ sessionSwitchBody.includes('setWidget("gsd-health", undefined)'),
86
+ "session_switch handler must call setWidget(\"gsd-health\", undefined) when auto is active",
90
87
  );
91
-
92
- const guardIdx = sessionSwitchBody.indexOf("isAutoActive()");
93
- const setFooterIdx = sessionSwitchBody.indexOf("ctx.ui.setFooter(hideFooter)");
94
88
  assert.ok(
95
- guardIdx < setFooterIdx,
96
- "isAutoActive() guard must appear before ctx.ui.setFooter(hideFooter) in session_switch",
89
+ !sessionSwitchBody.includes("setFooter"),
90
+ "session_switch handler must NOT call setFooter",
97
91
  );
98
92
  });
99
93
 
100
- // ─── Behavioral test: setFooter NOT called when auto-mode is inactive ────────
94
+ // ─── Behavioral test: neither setFooter nor health suppression when auto inactive
101
95
 
102
- test("session_start does NOT call setFooter when isAutoActive() is false (default)", async (t) => {
96
+ test("session_start does NOT call setFooter or suppress gsd-health when isAutoActive() is false", async (t) => {
103
97
  const dir = join(
104
98
  tmpdir(),
105
99
  `gsd-footer-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -114,6 +108,7 @@ test("session_start does NOT call setFooter when isAutoActive() is false (defaul
114
108
  });
115
109
 
116
110
  let setFooterCallCount = 0;
111
+ let healthWidgetHideCount = 0;
117
112
 
118
113
  const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
119
114
  const pi = {
@@ -137,17 +132,14 @@ test("session_start does NOT call setFooter when isAutoActive() is false (defaul
137
132
  },
138
133
  setWorkingMessage: () => {},
139
134
  onTerminalInput: () => () => {},
140
- setWidget: () => {},
135
+ setWidget: (key: string, value: unknown) => {
136
+ if (key === "gsd-health" && value === undefined) healthWidgetHideCount++;
137
+ },
141
138
  },
142
139
  sessionManager: { getSessionId: () => null },
143
140
  model: null,
144
141
  } as any);
145
142
 
146
- // isAutoActive() is false at test time (no auto session started),
147
- // so setFooter must not be called.
148
- assert.equal(
149
- setFooterCallCount,
150
- 0,
151
- "setFooter must NOT be called when isAutoActive() returns false",
152
- );
143
+ assert.equal(setFooterCallCount, 0, "setFooter must NOT be called when isAutoActive() is false");
144
+ assert.equal(healthWidgetHideCount, 0, "gsd-health must NOT be hidden when isAutoActive() is false");
153
145
  });