gsd-pi 2.44.0-dev.d25d507 → 2.45.0-dev.6b9da3e

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 (245) hide show
  1. package/dist/resources/extensions/gsd/activity-log.js +7 -0
  2. package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
  3. package/dist/resources/extensions/gsd/auto/phases.js +37 -36
  4. package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
  5. package/dist/resources/extensions/gsd/auto-start.js +21 -2
  6. package/dist/resources/extensions/gsd/auto-timers.js +57 -3
  7. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
  8. package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
  9. package/dist/resources/extensions/gsd/auto.js +30 -3
  10. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
  11. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  12. package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
  13. package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
  14. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  15. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  16. package/dist/resources/extensions/gsd/doctor.js +8 -0
  17. package/dist/resources/extensions/gsd/git-service.js +8 -3
  18. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  19. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  20. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  21. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  22. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  23. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  24. package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
  25. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  26. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  27. package/dist/resources/extensions/gsd/repo-identity.js +45 -7
  28. package/dist/resources/extensions/gsd/rethink.js +115 -0
  29. package/dist/resources/extensions/gsd/state.js +41 -3
  30. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  31. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  32. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  33. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  34. package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
  35. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  36. package/dist/resources/extensions/mcp-client/index.js +14 -0
  37. package/dist/web/standalone/.next/BUILD_ID +1 -1
  38. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  39. package/dist/web/standalone/.next/build-manifest.json +3 -3
  40. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  41. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  42. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  44. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found/page.js +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  61. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.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_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/index.html +1 -1
  102. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  103. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  104. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  105. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  107. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  108. package/dist/web/standalone/.next/server/app/page.js +1 -1
  109. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  111. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  114. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  115. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  116. package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
  117. package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
  118. package/dist/web/standalone/.next/static/chunks/app/{page-b9367c5ae13b99c6.js → page-6654a8cca61a3d1c.js} +1 -1
  119. package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
  120. package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
  121. package/package.json +1 -1
  122. package/packages/native/dist/stream-process/index.js +2 -2
  123. package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
  124. package/packages/native/src/stream-process/index.ts +2 -2
  125. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  126. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  128. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  130. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  132. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  134. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  136. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  138. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  140. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/main.js +17 -0
  143. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  146. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  148. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  155. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  157. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  158. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  159. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  160. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  168. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  169. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  171. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  172. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  173. package/packages/pi-coding-agent/package.json +1 -1
  174. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  175. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  176. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  177. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  178. package/packages/pi-coding-agent/src/main.ts +19 -0
  179. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  180. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  181. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  182. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  183. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  184. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  185. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  186. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
  187. package/pkg/package.json +1 -1
  188. package/src/resources/extensions/gsd/activity-log.ts +1 -0
  189. package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
  190. package/src/resources/extensions/gsd/auto/phases.ts +46 -48
  191. package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
  192. package/src/resources/extensions/gsd/auto-start.ts +25 -2
  193. package/src/resources/extensions/gsd/auto-timers.ts +64 -3
  194. package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
  195. package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
  196. package/src/resources/extensions/gsd/auto.ts +37 -3
  197. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
  198. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  199. package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
  200. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  201. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  202. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  203. package/src/resources/extensions/gsd/doctor.ts +7 -1
  204. package/src/resources/extensions/gsd/git-service.ts +6 -2
  205. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  206. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  207. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  208. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  211. package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
  212. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  213. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  214. package/src/resources/extensions/gsd/repo-identity.ts +46 -7
  215. package/src/resources/extensions/gsd/rethink.ts +154 -0
  216. package/src/resources/extensions/gsd/state.ts +41 -1
  217. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  218. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  219. package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
  220. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
  221. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
  222. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  223. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  224. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
  225. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  226. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  227. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  228. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
  229. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  230. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
  231. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  232. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
  233. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
  234. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  235. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  236. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  237. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  238. package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
  239. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  240. package/src/resources/extensions/mcp-client/index.ts +20 -0
  241. package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
  242. package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
  243. package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
  244. /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_buildManifest.js +0 -0
  245. /package/dist/web/standalone/.next/static/{tokoGmfkYfWf1_Yl_Gz7i → rzO54ZboyINyEt7cVM_uS}/_ssgManifest.js +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Regression test for #2322: recoveryAttempts persists across re-dispatches,
