gsd-pi 2.22.0 → 2.24.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 (228) hide show
  1. package/README.md +25 -1
  2. package/dist/cli.js +74 -7
  3. package/dist/headless.d.ts +25 -0
  4. package/dist/headless.js +454 -0
  5. package/dist/help-text.js +47 -0
  6. package/dist/mcp-server.d.ts +20 -3
  7. package/dist/mcp-server.js +21 -1
  8. package/dist/models-resolver.d.ts +32 -0
  9. package/dist/models-resolver.js +50 -0
  10. package/dist/resource-loader.js +64 -9
  11. package/dist/resources/extensions/bg-shell/output-formatter.ts +36 -16
  12. package/dist/resources/extensions/bg-shell/process-manager.ts +6 -4
  13. package/dist/resources/extensions/bg-shell/types.ts +33 -1
  14. package/dist/resources/extensions/browser-tools/capture.ts +18 -16
  15. package/dist/resources/extensions/browser-tools/index.ts +20 -0
  16. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  17. package/dist/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  18. package/dist/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  19. package/dist/resources/extensions/browser-tools/tools/device.ts +183 -0
  20. package/dist/resources/extensions/browser-tools/tools/extract.ts +229 -0
  21. package/dist/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  22. package/dist/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  23. package/dist/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  24. package/dist/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  25. package/dist/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  26. package/dist/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  27. package/dist/resources/extensions/gsd/auto-dashboard.ts +2 -0
  28. package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
  29. package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
  30. package/dist/resources/extensions/gsd/auto-recovery.ts +51 -2
  31. package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
  32. package/dist/resources/extensions/gsd/auto.ts +560 -52
  33. package/dist/resources/extensions/gsd/captures.ts +49 -0
  34. package/dist/resources/extensions/gsd/commands.ts +194 -11
  35. package/dist/resources/extensions/gsd/complexity.ts +1 -0
  36. package/dist/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  37. package/dist/resources/extensions/gsd/diff-context.ts +73 -80
  38. package/dist/resources/extensions/gsd/doctor.ts +76 -12
  39. package/dist/resources/extensions/gsd/exit-command.ts +2 -2
  40. package/dist/resources/extensions/gsd/forensics.ts +95 -52
  41. package/dist/resources/extensions/gsd/gitignore.ts +1 -0
  42. package/dist/resources/extensions/gsd/guided-flow.ts +85 -5
  43. package/dist/resources/extensions/gsd/index.ts +34 -1
  44. package/dist/resources/extensions/gsd/mcp-server.ts +33 -12
  45. package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  46. package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
  47. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  48. package/dist/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  49. package/dist/resources/extensions/gsd/preferences.ts +65 -1
  50. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  51. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -0
  52. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  53. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  54. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  55. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  56. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  57. package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
  58. package/dist/resources/extensions/gsd/roadmap-slices.ts +41 -1
  59. package/dist/resources/extensions/gsd/session-forensics.ts +36 -2
  60. package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
  61. package/dist/resources/extensions/gsd/state.ts +72 -30
  62. package/dist/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  63. package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  64. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  65. package/dist/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  66. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  67. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  68. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  69. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  70. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  71. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  72. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  73. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  74. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  75. package/dist/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  76. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  77. package/dist/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  78. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  79. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  80. package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  81. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  82. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  83. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  84. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  85. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  86. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  87. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  88. package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  89. package/dist/resources/extensions/gsd/triage-resolution.ts +83 -0
  90. package/dist/resources/extensions/gsd/types.ts +15 -1
  91. package/dist/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  92. package/dist/resources/extensions/gsd/workspace-index.ts +34 -6
  93. package/dist/resources/extensions/subagent/index.ts +5 -0
  94. package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
  95. package/dist/update-check.d.ts +9 -0
  96. package/dist/update-check.js +97 -0
  97. package/package.json +6 -1
  98. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/anthropic.js +16 -7
  100. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  101. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
  103. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
  106. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
  109. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  110. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  111. package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
  112. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  113. package/packages/pi-ai/src/providers/anthropic.ts +21 -8
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
  115. package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
  116. package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
  117. package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
  118. package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
  119. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts +10 -0
  123. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js +79 -0
  125. package/packages/pi-coding-agent/dist/core/tools/bash-background.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +18 -0
  127. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/bash.js +77 -1
  129. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +1 -1
  131. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/tools/index.js +1 -1
  133. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  135. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/index.js +1 -1
  137. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  138. package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
  139. package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
  140. package/packages/pi-coding-agent/src/core/tools/bash-background.test.ts +91 -0
  141. package/packages/pi-coding-agent/src/core/tools/bash.ts +83 -1
  142. package/packages/pi-coding-agent/src/core/tools/index.ts +1 -0
  143. package/packages/pi-coding-agent/src/index.ts +1 -0
  144. package/scripts/postinstall.js +7 -109
  145. package/src/resources/extensions/bg-shell/output-formatter.ts +36 -16
  146. package/src/resources/extensions/bg-shell/process-manager.ts +6 -4
  147. package/src/resources/extensions/bg-shell/types.ts +33 -1
  148. package/src/resources/extensions/browser-tools/capture.ts +18 -16
  149. package/src/resources/extensions/browser-tools/index.ts +20 -0
  150. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +25 -0
  151. package/src/resources/extensions/browser-tools/tools/action-cache.ts +216 -0
  152. package/src/resources/extensions/browser-tools/tools/codegen.ts +274 -0
  153. package/src/resources/extensions/browser-tools/tools/device.ts +183 -0
  154. package/src/resources/extensions/browser-tools/tools/extract.ts +229 -0
  155. package/src/resources/extensions/browser-tools/tools/injection-detect.ts +221 -0
  156. package/src/resources/extensions/browser-tools/tools/network-mock.ts +244 -0
  157. package/src/resources/extensions/browser-tools/tools/pdf.ts +92 -0
  158. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +202 -0
  159. package/src/resources/extensions/browser-tools/tools/visual-diff.ts +209 -0
  160. package/src/resources/extensions/browser-tools/tools/zoom.ts +104 -0
  161. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
  163. package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
  164. package/src/resources/extensions/gsd/auto-recovery.ts +51 -2
  165. package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
  166. package/src/resources/extensions/gsd/auto.ts +560 -52
  167. package/src/resources/extensions/gsd/captures.ts +49 -0
  168. package/src/resources/extensions/gsd/commands.ts +194 -11
  169. package/src/resources/extensions/gsd/complexity.ts +1 -0
  170. package/src/resources/extensions/gsd/dashboard-overlay.ts +54 -2
  171. package/src/resources/extensions/gsd/diff-context.ts +73 -80
  172. package/src/resources/extensions/gsd/doctor.ts +76 -12
  173. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  174. package/src/resources/extensions/gsd/forensics.ts +95 -52
  175. package/src/resources/extensions/gsd/gitignore.ts +1 -0
  176. package/src/resources/extensions/gsd/guided-flow.ts +85 -5
  177. package/src/resources/extensions/gsd/index.ts +34 -1
  178. package/src/resources/extensions/gsd/mcp-server.ts +33 -12
  179. package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
  180. package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
  181. package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
  182. package/src/resources/extensions/gsd/post-unit-hooks.ts +2 -1
  183. package/src/resources/extensions/gsd/preferences.ts +65 -1
  184. package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
  185. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -0
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +104 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -0
  188. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  189. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  190. package/src/resources/extensions/gsd/prompts/validate-milestone.md +70 -0
  191. package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
  192. package/src/resources/extensions/gsd/roadmap-slices.ts +41 -1
  193. package/src/resources/extensions/gsd/session-forensics.ts +36 -2
  194. package/src/resources/extensions/gsd/session-status-io.ts +197 -0
  195. package/src/resources/extensions/gsd/state.ts +72 -30
  196. package/src/resources/extensions/gsd/templates/milestone-validation.md +62 -0
  197. package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
  198. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
  199. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +186 -0
  200. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
  201. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +264 -0
  202. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +123 -0
  203. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
  204. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
  205. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
  206. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
  207. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
  208. package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
  209. package/src/resources/extensions/gsd/tests/doctor.test.ts +58 -0
  210. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +17 -6
  211. package/src/resources/extensions/gsd/tests/integration/headless-command.ts +534 -0
  212. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
  213. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
  214. package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
  215. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
  216. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
  217. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +43 -1
  218. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +120 -0
  219. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +203 -2
  220. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
  221. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +8 -3
  222. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
  223. package/src/resources/extensions/gsd/triage-resolution.ts +83 -0
  224. package/src/resources/extensions/gsd/types.ts +15 -1
  225. package/src/resources/extensions/gsd/visualizer-overlay.ts +8 -1
  226. package/src/resources/extensions/gsd/workspace-index.ts +34 -6
  227. package/src/resources/extensions/subagent/index.ts +5 -0
  228. package/src/resources/extensions/subagent/worker-registry.ts +99 -0
