@opengsd/gsd-pi 1.1.1-dev.9f86580 → 1.1.1-dev.b2556262

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 (261) hide show
  1. package/dist/headless-recover.js +56 -1
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/browser-tools/index.js +39 -22
  4. package/dist/resources/extensions/browser-tools/state.js +12 -0
  5. package/dist/resources/extensions/browser-tools/tools/session.js +3 -2
  6. package/dist/resources/extensions/browser-tools/utils.js +3 -3
  7. package/dist/resources/extensions/gsd/auto/loop.js +4 -2
  8. package/dist/resources/extensions/gsd/auto/phases.js +43 -10
  9. package/dist/resources/extensions/gsd/auto/session.js +20 -1
  10. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +1 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +72 -12
  12. package/dist/resources/extensions/gsd/auto-model-selection.js +128 -9
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -2
  14. package/dist/resources/extensions/gsd/auto-prompts.js +24 -19
  15. package/dist/resources/extensions/gsd/auto-recovery.js +4 -2
  16. package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
  17. package/dist/resources/extensions/gsd/auto-start.js +1 -1
  18. package/dist/resources/extensions/gsd/auto.js +14 -11
  19. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
  20. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +172 -65
  21. package/dist/resources/extensions/gsd/closeout-wizard.js +32 -9
  22. package/dist/resources/extensions/gsd/commands/handlers/ops.js +2 -9
  23. package/dist/resources/extensions/gsd/commands-maintenance.js +93 -15
  24. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +2 -2
  25. package/dist/resources/extensions/gsd/db-writer.js +35 -0
  26. package/dist/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  27. package/dist/resources/extensions/gsd/gsd-db.js +480 -172
  28. package/dist/resources/extensions/gsd/markdown-renderer.js +37 -53
  29. package/dist/resources/extensions/gsd/md-importer.js +38 -3
  30. package/dist/resources/extensions/gsd/migration-auto-check.js +126 -31
  31. package/dist/resources/extensions/gsd/parsers-legacy.js +23 -0
  32. package/dist/resources/extensions/gsd/planning-path-scope.js +22 -4
  33. package/dist/resources/extensions/gsd/pre-execution-checks.js +10 -2
  34. package/dist/resources/extensions/gsd/preferences-models.js +110 -43
  35. package/dist/resources/extensions/gsd/preferences-types.js +13 -0
  36. package/dist/resources/extensions/gsd/preferences-validation.js +68 -3
  37. package/dist/resources/extensions/gsd/preferences.js +4 -1
  38. package/dist/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/roadmap-slices.js +5 -1
  42. package/dist/resources/extensions/gsd/safety/content-validator.js +6 -4
  43. package/dist/resources/extensions/gsd/source-observations.js +306 -0
  44. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +15 -8
  45. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +33 -5
  46. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +34 -13
  47. package/dist/resources/extensions/gsd/state-reconciliation/index.js +39 -14
  48. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +4 -4
  49. package/dist/resources/extensions/gsd/state.js +7 -3
  50. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  51. package/dist/resources/extensions/gsd/tool-presentation-plan.js +1 -9
  52. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -6
  53. package/dist/resources/extensions/gsd/tools/plan-slice.js +42 -11
  54. package/dist/resources/extensions/gsd/tools/plan-task.js +7 -1
  55. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +57 -429
  56. package/dist/resources/extensions/gsd/uat-policy.js +130 -0
  57. package/dist/resources/extensions/gsd/uat-run.js +414 -0
  58. package/dist/resources/extensions/gsd/unit-context-manifest.js +3 -4
  59. package/dist/resources/extensions/gsd/verdict-parser.js +3 -8
  60. package/dist/resources/extensions/gsd/workflow-manifest.js +132 -5
  61. package/dist/resources/extensions/gsd/workflow-projections.js +8 -0
  62. package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -17
  63. package/dist/resources/extensions/subagent/agents.js +1 -0
  64. package/dist/resources/extensions/subagent/index.js +27 -12
  65. package/dist/resources/extensions/subagent/launch.js +7 -2
  66. package/dist/web/standalone/.next/BUILD_ID +1 -1
  67. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  68. package/dist/web/standalone/.next/build-manifest.json +2 -2
  69. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  70. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.html +1 -1
  87. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  94. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  95. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  97. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  98. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  99. package/dist/web/standalone/node_modules/@gsd/native/dist/native.js +22 -0
  100. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  101. package/package.json +4 -4
  102. package/packages/cloud-mcp-gateway/package.json +2 -2
  103. package/packages/contracts/package.json +1 -1
  104. package/packages/daemon/package.json +4 -4
  105. package/packages/gsd-agent-core/package.json +5 -5
  106. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js +21 -23
  108. package/packages/gsd-agent-modes/dist/modes/interactive/components/assistant-message.js.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +3 -0
  110. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  111. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +25 -0
  112. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  113. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +1 -0
  114. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  115. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +66 -12
  116. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  117. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  118. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +18 -11
  119. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  120. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.d.ts.map +1 -1
  121. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js +16 -0
  122. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-chat-render.js.map +1 -1
  123. package/packages/gsd-agent-modes/package.json +7 -7
  124. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  125. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  126. package/packages/mcp-server/package.json +3 -3
  127. package/packages/native/dist/native.js +22 -0
  128. package/packages/native/package.json +1 -1
  129. package/packages/pi-agent-core/package.json +1 -1
  130. package/packages/pi-ai/dist/image-models.generated.d.ts +30 -0
  131. package/packages/pi-ai/dist/image-models.generated.d.ts.map +1 -1
  132. package/packages/pi-ai/dist/image-models.generated.js +30 -0
  133. package/packages/pi-ai/dist/image-models.generated.js.map +1 -1
  134. package/packages/pi-ai/dist/models.generated.d.ts +23 -17
  135. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  136. package/packages/pi-ai/dist/models.generated.js +25 -24
  137. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  138. package/packages/pi-ai/package.json +1 -1
  139. package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
  140. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/theme/themes.js +1 -1
  142. package/packages/pi-coding-agent/dist/theme/themes.js.map +1 -1
  143. package/packages/pi-coding-agent/package.json +7 -7
  144. package/packages/pi-tui/dist/utils.d.ts +11 -0
  145. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  146. package/packages/pi-tui/dist/utils.js +119 -6
  147. package/packages/pi-tui/dist/utils.js.map +1 -1
  148. package/packages/pi-tui/package.json +2 -1
  149. package/packages/rpc-client/package.json +2 -2
  150. package/pkg/dist/theme/themes.js +1 -1
  151. package/pkg/dist/theme/themes.js.map +1 -1
  152. package/pkg/package.json +1 -1
  153. package/src/resources/extensions/browser-tools/index.ts +39 -22
  154. package/src/resources/extensions/browser-tools/state.ts +13 -0
  155. package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +57 -0
  156. package/src/resources/extensions/browser-tools/tools/session.ts +4 -2
  157. package/src/resources/extensions/browser-tools/utils.ts +3 -3
  158. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  159. package/src/resources/extensions/gsd/auto/loop.ts +4 -2
  160. package/src/resources/extensions/gsd/auto/phases.ts +42 -10
  161. package/src/resources/extensions/gsd/auto/session.ts +22 -1
  162. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +1 -0
  163. package/src/resources/extensions/gsd/auto-dispatch.ts +85 -12
  164. package/src/resources/extensions/gsd/auto-model-selection.ts +164 -12
  165. package/src/resources/extensions/gsd/auto-post-unit.ts +20 -2
  166. package/src/resources/extensions/gsd/auto-prompts.ts +23 -20
  167. package/src/resources/extensions/gsd/auto-recovery.ts +22 -3
  168. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  169. package/src/resources/extensions/gsd/auto-start.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +13 -10
  171. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
  172. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +225 -72
  173. package/src/resources/extensions/gsd/closeout-wizard.ts +47 -13
  174. package/src/resources/extensions/gsd/commands/handlers/ops.ts +2 -17
  175. package/src/resources/extensions/gsd/commands-maintenance.ts +124 -13
  176. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -2
  177. package/src/resources/extensions/gsd/db-writer.ts +38 -0
  178. package/src/resources/extensions/gsd/docs/preferences-reference.md +50 -1
  179. package/src/resources/extensions/gsd/gsd-db.ts +564 -186
  180. package/src/resources/extensions/gsd/markdown-renderer.ts +44 -66
  181. package/src/resources/extensions/gsd/md-importer.ts +49 -2
  182. package/src/resources/extensions/gsd/migration-auto-check.ts +154 -34
  183. package/src/resources/extensions/gsd/parsers-legacy.ts +20 -0
  184. package/src/resources/extensions/gsd/planning-path-scope.ts +22 -4
  185. package/src/resources/extensions/gsd/pre-execution-checks.ts +9 -2
  186. package/src/resources/extensions/gsd/preferences-models.ts +112 -43
  187. package/src/resources/extensions/gsd/preferences-types.ts +39 -0
  188. package/src/resources/extensions/gsd/preferences-validation.ts +76 -2
  189. package/src/resources/extensions/gsd/preferences.ts +5 -0
  190. package/src/resources/extensions/gsd/prompts/gate-evaluate.md +1 -1
  191. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  192. package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
  193. package/src/resources/extensions/gsd/roadmap-slices.ts +6 -1
  194. package/src/resources/extensions/gsd/safety/content-validator.ts +8 -5
  195. package/src/resources/extensions/gsd/source-observations.ts +402 -0
  196. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +20 -8
  197. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +44 -5
  198. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +39 -11
  199. package/src/resources/extensions/gsd/state-reconciliation/index.ts +45 -15
  200. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +4 -4
  201. package/src/resources/extensions/gsd/state.ts +7 -4
  202. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +15 -0
  203. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +299 -1
  204. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +32 -0
  205. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +75 -3
  206. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +22 -1
  207. package/src/resources/extensions/gsd/tests/before-provider-context-management.test.ts +145 -0
  208. package/src/resources/extensions/gsd/tests/closeout-wizard.test.ts +44 -0
  209. package/src/resources/extensions/gsd/tests/commands-dispatcher-unmerged-milestone.test.ts +26 -1
  210. package/src/resources/extensions/gsd/tests/content-validator.test.ts +74 -0
  211. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +16 -2
  212. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +1 -11
  213. package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +64 -0
  214. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +15 -0
  215. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +62 -1
  216. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +15 -0
  217. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +42 -0
  218. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +99 -0
  219. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +99 -2
  220. package/src/resources/extensions/gsd/tests/plan-task.test.ts +19 -0
  221. package/src/resources/extensions/gsd/tests/preferences.test.ts +14 -0
  222. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +1 -0
  223. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +9 -0
  224. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +101 -1
  225. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +2 -2
  226. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +8 -0
  227. package/src/resources/extensions/gsd/tests/schema-v21-sequence.test.ts +5 -3
  228. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +162 -18
  229. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +8 -0
  230. package/src/resources/extensions/gsd/tests/source-observations.test.ts +275 -0
  231. package/src/resources/extensions/gsd/tests/stale-queued-milestone.test.ts +43 -0
  232. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +76 -21
  233. package/src/resources/extensions/gsd/tests/thinking-level-resolution.test.ts +203 -0
  234. package/src/resources/extensions/gsd/tests/uat-policy.test.ts +170 -0
  235. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +7 -1
  236. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  237. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +306 -1
  238. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +73 -6
  239. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +511 -1
  240. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +44 -0
  241. package/src/resources/extensions/gsd/tool-contract.ts +28 -0
  242. package/src/resources/extensions/gsd/tool-presentation-plan.ts +1 -11
  243. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -6
  244. package/src/resources/extensions/gsd/tools/plan-slice.ts +54 -12
  245. package/src/resources/extensions/gsd/tools/plan-task.ts +8 -1
  246. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +66 -526
  247. package/src/resources/extensions/gsd/types.ts +1 -0
  248. package/src/resources/extensions/gsd/uat-policy.ts +191 -0
  249. package/src/resources/extensions/gsd/uat-run.ts +550 -0
  250. package/src/resources/extensions/gsd/unit-context-manifest.ts +3 -4
  251. package/src/resources/extensions/gsd/verdict-parser.ts +3 -10
  252. package/src/resources/extensions/gsd/workflow-manifest.ts +193 -7
  253. package/src/resources/extensions/gsd/workflow-projections.ts +9 -0
  254. package/src/resources/extensions/gsd/worktree-state-projection.ts +22 -22
  255. package/src/resources/extensions/shared/tests/format-utils.test.ts +8 -3
  256. package/src/resources/extensions/subagent/agents.ts +4 -0
  257. package/src/resources/extensions/subagent/index.ts +28 -3
  258. package/src/resources/extensions/subagent/launch.ts +8 -0
  259. package/src/resources/extensions/subagent/tests/model-override.test.ts +31 -0
  260. /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_buildManifest.js +0 -0
  261. /package/dist/web/standalone/.next/static/{zzYMrKpPGfRQRxSFO32Jr → tJOKQbQRO-9MiFDO8DIDS}/_ssgManifest.js +0 -0
