gsd-pi 2.46.1 → 2.47.0-dev.8cfe772

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 (220) hide show
  1. package/README.md +46 -29
  2. package/dist/resources/extensions/claude-code-cli/index.js +25 -0
  3. package/dist/resources/extensions/claude-code-cli/models.js +40 -0
  4. package/dist/resources/extensions/claude-code-cli/package.json +11 -0
  5. package/dist/resources/extensions/claude-code-cli/partial-builder.js +223 -0
  6. package/dist/resources/extensions/claude-code-cli/readiness.js +26 -0
  7. package/dist/resources/extensions/claude-code-cli/sdk-types.js +8 -0
  8. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +309 -0
  9. package/dist/resources/extensions/gsd/auto-start.js +17 -9
  10. package/dist/resources/extensions/gsd/guided-flow.js +78 -2
  11. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  13. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  14. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  16. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -2
  17. package/dist/resources/extensions/gsd/repo-identity.js +5 -2
  18. package/dist/resources/extensions/gsd/session-forensics.js +10 -1
  19. package/dist/resources/extensions/gsd/state.js +29 -2
  20. package/dist/resources/extensions/gsd/workflow-events.js +1 -1
  21. package/dist/web/standalone/.next/BUILD_ID +1 -1
  22. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  23. package/dist/web/standalone/.next/build-manifest.json +3 -3
  24. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  25. package/dist/web/standalone/.next/required-server-files.json +3 -3
  26. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  27. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  29. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  37. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  53. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  91. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  97. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  113. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  115. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  117. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  126. package/dist/web/standalone/.next/server/app/index.html +1 -1
  127. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  128. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  129. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  130. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  132. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  133. package/dist/web/standalone/.next/server/app/page.js +2 -2
  134. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  136. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  137. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  138. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/middleware.js +2 -2
  140. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  142. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  143. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  144. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  146. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  147. package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
  148. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  149. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  150. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  151. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  152. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  154. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  155. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  156. package/dist/web/standalone/server.js +1 -1
  157. package/package.json +3 -1
  158. package/packages/pi-agent-core/dist/agent-loop.js +27 -1
  159. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  160. package/packages/pi-agent-core/dist/agent.d.ts +7 -0
  161. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  162. package/packages/pi-agent-core/dist/agent.js +2 -0
  163. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  164. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  165. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  166. package/packages/pi-agent-core/dist/types.js.map +1 -1
  167. package/packages/pi-agent-core/src/agent-loop.ts +26 -1
  168. package/packages/pi-agent-core/src/agent.ts +10 -0
  169. package/packages/pi-agent-core/src/types.ts +10 -0
  170. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +27 -2
  171. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +43 -0
  173. package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/core/model-registry.js +26 -3
  176. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/sdk.js +1 -0
  179. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  180. package/packages/pi-coding-agent/package.json +1 -1
  181. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +27 -2
  182. package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +70 -0
  183. package/packages/pi-coding-agent/src/core/model-registry.ts +29 -2
  184. package/packages/pi-coding-agent/src/core/sdk.ts +1 -0
  185. package/packages/pi-tui/dist/components/box.d.ts +1 -0
  186. package/packages/pi-tui/dist/components/box.d.ts.map +1 -1
  187. package/packages/pi-tui/dist/components/box.js +10 -0
  188. package/packages/pi-tui/dist/components/box.js.map +1 -1
  189. package/packages/pi-tui/src/components/box.ts +10 -0
  190. package/pkg/package.json +1 -1
  191. package/src/resources/extensions/claude-code-cli/index.ts +28 -0
  192. package/src/resources/extensions/claude-code-cli/models.ts +42 -0
  193. package/src/resources/extensions/claude-code-cli/package.json +11 -0
  194. package/src/resources/extensions/claude-code-cli/partial-builder.ts +258 -0
  195. package/src/resources/extensions/claude-code-cli/readiness.ts +30 -0
  196. package/src/resources/extensions/claude-code-cli/sdk-types.ts +149 -0
  197. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +370 -0
  198. package/src/resources/extensions/gsd/auto-start.ts +15 -8
  199. package/src/resources/extensions/gsd/guided-flow.ts +96 -2
  200. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  201. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +2 -2
  202. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  203. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  204. package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
  205. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -2
  206. package/src/resources/extensions/gsd/repo-identity.ts +5 -2
  207. package/src/resources/extensions/gsd/session-forensics.ts +11 -1
  208. package/src/resources/extensions/gsd/state.ts +33 -1
  209. package/src/resources/extensions/gsd/tests/discuss-queued-milestones.test.ts +241 -0
  210. package/src/resources/extensions/gsd/tests/forensics-error-filter.test.ts +121 -0
  211. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +70 -0
  212. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +40 -0
  213. package/src/resources/extensions/gsd/tests/preflight-context-draft-filter.test.ts +115 -0
  214. package/src/resources/extensions/gsd/tests/run-uat.test.ts +25 -0
  215. package/src/resources/extensions/gsd/workflow-events.ts +1 -1
  216. package/dist/web/standalone/.next/static/chunks/app/page-12dd5ece0df4badc.js +0 -1
  217. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  218. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  219. /package/dist/web/standalone/.next/static/{P4nF4UcdATjrbNMBH_Ulh → DyrX2zX_4v7KZDbUNxE2q}/_buildManifest.js +0 -0
  220. /package/dist/web/standalone/.next/static/{P4nF4UcdATjrbNMBH_Ulh → DyrX2zX_4v7KZDbUNxE2q}/_ssgManifest.js +0 -0
