@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
@@ -502,7 +502,9 @@ describe('gsd-recover', async () => {
502
502
  insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
503
503
 
504
504
  const { ctx, notes } = makeCtx();
505
- await handleRecover(ctx, base, '--confirm');
505
+ // M999 is in the DB but not in the markdown, so recover would delete it:
506
+ // a data-loss recover now requires explicit --allow-data-loss.
507
+ await handleRecover(ctx, base, '--confirm --allow-data-loss');
506
508
 
507
509
  assert.equal(getMilestone('M999'), null, 'confirmed recover clears old hierarchy rows');
508
510
  assert.ok(getMilestone('M001'), 'confirmed recover imports markdown hierarchy');
@@ -512,4 +514,63 @@ describe('gsd-recover', async () => {
512
514
  cleanup(base);
513
515
  }
514
516
  });
517
+
518
+ test('handleRecover refuses to delete DB rows markdown lacks without --allow-data-loss', async () => {
519
+ const base = createFixtureBase();
520
+ try {
521
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
522
+ openDatabase(':memory:');
523
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
524
+
525
+ const { ctx, notes } = makeCtx();
526
+ await handleRecover(ctx, base, '--confirm');
527
+
528
+ // --confirm alone must NOT clear authoritative DB rows the markdown lacks.
529
+ assert.ok(getMilestone('M999'), 'data-loss recover is refused, DB row preserved');
530
+ assert.equal(getMilestone('M001'), null, 'markdown not imported on refusal');
531
+ assert.equal(notes.at(-1)?.kind, 'error');
532
+ } finally {
533
+ closeDatabase();
534
+ cleanup(base);
535
+ }
536
+ });
537
+
538
+ test('handleRecover interactive data-loss requires a second explicit acknowledgement', async () => {
539
+ const base = createFixtureBase();
540
+ try {
541
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
542
+ openDatabase(':memory:');
543
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
544
+
545
+ // First confirm (proceed?) = yes; second confirm (delete rows?) = no.
546
+ let call = 0;
547
+ const { ctx, notes } = makeCtx(async () => { call += 1; return call === 1; });
548
+ await handleRecover(ctx, base, '');
549
+
550
+ assert.ok(getMilestone('M999'), 'declining the data-loss ack preserves DB rows');
551
+ assert.equal(getMilestone('M001'), null, 'markdown not imported when data-loss ack declined');
552
+ assert.match(notes.at(-1)?.message ?? '', /cancelled/);
553
+ } finally {
554
+ closeDatabase();
555
+ cleanup(base);
556
+ }
557
+ });
558
+
559
+ test('handleRecover interactive proceeds when the data-loss deletion is acknowledged', async () => {
560
+ const base = createFixtureBase();
561
+ try {
562
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_M001);
563
+ openDatabase(':memory:');
564
+ insertMilestone({ id: 'M999', title: 'Existing DB State', status: 'active' });
565
+
566
+ const { ctx } = makeCtx(async () => true); // both confirms accepted
567
+ await handleRecover(ctx, base, '');
568
+
569
+ assert.equal(getMilestone('M999'), null, 'acknowledged data-loss recover clears old rows');
570
+ assert.ok(getMilestone('M001'), 'acknowledged data-loss recover imports markdown');
571
+ } finally {
572
+ closeDatabase();
573
+ cleanup(base);
574
+ }
575
+ });
515
576
  });
@@ -30,6 +30,7 @@ import {
30
30
  insertTask,
31
31
  openDatabase,
32
32
  } from "../gsd-db.js";
33
+ import { SourceObservationStore } from "../source-observations.js";
33
34
 
34
35
  // ─── Helpers ─────────────────────────────────────────────────────────────────
35
36
 
@@ -186,6 +187,7 @@ function makeSession() {
186
187
  currentMilestoneId: "M001",
187
188
  currentUnit: null,
188
189
  currentUnitRouting: null,
190
+ sourceObservations: new SourceObservationStore(),
189
191
  completedUnits: [],
190
192
  resourceVersionOnStart: null,
191
193
  lastPromptCharCount: undefined,
@@ -207,6 +209,19 @@ function makeSession() {
207
209
  newSession: () => Promise.resolve({ cancelled: false }),
208
210
  getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
209
211
  },
212
+ setCurrentUnit(this: any, unit: any) {
213
+ this.currentUnit = unit;
214
+ this.sourceObservations.beginUnit({
215
+ unitType: unit.type,
216
+ unitId: unit.id,
217
+ startedAt: unit.startedAt,
218
+ basePath: unit.workspaceRoot ?? this.basePath,
219
+ });
220
+ },
221
+ clearCurrentUnit(this: any) {
222
+ this.currentUnit = null;
223
+ this.sourceObservations.clear();
224
+ },
210
225
  clearTimers: () => {},
211
226
  } as any;