3
+ * causing instant task skip.
4
+ *
5
+ * When a unit hits recovery limits and is later re-dispatched, the
6
+ * recoveryAttempts counter from the prior execution carries over because
7
+ * the dispatch-time writeUnitRuntimeRecord call does not reset it.
8
+ * This causes the next execution to be instantly skipped with no steering
9
+ * message or second chance.
10
+ *
11
+ * The fix: include `recoveryAttempts: 0` in the dispatch-time runtime
12
+ * record write in runUnitPhase.
13
+ */
14
+
15
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+ import {
19
+ writeUnitRuntimeRecord,
20
+ readUnitRuntimeRecord,
21
+ } from "../unit-runtime.ts";
22
+ import { createTestContext } from "./test-helpers.ts";
23
+
24
+ const { assertEq, assertTrue, report } = createTestContext();
25
+
26
+ // ═══ Setup ════════════════════════════════════════════════════════════════════
27
+
28
+ const base = mkdtempSync(join(tmpdir(), "gsd-recovery-reset-test-"));
29
+ mkdirSync(join(base, ".gsd", "runtime", "units"), { recursive: true });
30
+
31
+ try {
32
+ // ═══ #2322: recoveryAttempts should reset on re-dispatch ═══════════════════
33
+
34
+ {
35
+ console.log("\n=== #2322: recoveryAttempts should reset on re-dispatch ===");
36
+
37
+ const unitType = "execute-task";
38
+ const unitId = "M001/S01/T01";
39
+ const startedAt1 = Date.now() - 10000;
40
+
41
+ // Simulate first dispatch — clean state
42
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt1, {
43
+ phase: "dispatched",
44
+ wrapupWarningSent: false,
45
+ timeoutAt: null,
46
+ lastProgressAt: startedAt1,
47
+ progressCount: 0,
48
+ lastProgressKind: "dispatch",
49
+ });
50
+
51
+ // Simulate timeout recovery incrementing recoveryAttempts
52
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt1, {
53
+ phase: "recovered",
54
+ recoveryAttempts: 1,
55
+ lastRecoveryReason: "hard",
56
+ });
57
+
58
+ const afterRecovery = readUnitRuntimeRecord(base, unitType, unitId);
59
+ assertEq(afterRecovery?.recoveryAttempts, 1, "recoveryAttempts should be 1 after recovery");
60
+ assertEq(afterRecovery?.lastRecoveryReason, "hard", "lastRecoveryReason should be 'hard'");
61
+
62
+ // Simulate re-dispatch (second execution of same unit).
63
+ // This is what runUnitPhase should do at dispatch time — explicitly reset
64
+ // recoveryAttempts so the new execution gets its full recovery budget.
65
+ const startedAt2 = Date.now();
66
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt2, {
67
+ phase: "dispatched",
68
+ wrapupWarningSent: false,
69
+ timeoutAt: null,
70
+ lastProgressAt: startedAt2,
71
+ progressCount: 0,
72
+ lastProgressKind: "dispatch",
73
+ recoveryAttempts: 0, // FIX: must be explicitly reset
74
+ });
75
+
76
+ const afterRedispatch = readUnitRuntimeRecord(base, unitType, unitId);
77
+ assertEq(
78
+ afterRedispatch?.recoveryAttempts,
79
+ 0,
80
+ "recoveryAttempts should be 0 after re-dispatch (was carried over from prior execution)",
81
+ );
82
+ }
83
+
84
+ // ═══ Verify the BUG scenario: omitting recoveryAttempts carries it over ═══
85
+
86
+ {
87
+ console.log("\n=== #2322: demonstrates bug — omitting recoveryAttempts carries it over ===");
88
+
89
+ const unitType = "execute-task";
90
+ const unitId = "M001/S01/T02";
91
+ const startedAt1 = Date.now() - 10000;
92
+
93
+ // First dispatch
94
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt1, {
95
+ phase: "dispatched",
96
+ });
97
+
98
+ // Timeout bumps recoveryAttempts to 1
99
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt1, {
100
+ recoveryAttempts: 1,
101
+ lastRecoveryReason: "hard",
102
+ });
103
+
104
+ // Re-dispatch WITHOUT resetting recoveryAttempts (the bug)
105
+ const startedAt2 = Date.now();
106
+ writeUnitRuntimeRecord(base, unitType, unitId, startedAt2, {
107
+ phase: "dispatched",
108
+ wrapupWarningSent: false,
109
+ timeoutAt: null,
110
+ lastProgressAt: startedAt2,
111
+ progressCount: 0,
112
+ lastProgressKind: "dispatch",
113
+ // recoveryAttempts: NOT included — this is the bug
114
+ });
115
+
116
+ const afterBuggyRedispatch = readUnitRuntimeRecord(base, unitType, unitId);
117
+ // This DEMONSTRATES the bug: recoveryAttempts is still 1
118
+ assertEq(
119
+ afterBuggyRedispatch?.recoveryAttempts,
120
+ 1,
121
+ "BUG DEMO: recoveryAttempts carries over when not explicitly reset",
122
+ );
123
+ }
124
+
125
+ // ═══ Hard timeout maxRecoveryAttempts=1 — second dispatch must get full budget ═══
126
+
127
+ {
128
+ console.log("\n=== #2322: second dispatch gets full hard-timeout budget after reset ===");
129
+
130
+ const unitType = "execute-task";
131
+ const unitId = "M001/S01/T03";
132
+
133
+ // First dispatch
134
+ const start1 = Date.now() - 20000;
135
+ writeUnitRuntimeRecord(base, unitType, unitId, start1, {
136
+ phase: "dispatched",
137
+ recoveryAttempts: 0,
138
+ });
139
+
140
+ // Hard timeout recovery — exhausts the budget (maxRecoveryAttempts=1 for hard)
141
+ writeUnitRuntimeRecord(base, unitType, unitId, start1, {
142
+ phase: "recovered",
143
+ recoveryAttempts: 1,
144
+ lastRecoveryReason: "hard",
145
+ });
146
+
147
+ const afterExhausted = readUnitRuntimeRecord(base, unitType, unitId);
148
+ assertEq(afterExhausted?.recoveryAttempts, 1, "budget exhausted after hard recovery");
149
+
150
+ // Second dispatch with fix: reset recoveryAttempts
151
+ const start2 = Date.now();
152
+ writeUnitRuntimeRecord(base, unitType, unitId, start2, {
153
+ phase: "dispatched",
154
+ wrapupWarningSent: false,
155
+ timeoutAt: null,
156
+ lastProgressAt: start2,
157
+ progressCount: 0,
158
+ lastProgressKind: "dispatch",
159
+ recoveryAttempts: 0,
160
+ });
161
+
162
+ const afterReset = readUnitRuntimeRecord(base, unitType, unitId);
163
+ assertEq(afterReset?.recoveryAttempts, 0, "second dispatch has full recovery budget");
164
+
165
+ // Now a hard timeout should be recoverable (0 < 1)
166
+ assertTrue(
167
+ (afterReset?.recoveryAttempts ?? 0) < 1,
168
+ "hard recovery should be allowed (recoveryAttempts < maxRecoveryAttempts)",
169
+ );
170
+ }
171
+
172
+ } finally {
173
+ rmSync(base, { recursive: true, force: true });
174
+ }
175
+
176
+ report();
@@ -0,0 +1,67 @@
1
+ /**
2
+ * stop-auto-merge-back.test.ts — Regression test for #2317.
3
+ *
4
+ * When auto-mode stops after a milestone is complete, stopAuto should trigger
5
+ * merge-back (mergeAndExit) instead of just exiting the worktree with
6
+ * preserveBranch: true. Otherwise milestone code stays stranded on the
7
+ * worktree branch and never reaches main.
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ // ─── Source analysis: stopAuto calls mergeAndExit for complete milestones ────
16
+
17
+ const autoSrcPath = join(import.meta.dirname, "..", "auto.ts");
18
+ const autoSrc = readFileSync(autoSrcPath, "utf-8");
19
+
20
+ test("#2317: stopAuto should check milestone completion status before choosing exit strategy", () => {
21
+ // stopAuto Step 4 should NOT unconditionally call exitMilestone(preserveBranch: true).
22
+ // It should check if the milestone is complete and call mergeAndExit instead.
23
+
24
+ // Find the Step 4 section
25
+ const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
26
+ assert.ok(step4Idx !== -1, "Step 4 comment exists in stopAuto");
27
+
28
+ // Extract a reasonable window around Step 4 (up to Step 5)
29
+ const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
30
+ const step4Block = autoSrc.slice(step4Idx, step5Idx);
31
+
32
+ // The fix: Step 4 should call mergeAndExit when milestone is complete
33
+ assert.ok(
34
+ step4Block.includes("mergeAndExit"),
35
+ "Step 4 should call mergeAndExit for completed milestones",
36
+ );
37
+ });
38
+
39
+ test("#2317: stopAuto should detect milestone completion via SUMMARY file or DB", () => {
40
+ const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
41
+ const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
42
+ const step4Block = autoSrc.slice(step4Idx, step5Idx);
43
+
44
+ // Should check completion status — either via SUMMARY file, DB getMilestone, or phase
45
+ const checksCompletion =
46
+ step4Block.includes("SUMMARY") ||
47
+ step4Block.includes("getMilestone") ||
48
+ step4Block.includes("complete") ||
49
+ step4Block.includes("isMilestoneComplete");
50
+
51
+ assert.ok(
52
+ checksCompletion,
53
+ "Step 4 should check if milestone is complete before deciding exit strategy",
54
+ );
55
+ });
56
+
57
+ test("#2317: stopAuto still preserves branch for incomplete milestones", () => {
58
+ const step4Idx = autoSrc.indexOf("Step 4: Auto-worktree exit");
59
+ const step5Idx = autoSrc.indexOf("Step 5:", step4Idx);
60
+ const step4Block = autoSrc.slice(step4Idx, step5Idx);
61
+
62
+ // preserveBranch should still be used as fallback for non-complete milestones
63
+ assert.ok(
64
+ step4Block.includes("preserveBranch"),
65
+ "Step 4 should still preserve branch for incomplete milestones (fallback path)",
66
+ );
67
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Regression test for #2358: Survivor branch recovery skipped in phase=complete.
3
+ *
4
+ * When bootstrapAutoSession finds a survivor milestone branch and the derived
5
+ * state phase is "complete", recovery/finalization is skipped entirely because
6
+ * the survivor branch detection only triggers when phase === "pre-planning".
7
+ * The milestone finalization (merge, cleanup) never runs, leaving the worktree
8
+ * and branch alive.
9
+ *
10
+ * The fix broadens the survivor branch detection to also check phase === "complete",
11
+ * and adds a finalization path that runs mergeAndExit before falling through to
12
+ * the normal "complete" handling.
13
+ */
14
+
15
+ import { createTestContext } from "./test-helpers.ts";
16
+
17
+ const { assertTrue, assertEq, report } = createTestContext();
18
+
19
+ // ═══ Test: survivor branch detection conditions ══════════════════════════════
20
+
21
+ // The survivor branch detection block in auto-start.ts checks:
22
+ // state.activeMilestone &&
23
+ // state.phase === "pre-planning" && // <-- BUG: too restrictive
24
+ // shouldUseWorktreeIsolation() &&
25
+ // !detectWorktreeName(base) &&
26
+ // !base.includes(...)
27
+ //
28
+ // The fix should also include state.phase === "complete".
29
+
30
+ {
31
+ console.log("\n=== #2358: survivor branch should be detected in phase=complete ===");
32
+
33
+ // Simulate the condition check before the fix (only pre-planning)
34
+ const phasesBeforeFix = ["pre-planning"];
35
+ const phasesAfterFix = ["pre-planning", "complete"];
36
+
37
+ const testPhase = "complete";
38
+
39
+ const detectedBefore = phasesBeforeFix.includes(testPhase);
40
+ assertEq(detectedBefore, false, "before fix: phase=complete should NOT trigger survivor detection");
41
+
42
+ const detectedAfter = phasesAfterFix.includes(testPhase);
43
+ assertEq(detectedAfter, true, "after fix: phase=complete SHOULD trigger survivor detection");
44
+ }
45
+
46
+ // ═══ Test: pre-planning survivor detection still works ═══════════════════════
47
+
48
+ {
49
+ console.log("\n=== #2358: pre-planning survivor detection is not broken ===");
50
+
51
+ const phasesAfterFix = ["pre-planning", "complete"];
52
+ const testPhase = "pre-planning";
53
+
54
+ const detected = phasesAfterFix.includes(testPhase);
55
+ assertEq(detected, true, "pre-planning should still trigger survivor detection after fix");
56
+ }
57
+
58
+ // ═══ Test: other phases do NOT trigger survivor detection ════════════════════
59
+
60
+ {
61
+ console.log("\n=== #2358: other phases should NOT trigger survivor detection ===");
62
+
63
+ const phasesAfterFix = ["pre-planning", "complete"];
64
+
65
+ for (const phase of ["planning", "executing", "blocked", "needs-discussion"]) {
66
+ const detected = phasesAfterFix.includes(phase);
67
+ assertEq(detected, false, `phase=${phase} should NOT trigger survivor detection`);
68
+ }
69
+ }
70
+
71
+ // ═══ Test: phase=complete + hasSurvivorBranch should trigger finalization ═════
72
+
73
+ {
74
+ console.log("\n=== #2358: phase=complete + survivor branch triggers finalization path ===");
75
+
76
+ // Simulate the decision logic after the fix:
77
+ // if (hasSurvivorBranch && state.phase === "complete") -> finalize
78
+ // if (hasSurvivorBranch && state.phase === "needs-discussion") -> discuss
79
+ // if (!hasSurvivorBranch && state.phase === "complete") -> showSmartEntry
80
+
81
+ const scenarios = [
82
+ { hasSurvivorBranch: true, phase: "complete", expected: "finalize" },
83
+ { hasSurvivorBranch: true, phase: "needs-discussion", expected: "discuss" },
84
+ { hasSurvivorBranch: true, phase: "pre-planning", expected: "continue" },
85
+ { hasSurvivorBranch: false, phase: "complete", expected: "showSmartEntry" },
86
+ ];
87
+
88
+ for (const { hasSurvivorBranch, phase, expected } of scenarios) {
89
+ let result: string;
90
+ if (hasSurvivorBranch && phase === "complete") {
91
+ result = "finalize";
92
+ } else if (hasSurvivorBranch && phase === "needs-discussion") {
93
+ result = "discuss";
94
+ } else if (!hasSurvivorBranch && (!phase || phase === "complete")) {
95
+ result = "showSmartEntry";
96
+ } else {
97
+ result = "continue";
98
+ }
99
+
100
+ assertEq(
101
+ result,
102
+ expected,
103
+ `hasSurvivorBranch=${hasSurvivorBranch}, phase=${phase} -> expected ${expected}, got ${result}`,
104
+ );
105
+ }
106
+ }
107
+
108
+ report();
@@ -0,0 +1,49 @@
1
+ /**
2
+ * terminated-transient.test.ts — Regression test for #2309.
3
+ *
4
+ * classifyProviderError should treat 'terminated' errors (process killed,
5
+ * connection reset) as transient with auto-resume, not permanent.
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { classifyProviderError } from "../provider-error-pause.ts";
11
+
12
+ test("#2309: 'terminated' errors should be classified as transient", () => {
13
+ const result = classifyProviderError("terminated");
14
+ assert.equal(result.isTransient, true, "'terminated' should be transient");
15
+ assert.equal(result.isRateLimit, false, "'terminated' is not a rate limit");
16
+ assert.ok(result.suggestedDelayMs > 0, "'terminated' should have a retry delay");
17
+ });
18
+
19
+ test("#2309: 'connection reset' errors should be classified as transient", () => {
20
+ const result = classifyProviderError("connection reset by peer");
21
+ assert.equal(result.isTransient, true, "'connection reset' should be transient");
22
+ });
23
+
24
+ test("#2309: 'other side closed' errors should be classified as transient", () => {
25
+ const result = classifyProviderError("other side closed the connection");
26
+ assert.equal(result.isTransient, true, "'other side closed' should be transient");
27
+ });
28
+
29
+ test("#2309: 'fetch failed' errors should be classified as transient", () => {
30
+ const result = classifyProviderError("fetch failed: network error");
31
+ assert.equal(result.isTransient, true, "'fetch failed' should be transient");
32
+ });
33
+
34
+ test("#2309: 'connection refused' errors should be classified as transient", () => {
35
+ const result = classifyProviderError("ECONNREFUSED: connection refused");
36
+ assert.equal(result.isTransient, true, "'connection refused' should be transient");
37
+ });
38
+
39
+ test("#2309: permanent errors are still permanent", () => {
40
+ const authResult = classifyProviderError("unauthorized: invalid API key");
41
+ assert.equal(authResult.isTransient, false, "auth errors should stay permanent");
42
+ assert.equal(authResult.suggestedDelayMs, 0, "permanent errors have no delay");
43
+ });
44
+
45
+ test("#2309: rate limits are still transient", () => {
46
+ const rlResult = classifyProviderError("rate limit exceeded (429)");
47
+ assert.equal(rlResult.isTransient, true, "rate limits are still transient");
48
+ assert.equal(rlResult.isRateLimit, true, "rate limits are flagged as rate limits");
49
+ });
@@ -34,6 +34,7 @@ const RENAME_MAP: Array<{ canonical: string; alias: string }> = [
34
34
  { canonical: "gsd_replan_slice", alias: "gsd_slice_replan" },
35
35
  { canonical: "gsd_reassess_roadmap", alias: "gsd_roadmap_reassess" },
36
36
  { canonical: "gsd_complete_milestone", alias: "gsd_milestone_complete" },
37
+ { canonical: "gsd_validate_milestone", alias: "gsd_milestone_validate" },
37
38
  ];
38
39
 
39
40
  // ─── Registration count ──────────────────────────────────────────────────────
@@ -43,7 +44,7 @@ console.log('\n── Tool naming: registration count ──');
43
44
  const pi = makeMockPi();
44
45
  registerDbTools(pi);
45
46
 
46
- assert.deepStrictEqual(pi.tools.length, 24, 'Should register exactly 24 tools (12 canonical + 12 aliases)');
47
+ assert.deepStrictEqual(pi.tools.length, 26, 'Should register exactly 26 tools (13 canonical + 13 aliases)');
47
48
 
48
49
  // ─── Both names exist for each pair ──────────────────────────────────────────
49
50
 
@@ -0,0 +1,65 @@
1
+ /**
2
+ * worktree-submodule-safety.test.ts — #2337
3
+ *
4
+ * Worktree teardown (removeWorktree) uses --force which destroys
5
+ * uncommitted changes in submodule directories. This test verifies
6
+ * that the removal logic detects submodules and preserves their state.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { createTestContext } from "./test-helpers.ts";
12
+
13
+ const { assertTrue, report } = createTestContext();
14
+
15
+ const srcPath = join(import.meta.dirname, "..", "worktree-manager.ts");
16
+ const src = readFileSync(srcPath, "utf-8");
17
+
18
+ console.log("\n=== #2337: Worktree teardown preserves submodule state ===");
19
+
20
+ // ── Test 1: removeWorktree function exists ──────────────────────────────
21
+
22
+ const removeWorktreeIdx = src.indexOf("export function removeWorktree");
23
+ assertTrue(removeWorktreeIdx > 0, "worktree-manager.ts exports removeWorktree");
24
+
25
+ const fnBody = src.slice(removeWorktreeIdx, removeWorktreeIdx + 3000);
26
+
27
+ // ── Test 2: The function checks for submodules before force removal ─────
28
+
29
+ const checksSubmodules =
30
+ fnBody.includes("submodule") ||
31
+ fnBody.includes(".gitmodules");
32
+
33
+ assertTrue(
34
+ checksSubmodules,
35
+ "removeWorktree checks for submodules before force removal (#2337)",
36
+ );
37
+
38
+ // ── Test 3: Submodule changes are stashed or warned about ───────────────
39
+
40
+ const preservesSubmoduleState =
41
+ fnBody.includes("stash") ||
42
+ fnBody.includes("uncommitted") ||
43
+ fnBody.includes("dirty") ||
44
+ fnBody.includes("submodule") && (fnBody.includes("warn") || fnBody.includes("preserv"));
45
+
46
+ assertTrue(
47
+ preservesSubmoduleState,
48
+ "removeWorktree preserves or warns about submodule uncommitted changes (#2337)",
49
+ );
50
+
51
+ // ── Test 4: Force removal is skipped when submodules have changes ───────
52
+
53
+ // The key fix: when submodules have dirty state, we should NOT use force
54
+ // removal. Instead, use non-force first and fall back to force only after
55
+ // submodule state is preserved.
56
+ const hasConditionalForce =
57
+ fnBody.includes("submodule") &&
58
+ (fnBody.includes("force") || fnBody.includes("--force"));
59
+
60
+ assertTrue(
61
+ hasConditionalForce,
62
+ "removeWorktree has conditional force logic around submodules (#2337)",
63
+ );
64
+
65
+ report();
@@ -20,6 +20,7 @@ export interface PlanSliceTaskInput {
20
20
  inputs: string[];
21
21
  expectedOutput: string[];
22
22
  observabilityImpact?: string;
23
+ fullPlanMd?: string;
23
24
  }
24
25
 
25
26
  export interface PlanSliceParams {
@@ -167,6 +168,7 @@ export async function handlePlanSlice(
167
168
  inputs: task.inputs,
168
169
  expectedOutput: task.expectedOutput,
169
170
  observabilityImpact: task.observabilityImpact ?? "",
171
+ fullPlanMd: task.fullPlanMd,
170
172
  });
171
173
  }
172
174
  });
@@ -15,6 +15,7 @@ export interface PlanTaskParams {
15
15
  inputs: string[];
16
16
  expectedOutput: string[];
17
17
  observabilityImpact?: string;
18
+ fullPlanMd?: string;
18
19
  }
19
20
 
20
21
  export interface PlanTaskResult {
@@ -94,6 +95,7 @@ export async function handlePlanTask(
94
95
  inputs: params.inputs,
95
96
  expectedOutput: params.expectedOutput,
96
97
  observabilityImpact: params.observabilityImpact ?? "",
98
+ fullPlanMd: params.fullPlanMd,
97
99
  });
98
100
  });
99
101
  } catch (err) {
@@ -21,6 +21,7 @@ export interface ReplanSliceTaskInput {
21
21
  verify: string;
22
22
  inputs: string[];
23
23
  expectedOutput: string[];
24
+ fullPlanMd?: string;
24
25
  }
25
26
 
26
27
  export interface ReplanSliceParams {
@@ -136,6 +137,7 @@ export async function handleReplanSlice(
136
137
  verify: updatedTask.verify || "",
137
138
  inputs: updatedTask.inputs || [],
138
139
  expectedOutput: updatedTask.expectedOutput || [],
140
+ fullPlanMd: updatedTask.fullPlanMd,
139
141
  });
140
142
  } else {
141
143
  // Insert new task then set planning fields
@@ -154,6 +156,7 @@ export async function handleReplanSlice(
154
156
  verify: updatedTask.verify || "",
155
157
  inputs: updatedTask.inputs || [],
156
158
  expectedOutput: updatedTask.expectedOutput || [],
159
+ fullPlanMd: updatedTask.fullPlanMd,
157
160
  });
158
161
  }
159
162
  }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * validate-milestone handler — the core operation behind gsd_validate_milestone.
3
+ *
4
+ * Persists milestone validation results to the assessments table,
5
+ * renders VALIDATION.md to disk, and invalidates caches.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+
10
+ import {
11
+ transaction,
12
+ _getAdapter,
13
+ } from "../gsd-db.js";
14
+ import { resolveMilestonePath, clearPathCache } from "../paths.js";
15
+ import { saveFile, clearParseCache } from "../files.js";
16
+ import { invalidateStateCache } from "../state.js";
17
+
18
+ export interface ValidateMilestoneParams {
19
+ milestoneId: string;
20
+ verdict: "pass" | "needs-attention" | "needs-remediation";
21
+ remediationRound: number;
22
+ successCriteriaChecklist: string;
23
+ sliceDeliveryAudit: string;
24
+ crossSliceIntegration: string;
25
+ requirementCoverage: string;
26
+ verdictRationale: string;
27
+ remediationPlan?: string;
28
+ }
29
+
30
+ export interface ValidateMilestoneResult {
31
+ milestoneId: string;
32
+ verdict: string;
33
+ validationPath: string;
34
+ }
35
+
36
+ function renderValidationMarkdown(params: ValidateMilestoneParams): string {
37
+ let md = `---
38
+ verdict: ${params.verdict}
39
+ remediation_round: ${params.remediationRound}
40
+ ---
41
+
42
+ # Milestone Validation: ${params.milestoneId}
43
+
44
+ ## Success Criteria Checklist
45
+ ${params.successCriteriaChecklist}
46
+
47
+ ## Slice Delivery Audit
48
+ ${params.sliceDeliveryAudit}
49
+
50
+ ## Cross-Slice Integration
51
+ ${params.crossSliceIntegration}
52
+
53
+ ## Requirement Coverage
54
+ ${params.requirementCoverage}
55
+
56
+ ## Verdict Rationale
57
+ ${params.verdictRationale}
58
+ `;
59
+
60
+ if (params.verdict === "needs-remediation" && params.remediationPlan) {
61
+ md += `\n## Remediation Plan\n${params.remediationPlan}\n`;
62
+ }
63
+
64
+ return md;
65
+ }
66
+
67
+ export async function handleValidateMilestone(
68
+ params: ValidateMilestoneParams,
69
+ basePath: string,
70
+ ): Promise<ValidateMilestoneResult | { error: string }> {
71
+ if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
72
+ return { error: "milestoneId is required and must be a non-empty string" };
73
+ }
74
+ const validVerdicts = ["pass", "needs-attention", "needs-remediation"];
75
+ if (!validVerdicts.includes(params.verdict)) {
76
+ return { error: `verdict must be one of: ${validVerdicts.join(", ")}` };
77
+ }
78
+
79
+ // ── Filesystem render ──────────────────────────────────────────────────
80
+ const validationMd = renderValidationMarkdown(params);
81
+
82
+ let validationPath: string;
83
+ const milestoneDir = resolveMilestonePath(basePath, params.milestoneId);
84
+ if (milestoneDir) {
85
+ validationPath = join(milestoneDir, `${params.milestoneId}-VALIDATION.md`);
86
+ } else {
87
+ const gsdDir = join(basePath, ".gsd");
88
+ const manualDir = join(gsdDir, "milestones", params.milestoneId);
89
+ validationPath = join(manualDir, `${params.milestoneId}-VALIDATION.md`);
90
+ }
91
+
92
+ try {
93
+ await saveFile(validationPath, validationMd);
94
+ } catch (renderErr) {
95
+ process.stderr.write(
96
+ `gsd-db: validate_milestone — disk render failed: ${(renderErr as Error).message}\n`,
97
+ );
98
+ return { error: `disk render failed: ${(renderErr as Error).message}` };
99
+ }
100
+
101
+ // ── DB write — store in assessments table ──────────────────────────────
102
+ const validatedAt = new Date().toISOString();
103
+
104
+ transaction(() => {
105
+ const adapter = _getAdapter()!;
106
+ adapter.prepare(
107
+ `INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at)
108
+ VALUES (:path, :mid, NULL, NULL, :verdict, 'milestone-validation', :content, :created_at)`,
109
+ ).run({
110
+ ":path": validationPath,
111
+ ":mid": params.milestoneId,
112
+ ":verdict": params.verdict,
113
+ ":content": validationMd,
114
+ ":created_at": validatedAt,
115
+ });
116
+ });
117
+
118
+ invalidateStateCache();
119
+ clearPathCache();
120
+ clearParseCache();
121
+
122
+ return {
123
+ milestoneId: params.milestoneId,
124
+ verdict: params.verdict,
125
+ validationPath,
126
+ };
127
+ }