@@ -7,10 +7,10 @@ import assert from "node:assert/strict";
7
7
  import { mkdirSync, readFileSync, writeFileSync, rmSync, existsSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import { tmpdir } from "node:os";
10
- import { appendCapture, markCaptureResolved, loadAllCaptures } from "../captures.ts";
10
+ import { appendCapture, markCaptureResolved, markCaptureExecuted, loadAllCaptures, loadActionableCaptures } from "../captures.ts";
11
11
  // Import only the functions that don't depend on @gsd/pi-coding-agent
12
12
  // (triage-ui.ts imports next-action-ui.ts which imports the unavailable package)
13
- import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt } from "../triage-resolution.ts";
13
+ import { executeInject, executeReplan, detectFileOverlap, loadDeferredCaptures, loadReplanCaptures, buildQuickTaskPrompt, executeTriageResolutions } from "../triage-resolution.ts";
14
14
 
15
15
  function makeTempDir(prefix: string): string {
16
16
  const dir = join(
@@ -213,3 +213,204 @@ test("resolution: buildQuickTaskPrompt includes capture text and ID", () => {
213
213
  assert.ok(prompt.includes("Quick Task"), "should have Quick Task header");
214
214
  assert.ok(prompt.includes("Do NOT modify"), "should warn about plan files");
215
215
  });
216
+
217
+ // ─── markCaptureExecuted ─────────────────────────────────────────────────────
218
+
219
+ test("resolution: markCaptureExecuted adds Executed field to capture", () => {
220
+ const tmp = makeTempDir("res-executed");
221
+ try {
222
+ const id = appendCapture(tmp, "fix the button");
223
+ markCaptureResolved(tmp, id, "quick-task", "execute as quick-task", "small fix");
224
+
225
+ markCaptureExecuted(tmp, id);
226
+
227
+ const all = loadAllCaptures(tmp);
228
+ assert.strictEqual(all.length, 1);
229
+ assert.strictEqual(all[0].executed, true, "should be marked as executed");
230
+ } finally {
231
+ rmSync(tmp, { recursive: true, force: true });
232
+ }
233
+ });
234
+
235
+ test("resolution: markCaptureExecuted is idempotent", () => {
236
+ const tmp = makeTempDir("res-executed-idem");
237
+ try {
238
+ const id = appendCapture(tmp, "fix something");
239
+ markCaptureResolved(tmp, id, "inject", "inject task", "needed");
240
+
241
+ markCaptureExecuted(tmp, id);
242
+ markCaptureExecuted(tmp, id); // call again — should not duplicate
243
+
244
+ const filePath = join(tmp, ".gsd", "CAPTURES.md");
245
+ const content = readFileSync(filePath, "utf-8");
246
+ const executedMatches = content.match(/\*\*Executed:\*\*/g);
247
+ assert.strictEqual(executedMatches?.length, 1, "should have exactly one Executed field");
248
+ } finally {
249
+ rmSync(tmp, { recursive: true, force: true });
250
+ }
251
+ });
252
+
253
+ // ─── loadActionableCaptures ──────────────────────────────────────────────────
254
+
255
+ test("resolution: loadActionableCaptures returns only unexecuted actionable captures", () => {
256
+ const tmp = makeTempDir("res-actionable");
257
+ try {
258
+ const id1 = appendCapture(tmp, "inject this task");
259
+ const id2 = appendCapture(tmp, "quick fix");
260
+ const id3 = appendCapture(tmp, "just a note");
261
+ const id4 = appendCapture(tmp, "replan needed");
262
+ const id5 = appendCapture(tmp, "already executed inject");
263
+
264
+ markCaptureResolved(tmp, id1, "inject", "add task", "needed");
265
+ markCaptureResolved(tmp, id2, "quick-task", "quick fix", "small");
266
+ markCaptureResolved(tmp, id3, "note", "acknowledged", "info");
267
+ markCaptureResolved(tmp, id4, "replan", "replan triggered", "approach changed");
268
+ markCaptureResolved(tmp, id5, "inject", "add task", "needed");
269
+ markCaptureExecuted(tmp, id5); // mark as executed
270
+
271
+ const actionable = loadActionableCaptures(tmp);
272
+ assert.strictEqual(actionable.length, 3, "should have 3 actionable captures");
273
+ assert.deepStrictEqual(
274
+ actionable.map(c => c.id),
275
+ [id1, id2, id4],
276
+ "should include inject, quick-task, replan but not note or executed inject",
277
+ );
278
+ } finally {
279
+ rmSync(tmp, { recursive: true, force: true });
280
+ }
281
+ });
282
+
283
+ // ─── executeTriageResolutions ────────────────────────────────────────────────
284
+
285
+ test("resolution: executeTriageResolutions executes inject captures", () => {
286
+ const tmp = makeTempDir("res-exec-inject");
287
+ try {
288
+ setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
289
+ const id1 = appendCapture(tmp, "add error handling");
290
+ const id2 = appendCapture(tmp, "add retry logic");
291
+ markCaptureResolved(tmp, id1, "inject", "add task", "needed");
292
+ markCaptureResolved(tmp, id2, "inject", "add task", "also needed");
293
+
294
+ const result = executeTriageResolutions(tmp, "M001", "S01");
295
+
296
+ assert.strictEqual(result.injected, 2, "should inject 2 tasks");
297
+ assert.strictEqual(result.replanned, 0);
298
+ assert.strictEqual(result.quickTasks.length, 0);
299
+
300
+ // Verify tasks were added to plan
301
+ const planPath = join(tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
302
+ const planContent = readFileSync(planPath, "utf-8");
303
+ assert.ok(planContent.includes("**T04:"), "should have T04");
304
+ assert.ok(planContent.includes("**T05:"), "should have T05");
305
+
306
+ // Verify captures marked as executed
307
+ const all = loadAllCaptures(tmp);
308
+ assert.strictEqual(all[0].executed, true, "first capture should be executed");
309
+ assert.strictEqual(all[1].executed, true, "second capture should be executed");
310
+ } finally {
311
+ rmSync(tmp, { recursive: true, force: true });
312
+ }
313
+ });
314
+
315
+ test("resolution: executeTriageResolutions executes replan captures", () => {
316
+ const tmp = makeTempDir("res-exec-replan");
317
+ try {
318
+ setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
319
+ const id = appendCapture(tmp, "approach is wrong");
320
+ markCaptureResolved(tmp, id, "replan", "replan triggered", "wrong approach");
321
+
322
+ const result = executeTriageResolutions(tmp, "M001", "S01");
323
+
324
+ assert.strictEqual(result.injected, 0);
325
+ assert.strictEqual(result.replanned, 1, "should trigger 1 replan");
326
+ assert.strictEqual(result.quickTasks.length, 0);
327
+
328
+ // Verify trigger file was written
329
+ const triggerPath = join(
330
+ tmp, ".gsd", "milestones", "M001", "slices", "S01", "S01-REPLAN-TRIGGER.md",
331
+ );
332
+ assert.ok(existsSync(triggerPath), "replan trigger should exist");
333
+
334
+ // Verify capture marked as executed
335
+ const all = loadAllCaptures(tmp);
336
+ assert.strictEqual(all[0].executed, true, "capture should be executed");
337
+ } finally {
338
+ rmSync(tmp, { recursive: true, force: true });
339
+ }
340
+ });
341
+
342
+ test("resolution: executeTriageResolutions queues quick-tasks without executing inline", () => {
343
+ const tmp = makeTempDir("res-exec-qt");
344
+ try {
345
+ const id = appendCapture(tmp, "fix typo in readme");
346
+ markCaptureResolved(tmp, id, "quick-task", "execute as quick-task", "small fix");
347
+
348
+ const result = executeTriageResolutions(tmp, "M001", "S01");
349
+
350
+ assert.strictEqual(result.injected, 0);
351
+ assert.strictEqual(result.replanned, 0);
352
+ assert.strictEqual(result.quickTasks.length, 1, "should queue 1 quick-task");
353
+ assert.strictEqual(result.quickTasks[0].id, id);
354
+
355
+ // Quick-tasks should NOT be marked as executed yet (caller marks after dispatch)
356
+ const all = loadAllCaptures(tmp);
357
+ assert.ok(!all[0].executed, "quick-task should not be executed yet");
358
+ } finally {
359
+ rmSync(tmp, { recursive: true, force: true });
360
+ }
361
+ });
362
+
363
+ test("resolution: executeTriageResolutions handles mixed classifications", () => {
364
+ const tmp = makeTempDir("res-exec-mixed");
365
+ try {
366
+ setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
367
+ const id1 = appendCapture(tmp, "inject a task");
368
+ const id2 = appendCapture(tmp, "quick fix typo");
369
+ const id3 = appendCapture(tmp, "just a note");
370
+ const id4 = appendCapture(tmp, "defer to later");
371
+
372
+ markCaptureResolved(tmp, id1, "inject", "add task", "needed");
373
+ markCaptureResolved(tmp, id2, "quick-task", "quick fix", "small");
374
+ markCaptureResolved(tmp, id3, "note", "acknowledged", "info");
375
+ markCaptureResolved(tmp, id4, "defer", "deferred", "later");
376
+
377
+ const result = executeTriageResolutions(tmp, "M001", "S01");
378
+
379
+ assert.strictEqual(result.injected, 1, "should inject 1 task");
380
+ assert.strictEqual(result.replanned, 0);
381
+ assert.strictEqual(result.quickTasks.length, 1, "should queue 1 quick-task");
382
+ assert.strictEqual(result.actions.length, 2, "should have 2 action entries (note/defer excluded)");
383
+ } finally {
384
+ rmSync(tmp, { recursive: true, force: true });
385
+ }
386
+ });
387
+
388
+ test("resolution: executeTriageResolutions skips already-executed captures", () => {
389
+ const tmp = makeTempDir("res-exec-skip");
390
+ try {
391
+ setupPlanFile(tmp, "M001", "S01", SAMPLE_PLAN);
392
+ const id = appendCapture(tmp, "already done");
393
+ markCaptureResolved(tmp, id, "inject", "add task", "needed");
394
+ markCaptureExecuted(tmp, id); // already executed
395
+
396
+ const result = executeTriageResolutions(tmp, "M001", "S01");
397
+
398
+ assert.strictEqual(result.injected, 0, "should not inject again");
399
+ assert.strictEqual(result.actions.length, 0, "should have no actions");
400
+ } finally {
401
+ rmSync(tmp, { recursive: true, force: true });
402
+ }
403
+ });
404
+
405
+ test("resolution: executeTriageResolutions returns empty result when no actionable captures", () => {
406
+ const tmp = makeTempDir("res-exec-empty");
407
+ try {
408
+ const result = executeTriageResolutions(tmp, "M001", "S01");
409
+ assert.strictEqual(result.injected, 0);
410
+ assert.strictEqual(result.replanned, 0);
411
+ assert.strictEqual(result.quickTasks.length, 0);
412
+ assert.strictEqual(result.actions.length, 0);
413
+ } finally {
414
+ rmSync(tmp, { recursive: true, force: true });
415
+ }
416
+ });
@@ -0,0 +1,316 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, writeFileSync, existsSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { deriveState, isValidationTerminal } from "../state.ts";
9
+ import { resolveExpectedArtifactPath, verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.ts";
10
+ import { resolveDispatch, type DispatchContext } from "../auto-dispatch.ts";
11
+ import type { GSDState } from "../types.ts";
12
+ import { clearPathCache } from "../paths.ts";
13
+ import { clearParseCache } from "../files.ts";
14
+
15
+ // ─── Helpers ──────────────────────────────────────────────────────────────
16
+
17
+ function makeTmpBase(): string {
18
+ const base = join(tmpdir(), `gsd-val-test-${randomUUID()}`);
19
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
20
+ return base;
21
+ }
22
+
23
+ function cleanup(base: string): void {
24
+ clearPathCache();
25
+ clearParseCache();
26
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
27
+ }
28
+
29
+ function writeRoadmap(base: string, mid: string, content: string): void {
30
+ const dir = join(base, ".gsd", "milestones", mid);
31
+ mkdirSync(dir, { recursive: true });
32
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
33
+ }
34
+
35
+ function writeMilestoneSummary(base: string, mid: string, content: string): void {
36
+ const dir = join(base, ".gsd", "milestones", mid);
37
+ mkdirSync(dir, { recursive: true });
38
+ writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
39
+ }
40
+
41
+ function writeValidation(base: string, mid: string, content: string): void {
42
+ const dir = join(base, ".gsd", "milestones", mid);
43
+ mkdirSync(dir, { recursive: true });
44
+ writeFileSync(join(dir, `${mid}-VALIDATION.md`), content);
45
+ }
46
+
47
+ function writeSlicePlan(base: string, mid: string, sid: string, content: string): void {
48
+ const dir = join(base, ".gsd", "milestones", mid, "slices", sid);
49
+ mkdirSync(join(dir, "tasks"), { recursive: true });
50
+ writeFileSync(join(dir, `${sid}-PLAN.md`), content);
51
+ }
52
+
53
+ function writeSliceSummary(base: string, mid: string, sid: string, content: string): void {
54
+ const dir = join(base, ".gsd", "milestones", mid, "slices", sid);
55
+ mkdirSync(dir, { recursive: true });
56
+ writeFileSync(join(dir, `${sid}-SUMMARY.md`), content);
57
+ }
58
+
59
+ const ALL_DONE_ROADMAP = `# M001: Test Milestone
60
+
61
+ ## Vision
62
+ Test
63
+
64
+ ## Success Criteria
65
+ - It works
66
+
67
+ ## Slices
68
+
69
+ - [x] **S01: First slice** \`risk:low\` \`depends:[]\`
70
+ > After this: it works
71
+
72
+ ## Boundary Map
73
+
74
+ | From | To | Produces | Consumes |
75
+ |------|-----|----------|----------|
76
+ | S01 | terminal | output | nothing |
77
+ `;
78
+
79
+ const CONTEXT_FILE = `---
80
+ id: M001
81
+ title: Test Milestone
82
+ ---
83
+
84
+ # Context
85
+ Test context.
86
+ `;
87
+
88
+ // ─── isValidationTerminal ─────────────────────────────────────────────────
89
+
90
+ test("isValidationTerminal returns true for verdict: pass", () => {
91
+ const content = "---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation";
92
+ assert.equal(isValidationTerminal(content), true);
93
+ });
94
+
95
+ test("isValidationTerminal returns true for verdict: needs-attention", () => {
96
+ const content = "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation";
97
+ assert.equal(isValidationTerminal(content), true);
98
+ });
99
+
100
+ test("isValidationTerminal returns false for verdict: needs-remediation", () => {
101
+ const content = "---\nverdict: needs-remediation\nremediation_round: 0\n---\n\n# Validation";
102
+ assert.equal(isValidationTerminal(content), false);
103
+ });
104
+
105
+ test("isValidationTerminal returns false for missing frontmatter", () => {
106
+ const content = "# Validation\nNo frontmatter here.";
107
+ assert.equal(isValidationTerminal(content), false);
108
+ });
109
+
110
+ test("isValidationTerminal returns false for missing verdict field", () => {
111
+ const content = "---\nremediation_round: 0\n---\n\n# Validation";
112
+ assert.equal(isValidationTerminal(content), false);
113
+ });
114
+
115
+ // ─── deriveState: validating-milestone ────────────────────────────────────
116
+
117
+ test("deriveState returns validating-milestone when all slices done and no VALIDATION file", async () => {
118
+ const base = makeTmpBase();
119
+ try {
120
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
121
+ // Write CONTEXT so milestone has a title
122
+ const dir = join(base, ".gsd", "milestones", "M001");
123
+ writeFileSync(join(dir, "M001-CONTEXT.md"), CONTEXT_FILE);
124
+
125
+ const state = await deriveState(base);
126
+ assert.equal(state.phase, "validating-milestone");
127
+ assert.equal(state.activeMilestone?.id, "M001");
128
+ assert.equal(state.activeSlice, null);
129
+ } finally {
130
+ cleanup(base);
131
+ }
132
+ });
133
+
134
+ test("deriveState returns completing-milestone when VALIDATION exists with terminal verdict", async () => {
135
+ const base = makeTmpBase();
136
+ try {
137
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
138
+ writeValidation(base, "M001", "---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nAll good.");
139
+
140
+ const state = await deriveState(base);
141
+ assert.equal(state.phase, "completing-milestone");
142
+ assert.equal(state.activeMilestone?.id, "M001");
143
+ } finally {
144
+ cleanup(base);
145
+ }
146
+ });
147
+
148
+ test("deriveState returns validating-milestone when VALIDATION exists with needs-remediation verdict", async () => {
149
+ const base = makeTmpBase();
150
+ try {
151
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
152
+ writeValidation(base, "M001", "---\nverdict: needs-remediation\nremediation_round: 0\n---\n\n# Validation\nNeeds fixes.");
153
+
154
+ const state = await deriveState(base);
155
+ assert.equal(state.phase, "validating-milestone");
156
+ assert.equal(state.activeMilestone?.id, "M001");
157
+ } finally {
158
+ cleanup(base);
159
+ }
160
+ });
161
+
162
+ test("deriveState returns complete when both VALIDATION and SUMMARY exist", async () => {
163
+ const base = makeTmpBase();
164
+ try {
165
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
166
+ writeValidation(base, "M001", "---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.");
167
+ writeMilestoneSummary(base, "M001", "# Summary\nDone.");
168
+
169
+ const state = await deriveState(base);
170
+ assert.equal(state.phase, "complete");
171
+ } finally {
172
+ cleanup(base);
173
+ }
174
+ });
175
+
176
+ // ─── Dispatch rule ────────────────────────────────────────────────────────
177
+
178
+ test("dispatch rule matches validating-milestone phase", async () => {
179
+ const state: GSDState = {
180
+ activeMilestone: { id: "M001", title: "Test" },
181
+ activeSlice: null,
182
+ activeTask: null,
183
+ phase: "validating-milestone",
184
+ recentDecisions: [],
185
+ blockers: [],
186
+ nextAction: "Validate milestone M001.",
187
+ registry: [{ id: "M001", title: "Test", status: "active" }],
188
+ progress: { milestones: { done: 0, total: 1 } },
189
+ };
190
+
191
+ const base = makeTmpBase();
192
+ try {
193
+ // Set up minimal milestone structure for the prompt builder
194
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
195
+
196
+ const ctx: DispatchContext = {
197
+ basePath: base,
198
+ mid: "M001",
199
+ midTitle: "Test",
200
+ state,
201
+ prefs: undefined,
202
+ };
203
+ const result = await resolveDispatch(ctx);
204
+ assert.equal(result.action, "dispatch");
205
+ if (result.action === "dispatch") {
206
+ assert.equal(result.unitType, "validate-milestone");
207
+ assert.equal(result.unitId, "M001");
208
+ }
209
+ } finally {
210
+ cleanup(base);
211
+ }
212
+ });
213
+
214
+ test("dispatch rule skips when skip_milestone_validation preference is set", async () => {
215
+ const state: GSDState = {
216
+ activeMilestone: { id: "M001", title: "Test" },
217
+ activeSlice: null,
218
+ activeTask: null,
219
+ phase: "validating-milestone",
220
+ recentDecisions: [],
221
+ blockers: [],
222
+ nextAction: "Validate milestone M001.",
223
+ registry: [{ id: "M001", title: "Test", status: "active" }],
224
+ progress: { milestones: { done: 0, total: 1 } },
225
+ };
226
+
227
+ const base = makeTmpBase();
228
+ try {
229
+ writeRoadmap(base, "M001", ALL_DONE_ROADMAP);
230
+
231
+ const ctx: DispatchContext = {
232
+ basePath: base,
233
+ mid: "M001",
234
+ midTitle: "Test",
235
+ state,
236
+ prefs: { phases: { skip_milestone_validation: true } },
237
+ };
238
+ const result = await resolveDispatch(ctx);
239
+ assert.equal(result.action, "skip");
240
+
241
+ // Verify the VALIDATION file was written
242
+ const validationPath = join(base, ".gsd", "milestones", "M001", "M001-VALIDATION.md");
243
+ assert.ok(existsSync(validationPath), "VALIDATION file should be written on skip");
244
+ } finally {
245
+ cleanup(base);
246
+ }
247
+ });
248
+
249
+ // ─── Artifact resolution & verification ───────────────────────────────────
250
+
251
+ test("resolveExpectedArtifactPath returns VALIDATION path for validate-milestone", () => {
252
+ const base = makeTmpBase();
253
+ try {
254
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
255
+ const result = resolveExpectedArtifactPath("validate-milestone", "M001", base);
256
+ assert.ok(result);
257
+ assert.ok(result!.includes("VALIDATION"));
258
+ } finally {
259
+ cleanup(base);
260
+ }
261
+ });
262
+
263
+ test("verifyExpectedArtifact passes when VALIDATION.md exists", () => {
264
+ const base = makeTmpBase();
265
+ try {
266
+ writeValidation(base, "M001", "---\nverdict: pass\n---\n# Val");
267
+ clearPathCache();
268
+ clearParseCache();
269
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
270
+ assert.equal(result, true);
271
+ } finally {
272
+ cleanup(base);
273
+ }
274
+ });
275
+
276
+ test("verifyExpectedArtifact fails when VALIDATION.md is missing", () => {
277
+ const base = makeTmpBase();
278
+ try {
279
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
280
+ clearPathCache();
281
+ clearParseCache();
282
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
283
+ assert.equal(result, false);
284
+ } finally {
285
+ cleanup(base);
286
+ }
287
+ });
288
+
289
+ // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
290
+
291
+ test("diagnoseExpectedArtifact returns validation path for validate-milestone", () => {
292
+ const base = makeTmpBase();
293
+ try {
294
+ const result = diagnoseExpectedArtifact("validate-milestone", "M001", base);
295
+ assert.ok(result);
296
+ assert.ok(result!.includes("VALIDATION"));
297
+ assert.ok(result!.includes("milestone validation report"));
298
+ } finally {
299
+ cleanup(base);
300
+ }
301
+ });
302
+
303
+ // ─── buildLoopRemediationSteps ────────────────────────────────────────────
304
+
305
+ test("buildLoopRemediationSteps returns steps for validate-milestone", () => {
306
+ const base = makeTmpBase();
307
+ try {
308
+ const result = buildLoopRemediationSteps("validate-milestone", "M001", base);
309
+ assert.ok(result);
310
+ assert.ok(result!.includes("VALIDATION"));
311
+ assert.ok(result!.includes("verdict: pass"));
312
+ assert.ok(result!.includes("gsd doctor"));
313
+ } finally {
314
+ cleanup(base);
315
+ }
316
+ });
@@ -1,5 +1,5 @@
1
1
  // Tests for GSD visualizer overlay.
2
- // Verifies filter mode, tab switching, and export key handling.
2
+ // Verifies filter mode, tab switching, including reverse tab navigation, and export key handling.
3
3
 
4
4
  import { readFileSync } from "node:fs";
5
5
  import { join, dirname } from "node:path";
@@ -81,6 +81,11 @@ assertTrue(
81
81
  "tab key wraps around TAB_COUNT",
82
82
  );
83
83
 
84
+ assertTrue(
85
+ overlaySrc.includes('Key.shift("tab")') || overlaySrc.includes("Key.shift('tab')"),
86
+ "supports Shift+Tab for reverse tab switching",
87
+ );
88
+
84
89
  console.log("\n=== Overlay: Export Key Interception ===");
85
90
 
86
91
  assertTrue(
@@ -101,8 +106,8 @@ assertTrue(
101
106
  console.log("\n=== Overlay: Footer ===");
102
107
 
103
108
  assertTrue(
104
- overlaySrc.includes("Tab/1-7"),
105
- "footer hint shows 1-7 tab range",
109
+ overlaySrc.includes("Tab/Shift+Tab/1-7"),
110
+ "footer hint shows Tab, Shift+Tab, and 1-7 tab range",
106
111
  );
107
112
 
108
113
  assertTrue(