212
227
  }
@@ -393,6 +393,48 @@ test('── markdown-renderer: renderPlanCheckboxes round-trip ──', async (
393
393
  }
394
394
  });
395
395
 
396
+ test('── markdown-renderer: renderPlanCheckboxes re-renders DB tasks added after the plan artifact ──', async () => {
397
+ // Regression for the lossy-projection root cause: renderPlanCheckboxes used to
398
+ // patch the cached PLAN artifact in place, silently dropping tasks added to
399
+ // the DB after the artifact was first written (the 4S/0T-vs-5S/13T drift).
400
+ const tmpDir = makeTmpDir();
401
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
402
+ openDatabase(dbPath);
403
+ clearAllCaches();
404
+
405
+ try {
406
+ scaffoldDirs(tmpDir, 'M001', ['S01']);
407
+ insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
408
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice', status: 'pending' });
409
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First task', status: 'pending' });
410
+
411
+ // PLAN.md on disk reflects an earlier state with only T01.
412
+ const planPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md');
413
+ fs.writeFileSync(planPath, makePlanContent('S01', [{ id: 'T01', title: 'First task', done: false }]));
414
+ clearAllCaches();
415
+
416
+ // Two more tasks are written to the DB after the artifact already exists.
417
+ insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Second task', status: 'done' });
418
+ insertTask({ id: 'T03', sliceId: 'S01', milestoneId: 'M001', title: 'Third task', status: 'pending' });
419
+
420
+ const ok = await renderPlanCheckboxes(tmpDir, 'M001', 'S01');
421
+ assert.ok(ok, 'renderPlanCheckboxes returns true');
422
+
423
+ const parsed = parsePlan(fs.readFileSync(planPath, 'utf-8'));
424
+ clearAllCaches();
425
+ assert.deepStrictEqual(
426
+ parsed.tasks.map(t => t.id).sort(),
427
+ ['T01', 'T02', 'T03'],
428
+ 'full re-render must include tasks added after the artifact was written',
429
+ );
430
+ assert.ok(parsed.tasks.find(t => t.id === 'T02')!.done, 'T02 reflects done status from DB');
431
+ assert.ok(!parsed.tasks.find(t => t.id === 'T03')!.done, 'T03 reflects pending status from DB');
432
+ } finally {
433
+ closeDatabase();
434
+ cleanupDir(tmpDir);
435
+ }
436
+ });
437
+
396
438
  test('── markdown-renderer: renderPlanCheckboxes bidirectional ──', async () => {
397
439
  const tmpDir = makeTmpDir();
398
440
  const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
@@ -151,6 +151,105 @@ test("migration auto-check leaves matching DB hierarchy alone", async () => {
151
151
  }
152
152
  });
153
153
 
154
+ test("migration auto-check flags a populated DB with missing markdown and points at rebuild (not recover)", async () => {
155
+ const base = makeBase();
156
+ try {
157
+ // A project with no milestone markdown: simulate lost/empty projections
158
+ // over a populated DB. The previous early return treated all-zero markdown
159
+ // as 'no project' and never even opened the DB, silently hiding the rows.
160
+ await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
161
+ assert.equal(await ensureDbOpen(base), true);
162
+ assert.deepEqual(countMarkdownHierarchy(base), { milestones: 0, slices: 0, tasks: 0 });
163
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
164
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
165
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Legacy Task", status: "pending" });
166
+
167
+ const result = await checkMarkdownHierarchyAgainstDb(base);
168
+ assert.equal(result.action, "recovery-required");
169
+ assert.equal(result.reason, "markdown-missing");
170
+ // The DB is the richer side, so recover (md → DB) would DELETE rows. The
171
+ // safe repair is to re-project from the DB.
172
+ assert.equal(result.recoveryCommand, "/gsd rebuild markdown");
173
+ assert.match(result.message ?? "", /rebuild markdown/);
174
+ assert.match(result.message ?? "", /Do NOT run/);
175
+ // The check must not mutate the DB.
176
+ assert.equal(getAllMilestones().length, 1);
177
+ assert.equal(getSliceTasks("M001", "S01").length, 1);
178
+ } finally {
179
+ cleanup(base);
180
+ }
181
+ });
182
+
183
+ test("migration auto-check detects identity drift even when counts match", async () => {
184
+ const base = makeBase();
185
+ try {
186
+ await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01
187
+ assert.equal(await ensureDbOpen(base), true);
188
+ // Same cardinalities (1M/1S/1T) but a DIFFERENT slice identity (S99 vs S01).
189
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
190
+ insertSlice({ id: "S99", milestoneId: "M001", title: "Other Slice", status: "pending", risk: "medium", depends: [], demo: "d", sequence: 1 });
191
+ insertTask({ id: "T01", sliceId: "S99", milestoneId: "M001", title: "Legacy Task", status: "pending" });
192
+
193
+ const result = await checkMarkdownHierarchyAgainstDb(base);
194
+ // Counts are equal on both sides, so the old count-only comparison reported
195
+ // 'in-sync'. Identity comparison must catch the divergence instead.
196
+ assert.equal(result.action, "recovery-required");
197
+ assert.notEqual(result.reason, "in-sync");
198
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
199
+ assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 1 });
200
+ // The DB holds S99 (which markdown lacks), so recover would DELETE it. Even
201
+ // at equal counts the safe recommendation must be rebuild, not recover.
202
+ assert.equal(result.recoveryCommand, "/gsd rebuild markdown");
203
+ assert.match(result.message ?? "", /Do NOT run/);
204
+ } finally {
205
+ cleanup(base);
206
+ }
207
+ });
208
+
209
+ test("recoverWouldDeleteDbRows flags identity drift the markdown lacks (even at equal counts)", async () => {
210
+ const base = makeBase();
211
+ try {
212
+ await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01
213
+ assert.equal(await ensureDbOpen(base), true);
214
+ // DB row identity (S99) differs from markdown (S01) at the same count.
215
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
216
+ insertSlice({ id: "S99", milestoneId: "M001", title: "Other Slice", status: "pending", risk: "medium", depends: [], demo: "d", sequence: 1 });
217
+ insertTask({ id: "T01", sliceId: "S99", milestoneId: "M001", title: "Legacy Task", status: "pending" });
218
+
219
+ const { recoverWouldDeleteDbRows } = await import("../migration-auto-check.ts");
220
+ assert.equal(recoverWouldDeleteDbRows(base), true, "DB S99 is absent from markdown — recover would delete it");
221
+ } finally {
222
+ cleanup(base);
223
+ }
224
+ });
225
+
226
+ test("migration auto-check canonicalizes a legacy descriptor milestone dir (no false drift)", async () => {
227
+ const base = makeBase();
228
+ try {
229
+ await writeGSDDirectory(projectFixture(), base); // creates .gsd/milestones/M001
230
+ // Rename the dir to a legacy descriptor form while the DB id stays "M001".
231
+ // scanMarkdownHierarchy must canonicalize "M001-old" → "M001" so the
232
+ // identity sets line up with scanDbHierarchy (which uses milestone.id).
233
+ const milestonesRoot = join(base, ".gsd", "milestones");
234
+ renameSync(join(milestonesRoot, "M001"), join(milestonesRoot, "M001-old"));
235
+
236
+ assert.equal(await ensureDbOpen(base), true);
237
+ insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
238
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
239
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Legacy Task", status: "pending" });
240
+
241
+ const result = await checkMarkdownHierarchyAgainstDb(base);
242
+ // Must be in-sync: the raw dir name "M001-old" would otherwise mismatch the
243
+ // DB id "M001" and be flagged as false drift.
244
+ assert.equal(result.action, "none");
245
+ assert.equal(result.reason, "in-sync");
246
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
247
+ assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 1 });
248
+ } finally {
249
+ cleanup(base);
250
+ }
251
+ });
252
+
154
253
  test("migration auto-check refreshes a stale open DB handle before comparing", async () => {
155
254
  const base = makeBase();
156
255
  try {
@@ -133,6 +133,36 @@ test('handlePlanSlice persists explicit slice/task target repositories', async (
133
133
  }
134
134
  });
135
135
 
136
+ test('handlePlanSlice honors configured gate-evaluation gate sets', async () => {
137
+ const base = makeTmpBase();
138
+ openDatabase(join(base, '.gsd', 'gsd.db'));
139
+
140
+ try {
141
+ seedParentSlice();
142
+ writeFileSync(
143
+ join(base, '.gsd', 'PREFERENCES.md'),
144
+ [
145
+ '---',
146
+ 'gate_evaluation:',
147
+ ' enabled: true',
148
+ ' slice_gates:',
149
+ ' - Q3',
150
+ ' task_gates: false',
151
+ '---',
152
+ ].join('\n'),
153
+ 'utf-8',
154
+ );
155
+
156
+ const result = await handlePlanSlice(validParams(), base);
157
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
158
+
159
+ const gateIds = getGateResults('M001', 'S02').map((gate) => gate.gate_id).sort();
160
+ assert.deepEqual(gateIds, ['Q3', 'Q8']);
161
+ } finally {
162
+ cleanup(base);
163
+ }
164
+ });
165
+
136
166
  test('handlePlanSlice rejects unknown target repositories', async () => {
137
167
  const base = makeTmpBase();
138
168
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -196,6 +226,50 @@ test('handlePlanSlice enforces absolute path scope to declared target repositori
196
226
  }
197
227
  });
198
228
 
229
+ test('handlePlanSlice resolves relative task IO paths against declared target repository roots', async () => {
230
+ const base = makeTmpBase();
231
+ openDatabase(join(base, '.gsd', 'gsd.db'));
232
+
233
+ try {
234
+ seedParentSlice();
235
+ mkdirSync(join(base, 'frontend'), { recursive: true });
236
+ writeFileSync(join(base, 'frontend', 'app.js'), 'export {};\n', 'utf-8');
237
+ writeFileSync(
238
+ join(base, '.gsd', 'PREFERENCES.md'),
239
+ [
240
+ '---',
241
+ 'workspace:',
242
+ ' mode: parent',
243
+ ' repositories:',
244
+ ' frontend:',
245
+ ' path: frontend',
246
+ '---',
247
+ ].join('\n'),
248
+ 'utf-8',
249
+ );
250
+
251
+ const params = validParams();
252
+ const result = await handlePlanSlice({
253
+ ...params,
254
+ targetRepositories: ['frontend'],
255
+ tasks: [
256
+ {
257
+ ...params.tasks[0],
258
+ files: ['app.js'],
259
+ inputs: ['app.js'],
260
+ expectedOutput: ['app.js'],
261
+ targetRepositories: ['frontend'],
262
+ },
263
+ ],
264
+ }, base);
265
+
266
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
267
+ assert.deepEqual(getSliceTasks('M001', 'S02').map((task) => task.id), ['T01']);
268
+ } finally {
269
+ cleanup(base);
270
+ }
271
+ });
272
+
199
273
  test('handlePlanSlice rejects relative traversal outside declared target repositories', async () => {
200
274
  const base = makeTmpBase();
201
275
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -749,7 +823,7 @@ test('regression: validateTasks surfaces clean per-field errors for non-array IO
749
823
  }
750
824
  });
751
825
 
752
- test('handlePlanSlice skips prose and sentinel values in planning path scope', async () => {
826
+ test('handlePlanSlice skips prose and sentinel input values in planning path scope', async () => {
753
827
  const base = makeTmpBase();
754
828
  openDatabase(join(base, '.gsd', 'gsd.db'));
755
829
 
@@ -760,7 +834,7 @@ test('handlePlanSlice skips prose and sentinel values in planning path scope', a
760
834
  tasks: [{
761
835
  ...validParams().tasks[0],
762
836
  inputs: ['Current enum shape in codebase', 'None'],
763
- expectedOutput: ['Updated planning-path-scope.ts — validates paths only'],
837
+ expectedOutput: ['src/resources/extensions/gsd/planning-path-scope.ts'],
764
838
  }],
765
839
  }, base);
766
840
 
@@ -771,6 +845,29 @@ test('handlePlanSlice skips prose and sentinel values in planning path scope', a
771
845
  }
772
846
  });
773
847
 
848
+ test('handlePlanSlice rejects prose expectedOutput entries before path-scope validation', async () => {
849
+ const base = makeTmpBase();
850
+ openDatabase(join(base, '.gsd', 'gsd.db'));
851
+
852
+ try {
853
+ seedParentSlice();
854
+ const result = await handlePlanSlice({
855
+ ...validParams(),
856
+ tasks: [{
857
+ ...validParams().tasks[0],
858
+ expectedOutput: ['Browser UI supports due-date add/edit flows and mixed-list urgency rendering.'],
859
+ }],
860
+ }, base);
861
+
862
+ assert.ok('error' in result);
863
+ assert.match(result.error, /expectedOutput must contain only file paths/);
864
+ assert.doesNotMatch(result.error, /outside allowed repository roots/);
865
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid output contract must not persist tasks');
866
+ } finally {
867
+ cleanup(base);
868
+ }
869
+ });
870
+
774
871
  test('handlePlanSlice resolves relative task IO paths against worktree roots', async () => {
775
872
  const base = makeTmpBase();
776
873
  const worktree = join(base, '.gsd', 'worktrees', 'M001');
@@ -96,6 +96,25 @@ test('handlePlanTask explains string IO fields must be arrays', async () => {
96
96
  }
97
97
  });
98
98
 
99
+ test('handlePlanTask rejects prose expectedOutput entries', async () => {
100
+ const base = makeTmpBase();
101
+ openDatabase(join(base, '.gsd', 'gsd.db'));
102
+
103
+ try {
104
+ seedParent();
105
+ const result = await handlePlanTask({
106
+ ...validParams(),
107
+ expectedOutput: ['Browser UI supports due-date add/edit flows and mixed-list urgency rendering.'],
108
+ }, base);
109
+ assert.ok('error' in result);
110
+ assert.match(result.error, /validation failed: expectedOutput must contain only file paths/);
111
+ assert.doesNotMatch(result.error, /outside allowed repository roots/);
112
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid output contract must not persist the task');
113
+ } finally {
114
+ cleanup(base);
115
+ }
116
+ });
117
+
99
118
  test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
100
119
  const base = makeTmpBase();
101
120
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -467,6 +467,20 @@ test("notification fields validate correctly", () => {
467
467
  assert.equal(preferences.notifications?.on_complete, false);
468
468
  });
469
469
 
470
+ test("gate_evaluation slice_gates only accepts gate-evaluate-owned gates", () => {
471
+ const valid = validatePreferences({
472
+ gate_evaluation: { enabled: true, slice_gates: ["Q3", "Q4"], task_gates: false },
473
+ });
474
+ assert.equal(valid.errors.length, 0);
475
+ assert.deepEqual(valid.preferences.gate_evaluation?.slice_gates, ["Q3", "Q4"]);
476
+ assert.equal(valid.preferences.gate_evaluation?.task_gates, false);
477
+
478
+ const invalid = validatePreferences({
479
+ gate_evaluation: { enabled: true, slice_gates: ["Q3", "Q8"] },
480
+ });
481
+ assert.ok(invalid.errors.some((error) => error.includes("gate_evaluation.slice_gates")));
482
+ });
483
+
470
484
  test("cmux fields validate correctly", () => {
471
485
  const { preferences, errors } = validatePreferences({
472
486
  cmux: {
@@ -18,6 +18,7 @@ const PREF_SAMPLE_VALUES: Record<string, unknown> = {
18
18
  skill_rules: [{ when: "unit:execute-task", use: ["test-writer-fixer"] }],
19
19
  custom_instructions: ["Keep changes focused."],
20
20
  models: { execution: "openai/gpt-5" },
21
+ thinking: { planning: "xhigh", execution: "low" },
21
22
  skill_discovery: "auto",
22
23
  skill_staleness_days: 7,
23
24
  auto_supervisor: { soft_timeout_minutes: 20, idle_timeout_minutes: 10, hard_timeout_minutes: 30 },
@@ -17,6 +17,7 @@ import {
17
17
  } from "../bootstrap/register-hooks.ts";
18
18
  import { shouldBlockAutoUnitToolCall } from "../auto-unit-tool-scope.ts";
19
19
  import { UNIT_TOOL_CONTRACTS } from "../unit-tool-contracts.ts";
20
+ import { uatTypeIncludesBrowser } from "../uat-policy.ts";
20
21
 
21
22
  const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
22
23
  const templatesDir = join(process.cwd(), "src/resources/extensions/gsd/templates");
@@ -183,6 +184,7 @@ test("live-runtime and mixed UAT presentations also surface browser tools", () =
183
184
  // drive a browser, so the runner must actually receive the browser tools and
184
185
  // a hybrid surface — otherwise live checks silently downgrade to NEEDS-HUMAN.
185
186
  for (const uatType of ["live-runtime", "mixed", "human-experience"] as const) {
187
+ assert.equal(uatTypeIncludesBrowser(uatType), true, `${uatType} policy should include browser tools`);
186
188
  const presentation = buildRunUatPresentationForType(uatType);
187
189
  assert.equal(presentation.surface, "hybrid", `${uatType} should use the hybrid surface`);
188
190
  for (const toolName of RUN_UAT_BROWSER_TOOL_NAMES) {
@@ -196,6 +198,7 @@ test("live-runtime and mixed UAT presentations also surface browser tools", () =
196
198
 
197
199
  test("artifact-driven and runtime-executable UAT presentations stay browser-free", () => {
198
200
  for (const uatType of ["artifact-driven", "runtime-executable"] as const) {
201
+ assert.equal(uatTypeIncludesBrowser(uatType), false, `${uatType} policy should stay browser-free`);
199
202
  const presentation = buildRunUatPresentationForType(uatType);
200
203
  assert.equal(presentation.surface, "mcp", `${uatType} should use the mcp surface`);
201
204
  assert.ok(
@@ -659,6 +662,12 @@ test("parallel subagent prompts forbid serialized tasks arrays", () => {
659
662
  }
660
663
  });
661
664
 
665
+ test("gate-evaluate prompt requires gate result findings field", () => {
666
+ const prompt = readPrompt("gate-evaluate");
667
+ assert.match(prompt, /`findings`/);
668
+ assert.match(prompt, /empty string if none/i);
669
+ });
670
+
662
671
  // ─── Project-shape classifier + 3-or-4-options-with-Other-hatch contract ──
663
672
 
664
673
  test("guided-discuss-project classifies project shape and persists the verdict to PROJECT.md", () => {
@@ -1,10 +1,16 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, rmSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
7
  import { registerHooks } from "../bootstrap/register-hooks.ts";
8
+ import {
9
+ clearPendingAutoStart,
10
+ setPendingAutoStart,
11
+ } from "../guided-flow.ts";
12
+ import { closeDatabase, getMilestone } from "../gsd-db.ts";
13
+ import { deriveState, invalidateStateCache } from "../state.ts";
8
14
  import {
9
15
  getPendingGate,
10
16
  resetWriteGateState,
@@ -186,6 +192,100 @@ test("register-hooks unlocks milestone depth verification from question id witho
186
192
  );
187
193
  });
188
194
 
195
+ test("register-hooks persists first structured question round for new milestone re-entry", async (t) => {
196
+ const dir = makeTempDir("question-draft");
197
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
198
+ const originalCwd = process.cwd();
199
+ process.chdir(dir);
200
+ resetWriteGateState(dir);
201
+ clearPendingAutoStart(dir);
202
+
203
+ t.after(() => {
204
+ try {
205
+ resetWriteGateState(dir);
206
+ clearPendingAutoStart(dir);
207
+ closeDatabase();
208
+ } finally {
209
+ process.chdir(originalCwd);
210
+ rmSync(dir, { recursive: true, force: true });
211
+ }
212
+ });
213
+
214
+ const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<void> | void>>();
215
+ const pi = {
216
+ on(event: string, handler: (event: any, ctx?: any) => Promise<void> | void) {
217
+ const existing = handlers.get(event) ?? [];
218
+ existing.push(handler);
219
+ handlers.set(event, existing);
220
+ },
221
+ } as any;
222
+ const ctx = { cwd: dir, ui: { notify: () => undefined } } as any;
223
+
224
+ registerHooks(pi, []);
225
+ setPendingAutoStart(dir, {
226
+ basePath: dir,
227
+ milestoneId: "M004",
228
+ ctx,
229
+ pi: { sendMessage: () => undefined } as any,
230
+ });
231
+
232
+ const questions = [
233
+ {
234
+ id: "m004_shape",
235
+ header: "M004 Shape",
236
+ question: "What are you picturing for M004?",
237
+ options: [
238
+ { label: "Planning metadata (Recommended)", description: "Plan the next metadata layer." },
239
+ { label: "Find and organize", description: "Improve searching and organizing." },
240
+ ],
241
+ },
242
+ {
243
+ id: "boundary",
244
+ header: "Boundary",
245
+ question: "Which boundary should I plan around?",
246
+ options: [
247
+ { label: "No new dependencies (Recommended)", description: "Keep implementation vanilla." },
248
+ { label: "Browser APIs OK", description: "Use browser-native capabilities." },
249
+ ],
250
+ },
251
+ ];
252
+
253
+ for (const handler of handlers.get("tool_result") ?? []) {
254
+ await handler({
255
+ toolName: "ask_user_questions",
256
+ input: { questions },
257
+ details: {
258
+ response: {
259
+ answers: {
260
+ m004_shape: { selected: "Planning metadata (Recommended)" },
261
+ boundary: { selected: "No new dependencies (Recommended)" },
262
+ },
263
+ },
264
+ },
265
+ }, ctx);
266
+ }
267
+
268
+ const milestoneDir = join(dir, ".gsd", "milestones", "M004");
269
+ const draftPath = join(milestoneDir, "M004-CONTEXT-DRAFT.md");
270
+ const discussionPath = join(milestoneDir, "M004-DISCUSSION.md");
271
+
272
+ assert.equal(existsSync(draftPath), true, "first answer round should create a resumable context draft");
273
+ assert.equal(existsSync(discussionPath), true, "first answer round should create a discussion log");
274
+
275
+ const draft = readFileSync(draftPath, "utf-8");
276
+ assert.match(draft, /What are you picturing for M004\?/);
277
+ assert.match(draft, /Planning metadata \(Recommended\)/);
278
+ assert.match(draft, /No new dependencies \(Recommended\)/);
279
+
280
+ const row = getMilestone("M004");
281
+ assert.equal(row?.status, "queued", "new milestone shell should be registered in the DB");
282
+
283
+ invalidateStateCache();
284
+ const state = await deriveState(dir);
285
+ assert.equal(state.activeMilestone?.id, "M004");
286
+ assert.equal(state.phase, "needs-discussion");
287
+ });
288
+
189
289
  test("register-hooks clears depth gate when remote (Telegram/Slack/Discord) answer is normalized (#4406)", async (t) => {
190
290
  const dir = makeTempDir("remote");
191
291
  const originalCwd = process.cwd();
@@ -2,7 +2,7 @@
2
2
 
3
3
  import test from "node:test";
4
4
  import assert from "node:assert/strict";
5
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
  import { execFileSync } from "node:child_process";
@@ -117,7 +117,7 @@ test("repository registry keeps project root anchored to .gsd project in monorep
117
117
  });
118
118
 
119
119
  test("repository registry uses external-state worktree checkout as project root", (t) => {
120
- const base = mkdtempSync(join(tmpdir(), "gsd-repo-registry-external-"));
120
+ const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-repo-registry-external-")));
121
121
  t.after(() => rmSync(base, { recursive: true, force: true }));
122
122
  const worktree = join(base, ".gsd", "projects", "abc123", "worktrees", "M001");
123
123
  mkdirSync(worktree, { recursive: true });
@@ -67,6 +67,14 @@ test("Tool Contract compiles known Unit prompt and tool policy", () => {
67
67
  assert.deepEqual(result.ok && result.contract.forbiddenWorkflowTools, []);
68
68
  assert.equal(result.ok && result.contract.toolsPolicy.mode, "all");
69
69
  assert.ok(result.ok && result.contract.validationRules.includes("closeout-tool-present"));
70
+ assert.ok(result.ok && result.contract.validationRules.includes("source-observation-contract-present"));
71
+ assert.deepEqual(result.ok && result.contract.sourceObservations, {
72
+ mode: "whole-file-active-unit",
73
+ seedFields: ["task.files", "task.inputs"],
74
+ excludedFields: ["expectedOutput"],
75
+ maxBytes: 50 * 1024,
76
+ maxLines: 2000,
77
+ });
70
78
  });
71
79
 
72
80
  test("Tool Contract records high-risk cross-phase tool boundaries without single-owning every tool", () => {