@@ -49,6 +49,7 @@ import {
49
49
  getReplanHistory,
50
50
  getSlice,
51
51
  insertMilestone,
52
+ updateTaskStatus,
52
53
  type MilestoneRow,
53
54
  type SliceRow,
54
55
  type TaskRow,
@@ -629,7 +630,38 @@ export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
629
630
  }
630
631
 
631
632
  // ── Get tasks from DB ────────────────────────────────────────────────
632
- const tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
633
+ let tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
634
+
635
+ // ── Reconcile stale task status (#2514) ──────────────────────────────
636
+ // When a session disconnects after the agent writes SUMMARY + VERIFY
637
+ // artifacts but before postUnitPostVerification updates the DB, tasks
638
+ // remain "pending" in the DB despite being complete on disk. Without
639
+ // reconciliation, deriveState keeps returning the stale task as active,
640
+ // causing the dispatcher to re-dispatch the same completed task forever.
641
+ let reconciled = false;
642
+ for (const t of tasks) {
643
+ if (isStatusDone(t.status)) continue;
644
+ const summaryPath = resolveTaskFile(basePath, activeMilestone.id, activeSlice.id, t.id, "SUMMARY");
645
+ if (summaryPath && existsSync(summaryPath)) {
646
+ try {
647
+ updateTaskStatus(activeMilestone.id, activeSlice.id, t.id, "complete");
648
+ process.stderr.write(
649
+ `gsd-reconcile: task ${activeMilestone.id}/${activeSlice.id}/${t.id} had SUMMARY on disk but DB status was "${t.status}" — updated to "complete" (#2514)\n`,
650
+ );
651
+ reconciled = true;
652
+ } catch (e) {
653
+ // DB write failed — continue with stale status rather than crash
654
+ process.stderr.write(
655
+ `gsd-reconcile: failed to update task ${t.id}: ${(e as Error).message}\n`,
656
+ );
657
+ }
658
+ }
659
+ }
660
+ // Re-fetch tasks if any were reconciled so downstream logic sees fresh status
661
+ if (reconciled) {
662
+ tasks = getSliceTasks(activeMilestone.id, activeSlice.id);
663
+ }
664
+
633
665
  const taskProgress = {
634
666
  done: tasks.filter(t => isStatusDone(t.status)).length,
635
667
  total: tasks.length,
@@ -0,0 +1,241 @@
1
+ /**
2
+ * discuss-queued-milestones.test.ts — Tests for #2307.
3
+ *
4
+ * /gsd discuss was previously gated on state.activeMilestone, which prevented
5
+ * users from discussing queued (pending) milestones during roadmap grooming.
6
+ *
7
+ * These tests verify:
8
+ * 1. deriveState correctly identifies pending milestones (the set the picker
9
+ * will show when no active milestone is present)
10
+ * 2. resolveMilestoneFile correctly resolves context artifacts for pending
11
+ * milestones so the picker can report their discussion state
12
+ * 3. The guided-flow.ts source code no longer hard-exits when no active
13
+ * milestone exists but pending milestones are present
14
+ * 4. The helper functions for queued discuss exist in the source
15
+ */
16
+
17
+ import { describe, test, afterEach } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { tmpdir } from "node:os";
22
+ import { fileURLToPath } from "node:url";
23
+ import { dirname } from "node:path";
24
+
25
+ import { deriveState } from "../state.ts";
26
+ import { invalidateAllCaches } from "../cache.ts";
27
+ import { resolveMilestoneFile } from "../paths.ts";
28
+
29
+ // ─── Fixture Helpers ──────────────────────────────────────────────────────────
30
+
31
+ function createBase(): string {
32
+ const base = mkdtempSync(join(tmpdir(), "gsd-discuss-queued-"));
33
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
34
+ return base;
35
+ }
36
+
37
+ function cleanup(base: string): void {
38
+ rmSync(base, { recursive: true, force: true });
39
+ }
40
+
41
+ function writeMilestoneDir(base: string, mid: string): void {
42
+ mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
43
+ }
44
+
45
+ function writeContext(base: string, mid: string, content: string): void {
46
+ writeMilestoneDir(base, mid);
47
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-CONTEXT.md`), content);
48
+ }
49
+
50
+ function writeContextDraft(base: string, mid: string, content: string): void {
51
+ writeMilestoneDir(base, mid);
52
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-CONTEXT-DRAFT.md`), content);
53
+ }
54
+
55
+ function writeRoadmap(base: string, mid: string, content: string): void {
56
+ writeMilestoneDir(base, mid);
57
+ writeFileSync(join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`), content);
58
+ }
59
+
60
+ function readGuidedFlowSource(): string {
61
+ const thisFile = fileURLToPath(import.meta.url);
62
+ const thisDir = dirname(thisFile);
63
+ return readFileSync(join(thisDir, "..", "guided-flow.ts"), "utf-8");
64
+ }
65
+
66
+ // ─── Tests ────────────────────────────────────────────────────────────────────
67
+
68
+ describe("discuss-queued-milestones (#2307)", () => {
69
+
70
+ test("1. pending milestones appear in registry when active milestone exists", async () => {
71
+ const base = createBase();
72
+ try {
73
+ // M001: active — has context + roadmap with a slice
74
+ writeContext(base, "M001", "# M001: Active\nContext here.");
75
+ writeRoadmap(base, "M001",
76
+ "# M001: Active\n\n## Slices\n- [ ] **S01: Do work** `risk:low` `depends:[]`\n > After this: works\n");
77
+
78
+ // M002: pending — context only, no roadmap
79
+ writeContext(base, "M002", "# M002: Queued\nFuture work.");
80
+
81
+ // M003: pending — draft context only
82
+ writeContextDraft(base, "M003", "# M003: Draft\nSeed material.");
83
+
84
+ invalidateAllCaches();
85
+ const state = await deriveState(base);
86
+
87
+ assert.ok(!!state.activeMilestone, "M001 should be the active milestone");
88
+ assert.strictEqual(state.activeMilestone?.id, "M001");
89
+
90
+ const pendingIds = state.registry
91
+ .filter(m => m.status === "pending")
92
+ .map(m => m.id);
93
+
94
+ assert.ok(pendingIds.includes("M002"), "M002 should be pending");
95
+ assert.ok(pendingIds.includes("M003"), "M003 should be pending");
96
+ } finally {
97
+ cleanup(base);
98
+ }
99
+ });
100
+
101
+ test("2. first context-only milestone is active, subsequent ones are pending", async () => {
102
+ const base = createBase();
103
+ try {
104
+ // M001: first milestone with context but no roadmap — deriveState marks it active
105
+ writeContext(base, "M001", "# M001: First\nContext here.");
106
+ // M002: will be pending since M001 is active
107
+ writeContext(base, "M002", "# M002: Second\nMore future work.");
108
+
109
+ invalidateAllCaches();
110
+ const state = await deriveState(base);
111
+
112
+ // deriveState makes the first unfinished milestone "active" even without a roadmap
113
+ assert.ok(!!state.activeMilestone, "first milestone should be active");
114
+ assert.strictEqual(state.activeMilestone?.id, "M001", "M001 is the active milestone");
115
+
116
+ const pendingIds = state.registry
117
+ .filter(m => m.status === "pending")
118
+ .map(m => m.id);
119
+
120
+ assert.ok(pendingIds.includes("M002"),
121
+ "M002 should be pending — it comes after the active M001");
122
+ } finally {
123
+ cleanup(base);
124
+ }
125
+ });
126
+
127
+ test("3. resolveMilestoneFile finds CONTEXT.md for pending milestone", (t) => {
128
+ const base = createBase();
129
+ try {
130
+ writeContext(base, "M002", "# M002: Queued\nContent.");
131
+
132
+ const contextFile = resolveMilestoneFile(base, "M002", "CONTEXT");
133
+ assert.ok(contextFile !== null, "resolveMilestoneFile should find CONTEXT.md for M002");
134
+ assert.ok(contextFile!.endsWith("M002-CONTEXT.md"),
135
+ "resolved path should point to M002-CONTEXT.md");
136
+ } finally {
137
+ cleanup(base);
138
+ }
139
+ });
140
+
141
+ test("4. resolveMilestoneFile finds CONTEXT-DRAFT.md for pending milestone", (t) => {
142
+ const base = createBase();
143
+ try {
144
+ writeContextDraft(base, "M003", "# M003: Draft\nSeed content.");
145
+
146
+ const draftFile = resolveMilestoneFile(base, "M003", "CONTEXT-DRAFT");
147
+ assert.ok(draftFile !== null, "resolveMilestoneFile should find CONTEXT-DRAFT.md for M003");
148
+ assert.ok(draftFile!.endsWith("M003-CONTEXT-DRAFT.md"),
149
+ "resolved path should point to M003-CONTEXT-DRAFT.md");
150
+ } finally {
151
+ cleanup(base);
152
+ }
153
+ });
154
+
155
+ test("5. resolveMilestoneFile returns null when pending milestone has no context", (t) => {
156
+ const base = createBase();
157
+ try {
158
+ writeMilestoneDir(base, "M004");
159
+
160
+ const contextFile = resolveMilestoneFile(base, "M004", "CONTEXT");
161
+ assert.strictEqual(contextFile, null,
162
+ "resolveMilestoneFile should return null when no CONTEXT.md exists");
163
+
164
+ const draftFile = resolveMilestoneFile(base, "M004", "CONTEXT-DRAFT");
165
+ assert.strictEqual(draftFile, null,
166
+ "resolveMilestoneFile should return null when no CONTEXT-DRAFT.md exists");
167
+ } finally {
168
+ cleanup(base);
169
+ }
170
+ });
171
+
172
+ test("6. guided-flow no longer hard-exits when no active milestone but pending exist", () => {
173
+ const source = readGuidedFlowSource();
174
+
175
+ // The old guard was a simple early-exit:
176
+ // if (!state.activeMilestone) {
177
+ // ctx.ui.notify("No active milestone. Run /gsd to create one first.", "warning");
178
+ // return;
179
+ // }
180
+ //
181
+ // The new guard should check for pending milestones and route instead.
182
+ const oldGuardPattern = /if\s*\(!state\.activeMilestone\)\s*\{\s*ctx\.ui\.notify\("No active milestone/;
183
+ assert.ok(
184
+ !oldGuardPattern.test(source),
185
+ "guided-flow must not unconditionally exit when activeMilestone is null",
186
+ );
187
+ });
188
+
189
+ test("7. showDiscussQueuedMilestone helper exists in guided-flow", () => {
190
+ const source = readGuidedFlowSource();
191
+ assert.ok(
192
+ source.includes("showDiscussQueuedMilestone"),
193
+ "guided-flow must export showDiscussQueuedMilestone helper",
194
+ );
195
+ });
196
+
197
+ test("8. dispatchDiscussForMilestone helper exists in guided-flow", () => {
198
+ const source = readGuidedFlowSource();
199
+ assert.ok(
200
+ source.includes("dispatchDiscussForMilestone"),
201
+ "guided-flow must export dispatchDiscussForMilestone helper",
202
+ );
203
+ });
204
+
205
+ test("9. dispatchDiscussForMilestone does not set pendingAutoStart", () => {
206
+ const source = readGuidedFlowSource();
207
+
208
+ // Extract the dispatchDiscussForMilestone function body
209
+ const fnMatch = source.match(
210
+ /async function dispatchDiscussForMilestone\s*\([^)]*\)[^{]*\{([\s\S]*?)\n\}/,
211
+ );
212
+ assert.ok(!!fnMatch, "dispatchDiscussForMilestone function body must be present");
213
+
214
+ if (fnMatch) {
215
+ assert.ok(
216
+ !fnMatch[1].includes("pendingAutoStart"),
217
+ "dispatchDiscussForMilestone must NOT set pendingAutoStart — discussing a queued milestone must not activate it",
218
+ );
219
+ }
220
+ });
221
+
222
+ test("10. slice picker includes queued milestone option when pending milestones exist", () => {
223
+ const source = readGuidedFlowSource();
224
+ assert.ok(
225
+ source.includes("discuss_queued_milestone"),
226
+ "slice picker must include a 'discuss_queued_milestone' action id for queued milestones",
227
+ );
228
+ assert.ok(
229
+ source.includes("Discuss a queued milestone"),
230
+ "slice picker must label the queued milestone action clearly",
231
+ );
232
+ });
233
+
234
+ test("11. queued milestone picker labels entries with [queued]", () => {
235
+ const source = readGuidedFlowSource();
236
+ assert.ok(
237
+ source.includes("[queued]"),
238
+ "queued milestone picker must label entries with [queued] to distinguish from active",
239
+ );
240
+ });
241
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Regression test for #2539: extractTrace should not count benign bash
3
+ * exit-code-1 (grep no-match) or user skips as errors.
4
+ */
5
+ import { describe, test } from "node:test";
6
+ import assert from "node:assert/strict";
7
+
8
+ import { extractTrace } from "../session-forensics.ts";
9
+
10
+ /**
11
+ * Build a minimal JSONL entry pair: assistant tool_use → toolResult.
12
+ * This is the shape extractTrace() expects from session activity files.
13
+ */
14
+ function makeToolPair(
15
+ toolName: string,
16
+ input: Record<string, unknown>,
17
+ resultText: string,
18
+ isError: boolean,
19
+ ): unknown[] {
20
+ const toolCallId = `toolu_${Math.random().toString(36).slice(2, 10)}`;
21
+ return [
22
+ {
23
+ type: "message",
24
+ message: {
25
+ role: "assistant",
26
+ content: [
27
+ {
28
+ type: "toolCall",
29
+ id: toolCallId,
30
+ name: toolName,
31
+ arguments: input,
32
+ },
33
+ ],
34
+ },
35
+ },
36
+ {
37
+ type: "message",
38
+ message: {
39
+ role: "toolResult",
40
+ toolCallId,
41
+ toolName,
42
+ isError,
43
+ content: [{ type: "text", text: resultText }],
44
+ },
45
+ },
46
+ ];
47
+ }
48
+
49
+ describe("extractTrace error filtering (#2539)", () => {
50
+ test("grep exit-code-1 (no matches) is not counted as an error", () => {
51
+ const entries = makeToolPair(
52
+ "bash",
53
+ { command: "grep -rn 'nonexistent' src/" },
54
+ "(no output)\nCommand exited with code 1",
55
+ true,
56
+ );
57
+ const trace = extractTrace(entries);
58
+ assert.equal(trace.errors.length, 0, "grep no-match should not be an error");
59
+ });
60
+
61
+ test("user skip is not counted as an error", () => {
62
+ const entries = makeToolPair(
63
+ "bash",
64
+ { command: "npm run test" },
65
+ "Skipped due to queued user message",
66
+ true,
67
+ );
68
+ const trace = extractTrace(entries);
69
+ assert.equal(trace.errors.length, 0, "user skip should not be an error");
70
+ });
71
+
72
+ test("real bash error is still counted", () => {
73
+ const entries = makeToolPair(
74
+ "bash",
75
+ { command: "cat /nonexistent" },
76
+ "cat: /nonexistent: No such file or directory\nCommand exited with code 1",
77
+ true,
78
+ );
79
+ const trace = extractTrace(entries);
80
+ assert.equal(trace.errors.length, 1, "real error should still be counted");
81
+ assert.match(trace.errors[0], /No such file or directory/);
82
+ });
83
+
84
+ test("non-bash tool error is still counted", () => {
85
+ const entries = makeToolPair(
86
+ "edit",
87
+ { path: "foo.ts", oldText: "x", newText: "y" },
88
+ "oldText not found in file",
89
+ true,
90
+ );
91
+ const trace = extractTrace(entries);
92
+ assert.equal(trace.errors.length, 1, "non-bash tool errors should still be counted");
93
+ });
94
+
95
+ test("mixed entries: only real errors are counted", () => {
96
+ const entries = [
97
+ // benign grep no-match
98
+ ...makeToolPair("bash", { command: "grep -rn 'pattern' src/" }, "(no output)\nCommand exited with code 1", true),
99
+ // user skip
100
+ ...makeToolPair("bash", { command: "npm test" }, "Skipped due to queued user message", true),
101
+ // real error
102
+ ...makeToolPair("bash", { command: "node broken.js" }, "SyntaxError: Unexpected token\nCommand exited with code 1", true),
103
+ // successful command (not an error)
104
+ ...makeToolPair("bash", { command: "echo hello" }, "hello", false),
105
+ ];
106
+ const trace = extractTrace(entries);
107
+ assert.equal(trace.errors.length, 1, "only the real error should be counted");
108
+ assert.match(trace.errors[0], /SyntaxError/);
109
+ });
110
+
111
+ test("exit code 1 with actual output is still an error", () => {
112
+ const entries = makeToolPair(
113
+ "bash",
114
+ { command: "npm run lint" },
115
+ "src/foo.ts:10:5 - error TS2304: Cannot find name 'x'\nCommand exited with code 1",
116
+ true,
117
+ );
118
+ const trace = extractTrace(entries);
119
+ assert.equal(trace.errors.length, 1, "lint error with output should be counted");
120
+ });
121
+ });
@@ -119,3 +119,73 @@ describe("isInheritedRepo when git root is HOME (#2393)", () => {
119
119
  );
120
120
  });
121
121
  });
122
+
123
+ describe("isInheritedRepo with stale .gsd at parent git root", () => {
124
+ let parentRepo: string;
125
+
126
+ beforeEach(() => {
127
+ parentRepo = realpathSync(mkdtempSync(join(tmpdir(), "gsd-stale-parent-")));
128
+ run("git", ["init", "-b", "main"], parentRepo);
129
+ run("git", ["config", "user.name", "Test"], parentRepo);
130
+ run("git", ["config", "user.email", "test@example.com"], parentRepo);
131
+ writeFileSync(join(parentRepo, "README.md"), "# Parent\n", "utf-8");
132
+ run("git", ["add", "README.md"], parentRepo);
133
+ run("git", ["commit", "-m", "init"], parentRepo);
134
+ });
135
+
136
+ afterEach(() => {
137
+ rmSync(parentRepo, { recursive: true, force: true });
138
+ });
139
+
140
+ test("stale .gsd dir at parent git root does not suppress inherited detection", () => {
141
+ // Simulate a stale .gsd directory at the parent git root (e.g. from a
142
+ // prior doctor run or accidental init). This is a real directory, NOT
143
+ // a symlink, and NOT the global GSD home.
144
+ mkdirSync(join(parentRepo, ".gsd"), { recursive: true });
145
+
146
+ const projectDir = join(parentRepo, "my-project");
147
+ mkdirSync(projectDir, { recursive: true });
148
+
149
+ // Without fix: isProjectGsd(join(root, ".gsd")) returns true because
150
+ // the stale .gsd is a real directory that isn't the global GSD home,
151
+ // causing isInheritedRepo to return false (false negative).
152
+ //
153
+ // The stale .gsd at parent is still treated as a "project .gsd" by
154
+ // isProjectGsd(), so the git root check at line 128 returns false.
155
+ // This is the expected behavior for that check — the defense-in-depth
156
+ // fix in auto-start.ts handles this case by checking for local .git.
157
+ //
158
+ // Verify the function behavior is consistent:
159
+ assert.strictEqual(
160
+ isInheritedRepo(projectDir),
161
+ false,
162
+ "stale .gsd dir at git root still causes isInheritedRepo to return false " +
163
+ "(defense-in-depth in auto-start.ts handles this case)",
164
+ );
165
+ });
166
+
167
+ test("basePath's own .gsd symlink does not suppress inherited detection", () => {
168
+ // Create a project subdir with its own .gsd symlink (set up during
169
+ // the discuss phase, before auto-mode bootstrap runs).
170
+ const projectDir = join(parentRepo, "my-project");
171
+ mkdirSync(projectDir, { recursive: true });
172
+
173
+ const externalState = mkdtempSync(join(tmpdir(), "gsd-ext-state-"));
174
+ symlinkSync(externalState, join(projectDir, ".gsd"));
175
+
176
+ // Before fix: the walk-up loop started at normalizedBase (projectDir),
177
+ // found .gsd at projectDir, and returned false — even though projectDir
178
+ // has no .git of its own. The .gsd at basePath is irrelevant to whether
179
+ // the git repo is inherited from a parent.
180
+ //
181
+ // After fix: the walk-up starts at dirname(normalizedBase), skipping
182
+ // basePath's own .gsd.
183
+ assert.strictEqual(
184
+ isInheritedRepo(projectDir),
185
+ true,
186
+ "project's own .gsd symlink must not suppress inherited repo detection",
187
+ );
188
+
189
+ rmSync(externalState, { recursive: true, force: true });
190
+ });
191
+ });
@@ -61,6 +61,18 @@ test("plan-slice prompt: DB-backed tool names survive template substitution", ()
61
61
  assert.ok(result.includes("canonical write path"), "canonical write path language should survive substitution");
62
62
  });
63
63
 
64
+ test("plan-slice prompt: footer references gsd_plan_slice tool, not direct write", () => {
65
+ const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit." });
66
+ assert.ok(
67
+ result.includes("MUST call `gsd_plan_slice`"),
68
+ "footer should instruct calling gsd_plan_slice tool",
69
+ );
70
+ assert.ok(
71
+ !result.includes("MUST write the file"),
72
+ "footer should not instruct direct file write",
73
+ );
74
+ });
75
+
64
76
  test("domain-work prompts use skillActivation placeholder", () => {
65
77
  const prompts = [
66
78
  "research-milestone",
@@ -174,6 +186,34 @@ test("research-milestone prompt substitutes skillActivation", () => {
174
186
  assert.ok(!result.includes("{{skillActivation}}"));
175
187
  });
176
188
 
189
+ test("research-milestone prompt references gsd_summary_save, not direct write", () => {
190
+ const result = loadPrompt("research-milestone", {
191
+ workingDirectory: "/tmp/test-project",
192
+ milestoneId: "M001",
193
+ milestoneTitle: "Test Milestone",
194
+ milestonePath: ".gsd/milestones/M001",
195
+ contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
196
+ outputPath: "/tmp/test-project/.gsd/milestones/M001/M001-RESEARCH.md",
197
+ inlinedContext: "Context",
198
+ skillDiscoveryMode: "manual",
199
+ skillDiscoveryInstructions: " Discover skills manually.",
200
+ skillActivation: "Load research skills first.",
201
+ });
202
+
203
+ assert.ok(
204
+ result.includes("gsd_summary_save"),
205
+ "research-milestone should reference gsd_summary_save tool",
206
+ );
207
+ assert.ok(
208
+ result.includes('artifact_type: "RESEARCH"'),
209
+ "research-milestone should specify RESEARCH artifact type",
210
+ );
211
+ assert.ok(
212
+ !result.includes("MUST write the file"),
213
+ "research-milestone should not instruct direct file write",
214
+ );
215
+ });
216
+
177
217
  test("research-slice prompt substitutes skillActivation", () => {
178
218
  const result = loadPrompt("research-slice", {
179
219
  workingDirectory: "/tmp/test-project",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Regression test for #2473: Pre-flight CONTEXT-DRAFT warning should skip
3
+ * completed and parked milestones.
4
+ *
5
+ * The pre-flight loop in auto-start.ts warns about CONTEXT-DRAFT.md files
6
+ * so the user knows which milestones will pause for discussion. But completed
7
+ * milestones with leftover CONTEXT-DRAFT.md files are not actionable — the
8
+ * warning is noise.
9
+ *
10
+ * This test exercises the filtering logic directly: given a set of milestones
11
+ * with CONTEXT-DRAFT files, only active/pending ones should produce warnings.
12
+ */
13
+ import { describe, test, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+
19
+ import {
20
+ openDatabase,
21
+ closeDatabase,
22
+ isDbAvailable,
23
+ insertMilestone,
24
+ getMilestone,
25
+ } from "../gsd-db.ts";
26
+ import { resolveMilestoneFile } from "../paths.ts";
27
+
28
+ describe("pre-flight CONTEXT-DRAFT filter (#2473)", () => {
29
+ let tmpBase: string;
30
+ let gsd: string;
31
+
32
+ beforeEach(() => {
33
+ tmpBase = mkdtempSync(join(tmpdir(), "gsd-preflight-draft-"));
34
+ gsd = join(tmpBase, ".gsd");
35
+
36
+ // Create milestone directories with CONTEXT-DRAFT files
37
+ for (const id of ["M001", "M002", "M003"]) {
38
+ const msDir = join(gsd, "milestones", id);
39
+ mkdirSync(msDir, { recursive: true });
40
+ writeFileSync(join(msDir, `${id}-CONTEXT-DRAFT.md`), `# ${id}: Draft\n`);
41
+ }
42
+
43
+ // Open DB and insert milestones with different statuses
44
+ const dbPath = join(gsd, "gsd.db");
45
+ openDatabase(dbPath);
46
+ insertMilestone({ id: "M001", title: "Complete milestone", status: "complete" });
47
+ insertMilestone({ id: "M002", title: "Active milestone", status: "active" });
48
+ insertMilestone({ id: "M003", title: "Parked milestone", status: "parked" });
49
+ });
50
+
51
+ afterEach(() => {
52
+ closeDatabase();
53
+ rmSync(tmpBase, { recursive: true, force: true });
54
+ });
55
+
56
+ test("completed milestone is skipped — no warning emitted", () => {
57
+ assert.ok(isDbAvailable(), "DB should be available");
58
+ const ms = getMilestone("M001");
59
+ assert.equal(ms?.status, "complete");
60
+ });
61
+
62
+ test("parked milestone is skipped — no warning emitted", () => {
63
+ const ms = getMilestone("M003");
64
+ assert.equal(ms?.status, "parked");
65
+ });
66
+
67
+ test("active milestone with CONTEXT-DRAFT produces warning", () => {
68
+ const ms = getMilestone("M002");
69
+ assert.equal(ms?.status, "active");
70
+
71
+ const draft = resolveMilestoneFile(tmpBase, "M002", "CONTEXT-DRAFT");
72
+ assert.ok(draft, "CONTEXT-DRAFT file should be found for active milestone");
73
+ });
74
+
75
+ test("full pre-flight filter produces warnings only for active milestones", () => {
76
+ const milestoneIds = ["M001", "M002", "M003"];
77
+ const issues: string[] = [];
78
+
79
+ for (const id of milestoneIds) {
80
+ // Replicate the fixed pre-flight logic from auto-start.ts
81
+ if (isDbAvailable()) {
82
+ const ms = getMilestone(id);
83
+ if (ms?.status === "complete" || ms?.status === "parked") continue;
84
+ }
85
+ const draft = resolveMilestoneFile(tmpBase, id, "CONTEXT-DRAFT");
86
+ if (draft) {
87
+ issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
88
+ }
89
+ }
90
+
91
+ assert.equal(issues.length, 1, "only one warning should be emitted");
92
+ assert.match(issues[0], /M002/, "warning should be for the active milestone only");
93
+ });
94
+
95
+ test("when DB is unavailable, all milestones with CONTEXT-DRAFT produce warnings (safe fallback)", () => {
96
+ closeDatabase();
97
+ assert.ok(!isDbAvailable(), "DB should be unavailable after close");
98
+
99
+ const milestoneIds = ["M001", "M002", "M003"];
100
+ const issues: string[] = [];
101
+
102
+ for (const id of milestoneIds) {
103
+ if (isDbAvailable()) {
104
+ const ms = getMilestone(id);
105
+ if (ms?.status === "complete" || ms?.status === "parked") continue;
106
+ }
107
+ const draft = resolveMilestoneFile(tmpBase, id, "CONTEXT-DRAFT");
108
+ if (draft) {
109
+ issues.push(`${id}: has CONTEXT-DRAFT.md (will pause for discussion)`);
110
+ }
111
+ }
112
+
113
+ assert.equal(issues.length, 3, "all milestones should warn when DB is unavailable");
114
+ });
115
+ });
@@ -228,6 +228,31 @@ test('(k) run-uat prompt template', () => {
228
228
  );
229
229
  });