@@ -207,6 +207,7 @@ test("runFinalize merges a verified complete-milestone immediately and only once
207
207
  const startedAt = Date.now();
208
208
  let lifecycleMergeCalls = 0;
209
209
  let resolverMergeCalls = 0;
210
+ const stopAutoCalls: Array<{ reason?: string; options?: unknown }> = [];
210
211
  s.basePath = base;
211
212
  s.originalBasePath = base;
212
213
  s.currentMilestoneId = "M001";
@@ -219,6 +220,9 @@ test("runFinalize merges a verified complete-milestone immediately and only once
219
220
  const result = await runFinalizeWithDeps(s, {
220
221
  preflightCleanRoot: () => ({ stashPushed: false }),
221
222
  postflightPopStash: () => ({ needsManualRecovery: false }),
223
+ stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string, options?: unknown) => {
224
+ stopAutoCalls.push({ reason, options });
225
+ },
222
226
  resolver: {
223
227
  mergeAndExit() {
224
228
  resolverMergeCalls++;
@@ -232,10 +236,19 @@ test("runFinalize merges a verified complete-milestone immediately and only once
232
236
  },
233
237
  });
234
238
 
235
- assert.equal(result.action, "next");
239
+ assert.equal(result.action, "break");
240
+ assert.equal(result.reason, "milestone-complete");
236
241
  assert.equal(lifecycleMergeCalls, 1);
237
242
  assert.equal(resolverMergeCalls, 0);
238
243
  assert.equal(s.milestoneMergedInPhases, true);
244
+ assert.equal(stopAutoCalls.length, 1);
245
+ assert.equal(stopAutoCalls[0]?.reason, "Milestone M001 complete");
246
+ assert.deepEqual(stopAutoCalls[0]?.options, {
247
+ completionWidget: {
248
+ milestoneId: "M001",
249
+ milestoneTitle: "Milestone",
250
+ },
251
+ });
239
252
 
240
253
  s.currentUnit = {
241
254
  type: "complete-milestone",
@@ -245,6 +258,9 @@ test("runFinalize merges a verified complete-milestone immediately and only once
245
258
  const second = await runFinalizeWithDeps(s, {
246
259
  preflightCleanRoot: () => ({ stashPushed: false }),
247
260
  postflightPopStash: () => ({ needsManualRecovery: false }),
261
+ stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string, options?: unknown) => {
262
+ stopAutoCalls.push({ reason, options });
263
+ },
248
264
  resolver: {
249
265
  mergeAndExit() {
250
266
  resolverMergeCalls++;
@@ -258,9 +274,11 @@ test("runFinalize merges a verified complete-milestone immediately and only once
258
274
  },
259
275
  });
260
276
 
261
- assert.equal(second.action, "next");
277
+ assert.equal(second.action, "break");
278
+ assert.equal(second.reason, "milestone-complete");
262
279
  assert.equal(lifecycleMergeCalls, 1);
263
280
  assert.equal(resolverMergeCalls, 0);
281
+ assert.equal(stopAutoCalls.length, 2);
264
282
  });
265
283
 
266
284
  test("runFinalize does not render next-phase handoff for complete-milestone", async (t) => {
@@ -302,7 +320,7 @@ test("runFinalize does not render next-phase handoff for complete-milestone", as
302
320
  },
303
321
  );
304
322
 
305
- assert.equal(result.action, "next");
323
+ assert.equal(result.action, "break");
306
324
  assert.equal(
307
325
  widgetCalls.some(([key]) => key === "gsd-outcome"),
308
326
  false,
@@ -310,6 +328,60 @@ test("runFinalize does not render next-phase handoff for complete-milestone", as
310
328
  );
311
329
  });
312
330
 
331
+ test("runFinalize clears gsd-step and gsd-progress before stopAuto on complete-milestone", async (t) => {
332
+ const base = mkdtempSync(join(tmpdir(), "gsd-finalize-stale-widget-"));
333
+ t.after(() => {
334
+ rmSync(base, { recursive: true, force: true });
335
+ });
336
+
337
+ const s = new AutoSession();
338
+ s.basePath = base;
339
+ s.originalBasePath = base;
340
+ s.currentMilestoneId = "M001";
341
+ s.currentUnit = {
342
+ type: "complete-milestone",
343
+ id: "M001",
344
+ startedAt: Date.now(),
345
+ };
346
+
347
+ const statusCalls: Array<[string, unknown]> = [];
348
+ const widgetCalls: Array<[string, unknown]> = [];
349
+
350
+ await runFinalizeWithDeps(
351
+ s,
352
+ {
353
+ preflightCleanRoot: () => ({ stashPushed: false }),
354
+ postflightPopStash: () => ({ needsManualRecovery: false }),
355
+ lifecycle: {
356
+ exitMilestone() {
357
+ return { ok: true, merged: true, codeFilesChanged: false };
358
+ },
359
+ },
360
+ },
361
+ {
362
+ hasUI: true,
363
+ ui: {
364
+ notify() {},
365
+ setStatus(key: string, value: unknown) {
366
+ statusCalls.push([key, value]);
367
+ },
368
+ setWidget(key: string, value: unknown) {
369
+ widgetCalls.push([key, value]);
370
+ },
371
+ },
372
+ },
373
+ );
374
+
375
+ assert.ok(
376
+ statusCalls.some(([key, val]) => key === "gsd-step" && val === undefined),
377
+ "gsd-step status should be cleared before stopAuto",
378
+ );
379
+ assert.ok(
380
+ widgetCalls.some(([key, val]) => key === "gsd-progress" && val === undefined),
381
+ "gsd-progress widget should be cleared before stopAuto",
382
+ );
383
+ });
384
+
313
385
  test("runFinalize stops before merge when an isolated unit leaks app files into project root", async (t) => {
314
386
  const root = mkdtempSync(join(tmpdir(), "gsd-root-leak-root-"));
315
387
  const worktree = join(root, ".gsd", "worktrees", "M001");
@@ -9,7 +9,7 @@ import { randomUUID } from "node:crypto";
9
9
 
10
10
  import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, diagnoseWorktreeIntegrityFailure, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact, writeReactiveExecuteBlocker } from "../auto-recovery.ts";
11
11
  import { resolveMilestoneFile } from "../paths.ts";
12
- import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, insertAssessment, getMilestone, getMilestoneCommitAttributionShas, getTask } from "../gsd-db.ts";
12
+ import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, insertAssessment, getMilestone, getMilestoneCommitAttributionShas, getTask, saveGateResult } from "../gsd-db.ts";
13
13
  import { readEvents } from "../workflow-events.ts";
14
14
  import { clearParseCache } from "../files.ts";
15
15
  import { parseRoadmap } from "../parsers-legacy.ts";
@@ -1613,6 +1613,27 @@ test("verifyExpectedArtifact checks pending gate-evaluate artifacts without ESM
1613
1613
  assert.equal(verified, false, "pending gates should keep gate-evaluate unverified");
1614
1614
  });
1615
1615
 
1616
+ test("verifyExpectedArtifact fails closed for gate-evaluate when the DB is unavailable", () => {
1617
+ const base = makeTmpProject();
1618
+ closeDatabase();
1619
+
1620
+ const verified = verifyExpectedArtifact("gate-evaluate", "M001/S01/gates+Q3", base);
1621
+
1622
+ assert.equal(verified, false, "gate-evaluate must verify against the DB-backed gate rows");
1623
+ });
1624
+
1625
+ test("verifyExpectedArtifact ignores complete-slice gates in stale gate-evaluate unit ids", () => {
1626
+ const base = makeTmpProject();
1627
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
1628
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
1629
+ saveGateResult({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", verdict: "pass", rationale: "OK", findings: "" });
1630
+ saveGateResult({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", verdict: "pass", rationale: "OK", findings: "" });
1631
+
1632
+ const verified = verifyExpectedArtifact("gate-evaluate", "M001/S01/gates+Q3,Q4,Q8", base);
1633
+
1634
+ assert.equal(verified, true, "pending Q8 belongs to complete-slice and must not keep gate-evaluate unverified");
1635
+ });
1636
+
1616
1637
  // ─── #4414 regressions ────────────────────────────────────────────────────────
1617
1638
 
1618
1639
  test("#4414: writeBlockerPlaceholder invalidates path cache so dispatch guard sees file", () => {
@@ -0,0 +1,145 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ import { _setAutoActiveForTest } from "../auto.ts";
8
+ import { autoSession } from "../auto-runtime-state.js";
9
+ import { registerHooks } from "../bootstrap/register-hooks.ts";
10
+
11
+ type HookHandler = (event: any, ctx?: any) => Promise<any> | any;
12
+
13
+ function createHookHandlers(): Map<string, HookHandler[]> {
14
+ const handlers = new Map<string, HookHandler[]>();
15
+ const pi = {
16
+ on(event: string, handler: HookHandler) {
17
+ const existing = handlers.get(event) ?? [];
18
+ existing.push(handler);
19
+ handlers.set(event, existing);
20
+ },
21
+ };
22
+
23
+ registerHooks(pi as any, []);
24
+ return handlers;
25
+ }
26
+
27
+ function requireHook(handlers: Map<string, HookHandler[]>, event: string): HookHandler {
28
+ const handler = handlers.get(event)?.[0];
29
+ assert.ok(handler, `${event} hook should be registered`);
30
+ return handler;
31
+ }
32
+
33
+ test("before_provider_request truncates tool results outside auto-mode", async (t) => {
34
+ const dir = mkdtempSync(join(tmpdir(), "gsd-before-provider-context-"));
35
+ const gsdHome = join(dir, "home");
36
+ const project = join(dir, "project");
37
+ const previousCwd = process.cwd();
38
+ const previousGsdHome = process.env.GSD_HOME;
39
+
40
+ mkdirSync(join(project, ".gsd"), { recursive: true });
41
+ mkdirSync(gsdHome, { recursive: true });
42
+ writeFileSync(
43
+ join(project, ".gsd", "PREFERENCES.md"),
44
+ [
45
+ "---",
46
+ "version: 1",
47
+ "context_management:",
48
+ " tool_result_max_chars: 200",
49
+ " observation_mask_turns: 1",
50
+ "---",
51
+ "",
52
+ ].join("\n"),
53
+ "utf-8",
54
+ );
55
+
56
+ process.env.GSD_HOME = gsdHome;
57
+ process.chdir(project);
58
+ _setAutoActiveForTest(false);
59
+
60
+ t.after(() => {
61
+ _setAutoActiveForTest(false);
62
+ process.chdir(previousCwd);
63
+ if (previousGsdHome === undefined) delete process.env.GSD_HOME;
64
+ else process.env.GSD_HOME = previousGsdHome;
65
+ rmSync(dir, { recursive: true, force: true });
66
+ });
67
+
68
+ const beforeProviderRequest = requireHook(createHookHandlers(), "before_provider_request");
69
+ const messageText = "m".repeat(250);
70
+ const responsesOutput = "r".repeat(250);
71
+ const payload = {
72
+ messages: [
73
+ { role: "user", content: [{ type: "text", text: "keep me" }] },
74
+ {
75
+ role: "toolResult",
76
+ toolCallId: "toolu_test",
77
+ toolName: "Read",
78
+ isError: false,
79
+ content: [{ type: "text", text: messageText }],
80
+ },
81
+ ],
82
+ input: [
83
+ { role: "user", content: [{ type: "input_text", text: "keep me" }] },
84
+ { type: "function_call_output", call_id: "call_test", output: responsesOutput },
85
+ ],
86
+ };
87
+
88
+ await beforeProviderRequest({ payload });
89
+
90
+ const truncatedMessage = (payload.messages[1]?.content as Array<{ text?: string }>)[0]?.text ?? "";
91
+ const truncatedResponsesOutput = String(payload.input[1]?.output ?? "");
92
+
93
+ assert.match(truncatedMessage, /\[truncated\]/);
94
+ assert.match(truncatedResponsesOutput, /\[truncated\]/);
95
+ assert.ok(truncatedMessage.length < messageText.length);
96
+ assert.ok(truncatedResponsesOutput.length < responsesOutput.length);
97
+ assert.doesNotMatch(truncatedMessage, /result masked/);
98
+ assert.doesNotMatch(truncatedResponsesOutput, /result masked/);
99
+ });
100
+
101
+ test("successful shell result clears source context before provider injection", async (t) => {
102
+ const dir = mkdtempSync(join(tmpdir(), "gsd-before-provider-source-"));
103
+ const project = join(dir, "project");
104
+ mkdirSync(project, { recursive: true });
105
+ writeFileSync(join(project, "app.ts"), "export const value = 'before';\n");
106
+
107
+ autoSession.reset();
108
+ autoSession.active = true;
109
+ autoSession.basePath = project;
110
+ autoSession.setCurrentUnit({
111
+ type: "execute-task",
112
+ id: "M001/S01/T01",
113
+ startedAt: 123,
114
+ workspaceRoot: project,
115
+ });
116
+ autoSession.sourceObservations.observeRead({ path: "app.ts" });
117
+
118
+ t.after(() => {
119
+ autoSession.reset();
120
+ rmSync(dir, { recursive: true, force: true });
121
+ });
122
+
123
+ assert.match(autoSession.sourceObservations.renderActiveBlock() ?? "", /before/);
124
+
125
+ const handlers = createHookHandlers();
126
+ const toolResult = requireHook(handlers, "tool_result");
127
+ const beforeProviderRequest = requireHook(handlers, "before_provider_request");
128
+
129
+ await toolResult({
130
+ toolCallId: "toolu_bash",
131
+ toolName: "bash",
132
+ input: { command: "printf after > app.ts" },
133
+ isError: false,
134
+ result: "ok",
135
+ }, { cwd: project });
136
+
137
+ const payload = {
138
+ messages: [{ role: "user", content: [{ type: "text", text: "continue" }] }],
139
+ };
140
+ await beforeProviderRequest({ payload });
141
+
142
+ assert.equal(autoSession.sourceObservations.renderActiveBlock(), null);
143
+ assert.equal(payload.messages.length, 1);
144
+ assert.doesNotMatch(payload.messages[0].content[0].text, /Source Context Block/);
145
+ });
@@ -8,6 +8,7 @@ import {
8
8
  buildCloseoutMenuActions,
9
9
  buildIdleMenuSummary,
10
10
  getPrimaryCloseoutRecommendation,
11
+ showMilestoneMergeCloseout,
11
12
  } from "../closeout-wizard.ts";
12
13
  import { buildGsdHomeModel } from "../gsd-command-home.ts";
13
14
  import type { GSDState } from "../types.ts";
@@ -119,3 +120,46 @@ test("/gsd home recommends merge milestone when closeout is pending", () => {
119
120
  assert.equal(merge?.recommended, true);
120
121
  assert.equal(merge?.enabled, true);
121
122
  });
123
+
124
+ test("milestone merge closeout clears stale timer controls and installs the closeout outcome", () => {
125
+ const statuses: Array<[string, string | undefined]> = [];
126
+ const widgets: Array<[string, unknown]> = [];
127
+
128
+ showMilestoneMergeCloseout({
129
+ hasUI: true,
130
+ ui: {
131
+ setStatus: (key: string, value: string | undefined) => {
132
+ statuses.push([key, value]);
133
+ },
134
+ setWidget: (key: string, value: unknown) => {
135
+ widgets.push([key, value]);
136
+ },
137
+ },
138
+ } as any, {
139
+ milestoneId: "M004",
140
+ branch: "milestone/M004",
141
+ integrationBranch: "main",
142
+ files: ["src/app.ts"],
143
+ dirtyOverlap: [],
144
+ });
145
+
146
+ assert.deepEqual(statuses, [
147
+ ["gsd-auto", undefined],
148
+ ["gsd-step", undefined],
149
+ ]);
150
+ assert.ok(
151
+ widgets.some(([key, value]) => key === "gsd-progress" && value === undefined),
152
+ "stale progress/timer widget should be cleared",
153
+ );
154
+ const outcome = widgets.find(([key]) => key === "gsd-outcome")?.[1];
155
+ assert.equal(typeof outcome, "function");
156
+
157
+ const component = (outcome as any)(
158
+ { requestRender() {} },
159
+ { fg: (_color: string, text: string) => text, bold: (text: string) => text },
160
+ );
161
+ const rendered = component.render(100).join("\n");
162
+ assert.match(rendered, /Milestone M004 merged/);
163
+ assert.match(rendered, /Review the closeout/);
164
+ assert.doesNotMatch(rendered, /\/gsd auto/);
165
+ });
@@ -40,6 +40,7 @@ function makeMockCtx(base: string): {
40
40
  return {
41
41
  ctx: {
42
42
  cwd: base,
43
+ hasUI: true,
43
44
  ui: {
44
45
  notify: (message: string, kind: string) => {
45
46
  calls.push({ message, kind });
@@ -217,7 +218,7 @@ test("dispatcher recovers a completed unmerged milestone through complete-milest
217
218
  const base = makeTempRepo("gsd-dispatch-unmerged-");
218
219
  try {
219
220
  seedRegisteredCompletedWorktree(base);
220
- const { ctx, calls } = makeMockCtx(base);
221
+ const { ctx, calls, widgets, statuses } = makeMockCtx(base);
221
222
  const { pi, messages } = makeMockPi();
222
223
 
223
224
  await handleGSDCommand("dispatch complete-milestone M008", ctx, pi);
@@ -231,6 +232,30 @@ test("dispatcher recovers a completed unmerged milestone through complete-milest
231
232
  calls.some((call) => call.message.includes("Milestone M008 merged to main")),
232
233
  "user sees merge recovery success",
233
234
  );
235
+ assert.ok(
236
+ calls.some((call) => call.message.includes("Closeout is complete")),
237
+ "merge recovery ends at a closeout-complete message",
238
+ );
239
+ assert.ok(
240
+ calls.every((call) => !call.message.includes("Run /gsd again when ready")),
241
+ "merge recovery must not send the user past the closeout endpoint",
242
+ );
243
+ assert.ok(
244
+ statuses.some(([key, value]) => key === "gsd-auto" && value === undefined),
245
+ "merge recovery clears stale auto status so the run timer stops",
246
+ );
247
+ assert.ok(
248
+ statuses.some(([key, value]) => key === "gsd-step" && value === undefined),
249
+ "merge recovery clears stale step status",
250
+ );
251
+ assert.ok(
252
+ widgets.some(([key, value]) => key === "gsd-progress" && value === undefined),
253
+ "merge recovery clears stale progress/timer controls",
254
+ );
255
+ assert.ok(
256
+ widgets.some(([key, value]) => key === "gsd-outcome" && typeof value === "function"),
257
+ "merge recovery installs a durable closeout outcome",
258
+ );
234
259
  assert.equal(readFileSync(join(base, "index.html"), "utf-8"), "<h1>M008</h1>\n");
235
260
  assert.equal(git(base, "branch", "--list", "milestone/M008"), "");
236
261
  } finally {
@@ -0,0 +1,74 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import { validateContent } from "../safety/content-validator.ts";
8
+
9
+ function makeTempFile(content: string): { base: string; path: string } {
10
+ const base = mkdtempSync(join(tmpdir(), "gsd-content-validator-"));
11
+ mkdirSync(base, { recursive: true });
12
+ const path = join(base, "artifact.md");
13
+ writeFileSync(path, content, "utf-8");
14
+ return { base, path };
15
+ }
16
+
17
+ test("validateContent marks empty milestone roadmaps as blocking", () => {
18
+ const { base, path } = makeTempFile([
19
+ "# M004: Empty roadmap",
20
+ "",
21
+ "## Slices",
22
+ "",
23
+ "_TBD_",
24
+ "",
25
+ ].join("\n"));
26
+
27
+ try {
28
+ const violations = validateContent("plan-milestone", path);
29
+ assert.deepEqual(violations, [{
30
+ severity: "error",
31
+ reason: "Milestone roadmap has 0 slice(s) — expected at least 1",
32
+ }]);
33
+ } finally {
34
+ rmSync(base, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ test("validateContent accepts checkbox milestone roadmap slices", () => {
39
+ const { base, path } = makeTempFile([
40
+ "# M004: Roadmap",
41
+ "",
42
+ "## Slices",
43
+ "",
44
+ "- [ ] **S01: Browser due dates** `risk:low` `depends:[]`",
45
+ " > After this: due dates are visible.",
46
+ "",
47
+ ].join("\n"));
48
+
49
+ try {
50
+ const violations = validateContent("plan-milestone", path);
51
+ assert.deepEqual(violations, []);
52
+ } finally {
53
+ rmSync(base, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ test("validateContent marks empty slice plans as blocking", () => {
58
+ const { base, path } = makeTempFile([
59
+ "# S01: Empty slice",
60
+ "",
61
+ "## Tasks",
62
+ "",
63
+ "_TBD_",
64
+ "",
65
+ ].join("\n"));
66
+
67
+ try {
68
+ const violations = validateContent("plan-slice", path);
69
+ assert.equal(violations[0]?.severity, "error");
70
+ assert.equal(violations[0]?.reason, "Slice plan has 0 task(s) — expected at least 1");
71
+ } finally {
72
+ rmSync(base, { recursive: true, force: true });
73
+ }
74
+ });
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { describe, it, afterEach } from "node:test";
10
10
  import assert from "node:assert/strict";
11
- import { mkdtempSync, rmSync, existsSync } from "node:fs";
11
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { join } from "node:path";
13
13
  import { tmpdir } from "node:os";
14
14
 
@@ -18,7 +18,7 @@ import type { LoopDeps } from "../auto/loop-deps.js";
18
18
  import { WorktreeStateProjection } from "../worktree-state-projection.js";
19
19
  import type { SessionLockStatus } from "../session-lock.js";
20
20
  import { writeGraph, readGraph, type WorkflowGraph, type GraphStep } from "../graph.ts";
21
- import { writeFileSync } from "node:fs";
21
+ import { SourceObservationStore } from "../source-observations.js";
22
22
  import { stringify } from "yaml";
23
23
 
24
24
  // ─── Helpers ─────────────────────────────────────────────────────────────
@@ -115,6 +115,7 @@ function makeLoopSession(overrides?: Record<string, unknown>) {
115
115
  currentMilestoneId: null,
116
116
  currentUnit: null,
117
117
  currentUnitRouting: null,
118
+ sourceObservations: new SourceObservationStore(),
118
119
  completedUnits: [],
119
120
  resourceVersionOnStart: null,
120
121
  lastPromptCharCount: undefined,
@@ -139,6 +140,19 @@ function makeLoopSession(overrides?: Record<string, unknown>) {
139
140
  newSession: () => Promise.resolve({ cancelled: false }),
140
141
  getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
141
142
  },
143
+ setCurrentUnit(this: any, unit: any) {
144
+ this.currentUnit = unit;
145
+ this.sourceObservations.beginUnit({
146
+ unitType: unit.type,
147
+ unitId: unit.id,
148
+ startedAt: unit.startedAt,
149
+ basePath: unit.workspaceRoot ?? this.basePath,
150
+ });
151
+ },
152
+ clearCurrentUnit(this: any) {
153
+ this.currentUnit = null;
154
+ this.sourceObservations.clear();
155
+ },
142
156
  clearTimers: () => {},
143
157
  lockBasePath: "/tmp/project",
144
158
  ...overrides,
@@ -109,22 +109,12 @@ test("checkEngineHealth treats explicit reopen as authoritative when dispatch ti
109
109
  worker_id, host, pid, started_at, version, last_heartbeat_at, status, project_root_realpath
110
110
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
111
111
  ).run("worker-1", "localhost", 1, "2026-01-01T00:00:00.000Z", "test", "2026-01-01T00:00:00.000Z", "stopped", base);
112
- db.exec("PRAGMA writable_schema = ON");
113
112
  db.prepare(
114
- `UPDATE sqlite_schema
115
- SET sql = replace(sql, 'started_at TEXT NOT NULL', 'started_at TEXT')
116
- WHERE type = 'table' AND name = 'unit_dispatches'`,
117
- ).run();
118
- db.exec("PRAGMA writable_schema = OFF");
119
- closeDatabase();
120
- openDatabase(join(gsdDir, "gsd.db"));
121
- const reopenedDb = _getAdapter()!;
122
- reopenedDb.prepare(
123
113
  `INSERT INTO unit_dispatches (
124
114
  trace_id, worker_id, milestone_lease_token, milestone_id,
125
115
  unit_type, unit_id, status, attempt_n, started_at, ended_at
126
116
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
127
- ).run("trace-1", "worker-1", 1, "M001", "complete-milestone", "M001", "completed", 1, null, null);
117
+ ).run("trace-1", "worker-1", 1, "M001", "complete-milestone", "M001", "completed", 1, "", "");
128
118
  appendEvent(base, {
129
119
  cmd: "reopen-milestone",
130
120
  params: { milestoneId: "M001" },
@@ -19,10 +19,12 @@ import {
19
19
  saveGateResult,
20
20
  markAllGatesOmitted,
21
21
  getPendingSliceGateCount,
22
+ getGateResults,
22
23
  } from "../gsd-db.ts";
23
24
  import { deriveState, invalidateStateCache } from "../state.ts";
24
25
  import { renderPlanFromDb } from "../markdown-renderer.ts";
25
26
  import { invalidateAllCaches } from "../cache.ts";
27
+ import { DISPATCH_RULES } from "../auto-dispatch.ts";
26
28
 
27
29
  function setupTestProject(): { tmpDir: string; dbPath: string } {
28
30
  const tmpDir = mkdtempSync(join(tmpdir(), "gate-dispatch-"));
@@ -213,4 +215,66 @@ describe("evaluating-gates phase", () => {
213
215
  `pending Q8 must not stall evaluating-gates — got phase=${state.phase}`,
214
216
  );
215
217
  });
218
+
219
+ test("gate-evaluate dispatch id includes only gate-evaluate-owned gates", async () => {
220
+ planSlice(tmpDir);
221
+ await renderPlanFromDb(tmpDir, "M001", "S01");
222
+
223
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
224
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
225
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
226
+
227
+ invalidateStateCache();
228
+ const state = await deriveState(tmpDir);
229
+ assert.equal(state.phase, "evaluating-gates");
230
+
231
+ const rule = DISPATCH_RULES.find((candidate) => candidate.name === "evaluating-gates → gate-evaluate");
232
+ assert.ok(rule, "gate-evaluate dispatch rule must exist");
233
+
234
+ const result = await rule.match({
235
+ basePath: tmpDir,
236
+ mid: "M001",
237
+ midTitle: "Test Milestone",
238
+ state,
239
+ prefs: { gate_evaluation: { enabled: true } },
240
+ });
241
+
242
+ assert.ok(result);
243
+ assert.equal(result.action, "dispatch");
244
+ if (result.action !== "dispatch") throw new Error("expected gate-evaluate dispatch");
245
+ assert.equal(result.unitId, "M001/S01/gates+Q3,Q4");
246
+ assert.doesNotMatch(result.prompt, /\*\*Q8\*\*/);
247
+ });
248
+
249
+ test("disabled gate evaluation only omits gate-evaluate-owned gates", async () => {
250
+ planSlice(tmpDir);
251
+ await renderPlanFromDb(tmpDir, "M001", "S01");
252
+
253
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
254
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q4", scope: "slice" });
255
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q8", scope: "slice" });
256
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q5", scope: "task", taskId: "T01" });
257
+
258
+ invalidateStateCache();
259
+ const state = await deriveState(tmpDir);
260
+ assert.equal(state.phase, "evaluating-gates");
261
+
262
+ const rule = DISPATCH_RULES.find((candidate) => candidate.name === "evaluating-gates → gate-evaluate");
263
+ assert.ok(rule, "gate-evaluate dispatch rule must exist");
264
+
265
+ const result = await rule.match({
266
+ basePath: tmpDir,
267
+ mid: "M001",
268
+ midTitle: "Test Milestone",
269
+ state,
270
+ prefs: { gate_evaluation: { enabled: false } },
271
+ });
272
+
273
+ assert.equal(result?.action, "skip");
274
+ const byId = new Map(getGateResults("M001", "S01").map((gate) => [gate.gate_id, gate]));
275
+ assert.equal(byId.get("Q3")?.verdict, "omitted");
276
+ assert.equal(byId.get("Q4")?.verdict, "omitted");
277
+ assert.equal(byId.get("Q8")?.status, "pending");
278
+ assert.equal(byId.get("Q5")?.status, "pending");
279
+ });
216
280
  });
@@ -83,6 +83,21 @@ describe("quality_gates CRUD", () => {
83
83
  assert.ok(results[0].evaluated_at);
84
84
  });
85
85
 
86
+ test("saveGateResult throws when the gate row does not exist", () => {
87
+ assert.throws(
88
+ () => saveGateResult({
89
+ milestoneId: "M001",
90
+ sliceId: "S01",
91
+ gateId: "Q3",
92
+ verdict: "pass",
93
+ rationale: "No row exists.",
94
+ findings: "",
95
+ }),
96
+ /quality gate row not found/,
97
+ );
98
+ assert.equal(getGateResults("M001", "S01").length, 0);
99
+ });
100
+
86
101
  test("getPendingGates filters by scope", () => {
87
102
  insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
88
103
  insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q5", scope: "task", taskId: "T01" });