230
230
 
231
+ test('(k2) run-uat prompt references gsd_summary_save, not direct write', () => {
232
+ const promptResult = loadPromptFromWorktree('run-uat', {
233
+ workingDirectory: '/tmp/test-project',
234
+ milestoneId: 'M001',
235
+ sliceId: 'S01',
236
+ uatPath: '.gsd/milestones/M001/slices/S01/S01-UAT.md',
237
+ uatResultPath: '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md',
238
+ uatType: 'artifact-driven',
239
+ inlinedContext: '<!-- no context -->',
240
+ });
241
+
242
+ assert.ok(
243
+ promptResult.includes('gsd_summary_save'),
244
+ 'run-uat prompt should reference gsd_summary_save tool',
245
+ );
246
+ assert.ok(
247
+ promptResult.includes('artifact_type: "ASSESSMENT"'),
248
+ 'run-uat prompt should specify ASSESSMENT artifact type',
249
+ );
250
+ assert.ok(
251
+ !promptResult.includes('MUST write'),
252
+ 'run-uat prompt should not instruct direct file write in footer',
253
+ );
254
+ });
255
+
231
256
  test('(l) dispatch preconditions via resolveSliceFile', () => {
232
257
  const base = createFixtureBase();
233
258
  const uatContent = makeUatContent('artifact-driven');