@opengsd/gsd-pi 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/README.md +10 -8
  2. package/dist/cli.js +5 -1
  3. package/dist/headless-events.d.ts +2 -1
  4. package/dist/headless-events.js +11 -3
  5. package/dist/headless-milestone-readiness.d.ts +29 -0
  6. package/dist/headless-milestone-readiness.js +161 -0
  7. package/dist/headless.js +25 -2
  8. package/dist/print-mode-exit.d.ts +12 -0
  9. package/dist/print-mode-exit.js +14 -0
  10. package/dist/resources/.managed-resources-content-hash +1 -1
  11. package/dist/resources/GSD-WORKFLOW.md +16 -47
  12. package/dist/resources/extensions/browser-tools/capture.js +2 -77
  13. package/dist/resources/extensions/browser-tools/screenshot-constraints.js +83 -0
  14. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -2
  15. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +30 -2
  16. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +74 -19
  18. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -0
  19. package/dist/resources/extensions/gsd/auto-prompts.js +35 -1
  20. package/dist/resources/extensions/gsd/auto.js +5 -1
  21. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +78 -4
  22. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -26
  23. package/dist/resources/extensions/gsd/command-feedback.js +16 -12
  24. package/dist/resources/extensions/gsd/compat/compat-marker.js +19 -2
  25. package/dist/resources/extensions/gsd/db/command-queue.js +7 -2
  26. package/dist/resources/extensions/gsd/db/engine.js +51 -16
  27. package/dist/resources/extensions/gsd/db/writers/cascades.js +5 -1
  28. package/dist/resources/extensions/gsd/db/writers/status.js +13 -3
  29. package/dist/resources/extensions/gsd/db-base-schema.js +28 -0
  30. package/dist/resources/extensions/gsd/db-migration-steps.js +30 -0
  31. package/dist/resources/extensions/gsd/db-schema-metadata.js +4 -1
  32. package/dist/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  33. package/dist/resources/extensions/gsd/flat-phase-migration.js +156 -21
  34. package/dist/resources/extensions/gsd/gsd-db.js +187 -45
  35. package/dist/resources/extensions/gsd/guided-flow.js +29 -10
  36. package/dist/resources/extensions/gsd/markdown-renderer.js +16 -2
  37. package/dist/resources/extensions/gsd/md-importer.js +102 -14
  38. package/dist/resources/extensions/gsd/migrate/command.js +19 -162
  39. package/dist/resources/extensions/gsd/migrate/execution.js +98 -0
  40. package/dist/resources/extensions/gsd/migrate/plan.js +49 -0
  41. package/dist/resources/extensions/gsd/migrate/planning-writer.js +12 -17
  42. package/dist/resources/extensions/gsd/migrate/presentation.js +64 -0
  43. package/dist/resources/extensions/gsd/parallel-eligibility.js +5 -4
  44. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +28 -43
  45. package/dist/resources/extensions/gsd/paths.js +4 -1
  46. package/dist/resources/extensions/gsd/phase-anchor.js +7 -2
  47. package/dist/resources/extensions/gsd/post-execution-checks.js +4 -4
  48. package/dist/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  49. package/dist/resources/extensions/gsd/safety/evidence-collector.js +26 -1
  50. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +30 -5
  51. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.js +27 -13
  52. package/dist/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.js +13 -3
  53. package/dist/resources/extensions/gsd/stop-notice.js +15 -1
  54. package/dist/resources/extensions/gsd/sync-lock.js +13 -7
  55. package/dist/resources/extensions/gsd/tools/complete-task.js +82 -1
  56. package/dist/resources/extensions/gsd/tools/plan-task.js +13 -15
  57. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +18 -15
  58. package/dist/resources/extensions/gsd/tools/replan-task.js +128 -0
  59. package/dist/resources/extensions/gsd/tools/rework-brief.js +74 -0
  60. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +81 -0
  61. package/dist/resources/extensions/gsd/unit-registry.js +4 -0
  62. package/dist/resources/extensions/gsd/uok/plan-v2.js +19 -3
  63. package/dist/resources/extensions/gsd/worktree-state-projection.js +6 -0
  64. package/dist/resources/extensions/mac-tools/index.js +31 -9
  65. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  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/server/app/_global-error/page_client-reference-manifest.js +1 -1
  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/page_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.rsc +2 -2
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  84. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  87. package/dist/web/standalone/.next/server/app/index.html +1 -1
  88. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  89. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  91. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  93. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  94. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  96. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  97. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  99. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  100. package/dist/web/standalone/.next/static/css/3b1ceb7abfb9a230.css +1 -0
  101. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  102. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  103. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  104. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  105. package/dist/web/standalone/package.json +0 -1
  106. package/dist/web-mode.js +31 -3
  107. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  108. package/integrations/hermes/pyproject.toml +1 -1
  109. package/package.json +12 -10
  110. package/packages/cloud-mcp-gateway/package.json +2 -2
  111. package/packages/contracts/dist/workflow.d.ts +14 -0
  112. package/packages/contracts/dist/workflow.d.ts.map +1 -1
  113. package/packages/contracts/dist/workflow.js +16 -0
  114. package/packages/contracts/dist/workflow.js.map +1 -1
  115. package/packages/contracts/package.json +1 -1
  116. package/packages/daemon/dist/cloud-config.d.ts +1 -1
  117. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  118. package/packages/daemon/dist/cloud-config.js +5 -1
  119. package/packages/daemon/dist/cloud-config.js.map +1 -1
  120. package/packages/daemon/dist/cloud-config.test.js +51 -1
  121. package/packages/daemon/dist/cloud-config.test.js.map +1 -1
  122. package/packages/daemon/dist/local-tool-executor.d.ts.map +1 -1
  123. package/packages/daemon/dist/local-tool-executor.js +2 -0
  124. package/packages/daemon/dist/local-tool-executor.js.map +1 -1
  125. package/packages/daemon/package.json +6 -6
  126. package/packages/gsd-agent-core/package.json +5 -5
  127. package/packages/gsd-agent-modes/package.json +7 -7
  128. package/packages/gsd-cloud/dist/cli.d.ts +5 -0
  129. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -0
  130. package/packages/gsd-cloud/dist/cli.js +160 -0
  131. package/packages/gsd-cloud/dist/cli.js.map +1 -0
  132. package/packages/gsd-cloud/dist/cloud-config.d.ts +19 -0
  133. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -0
  134. package/packages/gsd-cloud/dist/cloud-config.js +231 -0
  135. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -0
  136. package/packages/gsd-cloud/dist/cloud-config.test.d.ts +2 -0
  137. package/packages/gsd-cloud/dist/cloud-config.test.d.ts.map +1 -0
  138. package/packages/gsd-cloud/dist/cloud-config.test.js +118 -0
  139. package/packages/gsd-cloud/dist/cloud-config.test.js.map +1 -0
  140. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts +2 -0
  141. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.d.ts.map +1 -0
  142. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js +37 -0
  143. package/packages/gsd-cloud/dist/cloud-gateway-lookup.test.js.map +1 -0
  144. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +35 -0
  145. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -0
  146. package/packages/gsd-cloud/dist/cloud-runtime.js +258 -0
  147. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -0
  148. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts +2 -0
  149. package/packages/gsd-cloud/dist/cloud-runtime.test.d.ts.map +1 -0
  150. package/packages/gsd-cloud/dist/cloud-runtime.test.js +88 -0
  151. package/packages/gsd-cloud/dist/cloud-runtime.test.js.map +1 -0
  152. package/packages/gsd-cloud/dist/cloud-token.d.ts +3 -0
  153. package/packages/gsd-cloud/dist/cloud-token.d.ts.map +1 -0
  154. package/packages/gsd-cloud/dist/cloud-token.js +37 -0
  155. package/packages/gsd-cloud/dist/cloud-token.js.map +1 -0
  156. package/packages/gsd-cloud/dist/config.d.ts +17 -0
  157. package/packages/gsd-cloud/dist/config.d.ts.map +1 -0
  158. package/packages/gsd-cloud/dist/config.js +146 -0
  159. package/packages/gsd-cloud/dist/config.js.map +1 -0
  160. package/packages/gsd-cloud/dist/device-flow.d.ts +24 -0
  161. package/packages/gsd-cloud/dist/device-flow.d.ts.map +1 -0
  162. package/packages/gsd-cloud/dist/device-flow.js +128 -0
  163. package/packages/gsd-cloud/dist/device-flow.js.map +1 -0
  164. package/packages/gsd-cloud/dist/device-flow.test.d.ts +2 -0
  165. package/packages/gsd-cloud/dist/device-flow.test.d.ts.map +1 -0
  166. package/packages/gsd-cloud/dist/device-flow.test.js +155 -0
  167. package/packages/gsd-cloud/dist/device-flow.test.js.map +1 -0
  168. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts +6 -0
  169. package/packages/gsd-cloud/dist/executors/claude-executor.d.ts.map +1 -0
  170. package/packages/gsd-cloud/dist/executors/claude-executor.js +15 -0
  171. package/packages/gsd-cloud/dist/executors/claude-executor.js.map +1 -0
  172. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts +6 -0
  173. package/packages/gsd-cloud/dist/executors/codex-executor.d.ts.map +1 -0
  174. package/packages/gsd-cloud/dist/executors/codex-executor.js +15 -0
  175. package/packages/gsd-cloud/dist/executors/codex-executor.js.map +1 -0
  176. package/packages/gsd-cloud/dist/executors/executor.d.ts +28 -0
  177. package/packages/gsd-cloud/dist/executors/executor.d.ts.map +1 -0
  178. package/packages/gsd-cloud/dist/executors/executor.js +11 -0
  179. package/packages/gsd-cloud/dist/executors/executor.js.map +1 -0
  180. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts +34 -0
  181. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.d.ts.map +1 -0
  182. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js +179 -0
  183. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.js.map +1 -0
  184. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts +2 -0
  185. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.d.ts.map +1 -0
  186. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js +44 -0
  187. package/packages/gsd-cloud/dist/executors/gsd-pi-executor.test.js.map +1 -0
  188. package/packages/gsd-cloud/dist/executors/index.d.ts +17 -0
  189. package/packages/gsd-cloud/dist/executors/index.d.ts.map +1 -0
  190. package/packages/gsd-cloud/dist/executors/index.js +29 -0
  191. package/packages/gsd-cloud/dist/executors/index.js.map +1 -0
  192. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts +36 -0
  193. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.d.ts.map +1 -0
  194. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js +176 -0
  195. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.js.map +1 -0
  196. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts +2 -0
  197. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.d.ts.map +1 -0
  198. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js +34 -0
  199. package/packages/gsd-cloud/dist/executors/mcp-stdio-client.test.js.map +1 -0
  200. package/packages/gsd-cloud/dist/index.d.ts +7 -0
  201. package/packages/gsd-cloud/dist/index.d.ts.map +1 -0
  202. package/packages/gsd-cloud/dist/index.js +8 -0
  203. package/packages/gsd-cloud/dist/index.js.map +1 -0
  204. package/packages/gsd-cloud/dist/inject-gateway.d.ts +16 -0
  205. package/packages/gsd-cloud/dist/inject-gateway.d.ts.map +1 -0
  206. package/packages/gsd-cloud/dist/inject-gateway.js +29 -0
  207. package/packages/gsd-cloud/dist/inject-gateway.js.map +1 -0
  208. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts +2 -0
  209. package/packages/gsd-cloud/dist/inject-gateway.test.d.ts.map +1 -0
  210. package/packages/gsd-cloud/dist/inject-gateway.test.js +41 -0
  211. package/packages/gsd-cloud/dist/inject-gateway.test.js.map +1 -0
  212. package/packages/gsd-cloud/dist/logger.d.ts +25 -0
  213. package/packages/gsd-cloud/dist/logger.d.ts.map +1 -0
  214. package/packages/gsd-cloud/dist/logger.js +72 -0
  215. package/packages/gsd-cloud/dist/logger.js.map +1 -0
  216. package/packages/gsd-cloud/dist/types.d.ts +51 -0
  217. package/packages/gsd-cloud/dist/types.d.ts.map +1 -0
  218. package/packages/gsd-cloud/dist/types.js +9 -0
  219. package/packages/gsd-cloud/dist/types.js.map +1 -0
  220. package/packages/gsd-cloud/package.json +3 -3
  221. package/packages/mcp-server/README.md +8 -0
  222. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  223. package/packages/mcp-server/dist/workflow-tools.js +64 -2
  224. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  225. package/packages/mcp-server/package.json +4 -4
  226. package/packages/native/package.json +1 -1
  227. package/packages/pi-agent-core/package.json +1 -1
  228. package/packages/pi-ai/dist/models.generated.d.ts +60 -0
  229. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  230. package/packages/pi-ai/dist/models.generated.js +54 -0
  231. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  232. package/packages/pi-ai/dist/providers/amazon-bedrock.js +1 -0
  233. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  234. package/packages/pi-ai/dist/providers/anthropic.js +19 -0
  235. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  236. package/packages/pi-ai/package.json +1 -1
  237. package/packages/pi-coding-agent/package.json +7 -7
  238. package/packages/pi-tui/package.json +2 -2
  239. package/packages/rpc-client/package.json +2 -2
  240. package/pkg/package.json +1 -1
  241. package/src/resources/GSD-WORKFLOW.md +16 -47
  242. package/src/resources/extensions/browser-tools/capture.ts +2 -86
  243. package/src/resources/extensions/browser-tools/screenshot-constraints.ts +94 -0
  244. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -1
  245. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +108 -0
  246. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +30 -2
  247. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  248. package/src/resources/extensions/gsd/auto-dispatch.ts +86 -19
  249. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -0
  250. package/src/resources/extensions/gsd/auto-prompts.ts +38 -1
  251. package/src/resources/extensions/gsd/auto.ts +5 -1
  252. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +91 -4
  253. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -24
  254. package/src/resources/extensions/gsd/command-feedback.ts +19 -12
  255. package/src/resources/extensions/gsd/compat/compat-marker.ts +21 -4
  256. package/src/resources/extensions/gsd/db/command-queue.ts +7 -1
  257. package/src/resources/extensions/gsd/db/engine.ts +53 -17
  258. package/src/resources/extensions/gsd/db/writers/cascades.ts +5 -1
  259. package/src/resources/extensions/gsd/db/writers/status.ts +25 -8
  260. package/src/resources/extensions/gsd/db-base-schema.ts +30 -0
  261. package/src/resources/extensions/gsd/db-migration-steps.ts +32 -0
  262. package/src/resources/extensions/gsd/db-schema-metadata.ts +4 -1
  263. package/src/resources/extensions/gsd/docs/preferences-reference.md +4 -4
  264. package/src/resources/extensions/gsd/flat-phase-migration.ts +160 -19
  265. package/src/resources/extensions/gsd/gsd-db.ts +255 -52
  266. package/src/resources/extensions/gsd/guided-flow.ts +39 -10
  267. package/src/resources/extensions/gsd/markdown-renderer.ts +23 -7
  268. package/src/resources/extensions/gsd/md-importer.ts +109 -11
  269. package/src/resources/extensions/gsd/migrate/command.ts +21 -227
  270. package/src/resources/extensions/gsd/migrate/execution.ts +136 -0
  271. package/src/resources/extensions/gsd/migrate/plan.ts +101 -0
  272. package/src/resources/extensions/gsd/migrate/planning-writer.ts +12 -20
  273. package/src/resources/extensions/gsd/migrate/presentation.ts +83 -0
  274. package/src/resources/extensions/gsd/parallel-eligibility.ts +5 -4
  275. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +37 -40
  276. package/src/resources/extensions/gsd/paths.ts +4 -1
  277. package/src/resources/extensions/gsd/phase-anchor.ts +8 -2
  278. package/src/resources/extensions/gsd/post-execution-checks.ts +4 -4
  279. package/src/resources/extensions/gsd/prompts/refine-slice.md +2 -2
  280. package/src/resources/extensions/gsd/safety/evidence-collector.ts +28 -1
  281. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +31 -5
  282. package/src/resources/extensions/gsd/state-reconciliation/drift/external-markdown-edit.ts +30 -13
  283. package/src/resources/extensions/gsd/state-reconciliation/drift/external-planning-edit.ts +16 -3
  284. package/src/resources/extensions/gsd/state-reconciliation/types.ts +2 -2
  285. package/src/resources/extensions/gsd/stop-notice.ts +18 -1
  286. package/src/resources/extensions/gsd/sync-lock.ts +14 -6
  287. package/src/resources/extensions/gsd/tests/closeout-git-deferral.test.ts +96 -1
  288. package/src/resources/extensions/gsd/tests/command-feedback.test.ts +12 -0
  289. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +104 -0
  290. package/src/resources/extensions/gsd/tests/complete-task.test.ts +286 -0
  291. package/src/resources/extensions/gsd/tests/db-engine-migrate-guards.test.ts +186 -0
  292. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +29 -0
  293. package/src/resources/extensions/gsd/tests/discuss-routing-fixes.test.ts +123 -0
  294. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +50 -0
  295. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +49 -0
  296. package/src/resources/extensions/gsd/tests/evidence-cross-ref.test.ts +81 -0
  297. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +22 -0
  298. package/src/resources/extensions/gsd/tests/external-edit-scoped-import.test.ts +262 -0
  299. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +202 -0
  300. package/src/resources/extensions/gsd/tests/gate-storage.test.ts +29 -0
  301. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +25 -0
  302. package/src/resources/extensions/gsd/tests/import-slice-milestone-completion-preserve.test.ts +116 -0
  303. package/src/resources/extensions/gsd/tests/migrate-plan.test.ts +68 -0
  304. package/src/resources/extensions/gsd/tests/migrate-presentation.test.ts +98 -0
  305. package/src/resources/extensions/gsd/tests/migrate-safety-audit.test.ts +1 -1
  306. package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +55 -0
  307. package/src/resources/extensions/gsd/tests/parallel-eligibility-file-overlap.test.ts +146 -0
  308. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +59 -0
  309. package/src/resources/extensions/gsd/tests/phase-anchor.test.ts +32 -4
  310. package/src/resources/extensions/gsd/tests/plan-task.test.ts +26 -23
  311. package/src/resources/extensions/gsd/tests/planning-writer.test.ts +29 -0
  312. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +22 -0
  313. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +50 -0
  314. package/src/resources/extensions/gsd/tests/replan-task-handler.test.ts +103 -0
  315. package/src/resources/extensions/gsd/tests/rework-brief.test.ts +197 -0
  316. package/src/resources/extensions/gsd/tests/skip-slice-cascades-tasks.test.ts +21 -0
  317. package/src/resources/extensions/gsd/tests/stop-notice.test.ts +17 -0
  318. package/src/resources/extensions/gsd/tests/sync-lock.test.ts +19 -0
  319. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +34 -0
  320. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +93 -1
  321. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -0
  322. package/src/resources/extensions/gsd/tests/workflow-migration-gates.test.ts +139 -0
  323. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +36 -0
  324. package/src/resources/extensions/gsd/tests/write-gate-seam.test.ts +52 -0
  325. package/src/resources/extensions/gsd/tools/complete-task.ts +120 -0
  326. package/src/resources/extensions/gsd/tools/plan-task.ts +14 -16
  327. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +18 -17
  328. package/src/resources/extensions/gsd/tools/replan-task.ts +160 -0
  329. package/src/resources/extensions/gsd/tools/rework-brief.ts +91 -0
  330. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +92 -0
  331. package/src/resources/extensions/gsd/types.ts +11 -0
  332. package/src/resources/extensions/gsd/unit-registry.ts +4 -0
  333. package/src/resources/extensions/gsd/uok/plan-v2.ts +20 -3
  334. package/src/resources/extensions/gsd/worktree-state-projection.ts +12 -0
  335. package/src/resources/extensions/mac-tools/index.ts +52 -9
  336. package/src/resources/extensions/mac-tools/tests/mac-screenshot-payload.test.cjs +70 -0
  337. package/dist/web/standalone/.next/static/css/18c2fdf261bfc1b7.css +0 -1
  338. /package/dist/web/standalone/.next/static/{UaXRrOYxBpG9QBINKhZkg → BE5XoklToI6lZjRzpXih8}/_buildManifest.js +0 -0
  339. /package/dist/web/standalone/.next/static/{UaXRrOYxBpG9QBINKhZkg → BE5XoklToI6lZjRzpXih8}/_ssgManifest.js +0 -0
@@ -57,6 +57,19 @@ test("needsFlatPhaseMigration returns false when no .gsd/milestones/", () => {
57
57
  assert.equal(needsFlatPhaseMigration(base), false);
58
58
  });
59
59
 
60
+ test("needsFlatPhaseMigration ignores legacy anchor runtime scaffolding", () => {
61
+ const base = mkdtempSync(join(tmpdir(), `gsd-anchor-nomig-${randomUUID()}`));
62
+ mkdirSync(join(base, ".gsd", "phases"), { recursive: true });
63
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "anchors"), { recursive: true });
64
+ writeFileSync(
65
+ join(base, ".gsd", "milestones", "M001", "anchors", "research-slice.json"),
66
+ "{}",
67
+ "utf-8",
68
+ );
69
+ tmpDirs.push(base);
70
+ assert.equal(needsFlatPhaseMigration(base), false);
71
+ });
72
+
60
73
  test("migrateToFlatPhase moves content from milestones/ to phases/", async () => {
61
74
  const base = makeTmp();
62
75
  await migrateToFlatPhase(base);
@@ -109,6 +122,59 @@ test("migrateToFlatPhase creates a backup", async () => {
109
122
  assert.ok(backups.length >= 1, "at least one migrate-* backup dir should exist");
110
123
  });
111
124
 
125
+ test("migrateToFlatPhase backs up existing phases projection before clearing it", async () => {
126
+ const base = makeTmp();
127
+ const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
128
+ mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
129
+ writeFileSync(reviewPath, "# Plan Review\n\nHand-authored review.", "utf-8");
130
+
131
+ await migrateToFlatPhase(base);
132
+
133
+ const backupRoot = join(base, ".gsd-backups");
134
+ const backups = readdirSync(backupRoot).filter(d => d.startsWith("migrate-"));
135
+ assert.equal(backups.length, 1, "one migrate backup should exist");
136
+ const backedUpReview = join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md");
137
+ assert.equal(
138
+ readFileSync(backedUpReview, "utf-8"),
139
+ "# Plan Review\n\nHand-authored review.",
140
+ "pre-existing phases/ files should have a recovery copy",
141
+ );
142
+ });
143
+
144
+ test("failed migration restores pre-existing phases projection from backup", async (t) => {
145
+ const base = makeTmp();
146
+ const phasesPath = join(base, ".gsd", "phases");
147
+ const reviewPath = join(phasesPath, "01-foundation", "PLAN-REVIEW.md");
148
+ mkdirSync(join(phasesPath, "01-foundation"), { recursive: true });
149
+ writeFileSync(reviewPath, "# Plan Review\n\nHand-authored review.", "utf-8");
150
+
151
+ let sabotagedClear = false;
152
+ const restoreFsOps = _setFlatPhaseMigrationFsOpsForTest({
153
+ rmSync(target, opts) {
154
+ rmSync(target, opts as never);
155
+ if (target === phasesPath && !sabotagedClear) {
156
+ sabotagedClear = true;
157
+ writeFileSync(phasesPath, "not a directory", "utf-8");
158
+ }
159
+ },
160
+ });
161
+ t.after(restoreFsOps);
162
+
163
+ await assert.rejects(migrateToFlatPhase(base), /flat-phase migration render failed/);
164
+
165
+ assert.equal(sabotagedClear, true, "test should force a post-clear render failure");
166
+ assert.equal(
167
+ readFileSync(reviewPath, "utf-8"),
168
+ "# Plan Review\n\nHand-authored review.",
169
+ "rollback should restore the pre-existing phases/ files",
170
+ );
171
+ const backupRoot = join(base, ".gsd-backups");
172
+ const leaked = existsSync(backupRoot)
173
+ ? readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"))
174
+ : [];
175
+ assert.equal(leaked.length, 0, "rollback should still clean the backup it created");
176
+ });
177
+
112
178
  test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir", async (t) => {
113
179
  const base = makeTmp();
114
180
  const milestonesPath = join(base, ".gsd", "milestones");
@@ -136,6 +202,33 @@ test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir",
136
202
  assert.equal(leaked.length, 0, "rollback must delete the migrate-* backup it created");
137
203
  });
138
204
 
205
+ test("resumed migration stores phases backup in retained migrate snapshot", async () => {
206
+ const base = makeTmp();
207
+ const milestonesPath = join(base, ".gsd", "milestones");
208
+ const migratingPath = join(base, ".gsd", "milestones.migrating");
209
+ const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
210
+ mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
211
+ writeFileSync(reviewPath, "# Plan Review\n\nResume recovery copy.", "utf-8");
212
+ renameSync(milestonesPath, migratingPath);
213
+
214
+ await migrateToFlatPhase(base);
215
+
216
+ assert.equal(existsSync(migratingPath), false, "resume staging dir should be removed after success");
217
+ const backupRoot = join(base, ".gsd-backups");
218
+ const backups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
219
+ assert.equal(backups.length, 1, "resumed migration should create one retained migrate backup");
220
+ assert.equal(
221
+ readFileSync(join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md"), "utf-8"),
222
+ "# Plan Review\n\nResume recovery copy.",
223
+ "resume should retain the phases snapshot under .gsd-backups",
224
+ );
225
+ assert.equal(
226
+ existsSync(join(migratingPath, "__phases")),
227
+ false,
228
+ "resume should not store the phases snapshot inside the disposable migrating tree",
229
+ );
230
+ });
231
+
139
232
  test("migrateToFlatPhase preserves milestone/slice/task counts in DB", async () => {
140
233
  const base = makeTmp();
141
234
  const msBefore = getAllMilestones().length;
@@ -190,6 +283,42 @@ test("migrateToFlatPhase prunes legacy milestones artifact rows after flat rende
190
283
  );
191
284
  });
192
285
 
286
+ test("migrateToFlatPhase prunes stale flat-phase artifact rows that renderAll intentionally skips", async () => {
287
+ const base = makeTmp();
288
+ insertArtifact({
289
+ path: "phases/01-foundation/T01-PLAN.md",
290
+ artifact_type: "PLAN",
291
+ milestone_id: "M001",
292
+ slice_id: "S01",
293
+ task_id: "T01",
294
+ full_content: "# Legacy standalone task plan\n",
295
+ });
296
+ insertArtifact({
297
+ path: "phases/01-foundation/01-01-RESEARCH.md",
298
+ artifact_type: "RESEARCH",
299
+ milestone_id: "M001",
300
+ slice_id: "S01",
301
+ task_id: null,
302
+ full_content: "",
303
+ });
304
+
305
+ await migrateToFlatPhase(base);
306
+
307
+ const rows = _getAdapter()!
308
+ .prepare("SELECT path FROM artifacts ORDER BY path")
309
+ .all() as Array<{ path: string }>;
310
+ assert.equal(
311
+ rows.some((row) => row.path === "phases/01-foundation/T01-PLAN.md"),
312
+ false,
313
+ "standalone task PLAN rows should be pruned when the flat renderer does not recreate the file",
314
+ );
315
+ assert.equal(
316
+ rows.some((row) => row.path === "phases/01-foundation/01-01-RESEARCH.md"),
317
+ false,
318
+ "empty-content artifact rows should be pruned when the flat renderer skips the file",
319
+ );
320
+ });
321
+
193
322
  test("migrateToFlatPhase is idempotent (second run is a no-op)", async () => {
194
323
  const base = makeTmp();
195
324
  await migrateToFlatPhase(base);
@@ -199,6 +328,79 @@ test("migrateToFlatPhase is idempotent (second run is a no-op)", async () => {
199
328
  assert.equal(backups.length, 1, "should only have one backup");
200
329
  });
201
330
 
331
+ test("re-fired migration reuses the existing backup instead of leaking a new migrate-* dir (#1292)", async () => {
332
+ const base = makeTmp();
333
+ await migrateToFlatPhase(base);
334
+ const backupRoot = join(base, ".gsd-backups");
335
+ const firstBackups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
336
+ assert.equal(firstBackups.length, 1, "first migration snapshots exactly one backup");
337
+
338
+ // Simulate the re-fire: the legacy .gsd/milestones/ layout reappears (e.g. a
339
+ // marker-key mismatch re-imports the whole tree). The DB rows still exist, so
340
+ // the migration gate proceeds again — it must not snapshot a second backup.
341
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01"), { recursive: true });
342
+ assert.equal(needsFlatPhaseMigration(base), true, "reappeared legacy layout re-triggers migration");
343
+
344
+ await migrateToFlatPhase(base);
345
+
346
+ const afterBackups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
347
+ assert.deepEqual(afterBackups, firstBackups, "re-fire must not leak a second migrate-* backup");
348
+ assert.equal(existsSync(join(base, ".gsd", "milestones")), false, "legacy milestones/ removed again");
349
+ });
350
+
351
+ test("rollback after a re-fired migration preserves the reused migrate-* backup", async (t) => {
352
+ const base = makeTmp();
353
+ await migrateToFlatPhase(base);
354
+ const backupRoot = join(base, ".gsd-backups");
355
+ const firstBackups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
356
+ assert.equal(firstBackups.length, 1, "first migration snapshots exactly one backup");
357
+
358
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01"), { recursive: true });
359
+ const milestonesPath = join(base, ".gsd", "milestones");
360
+ const phasesPath = join(base, ".gsd", "phases");
361
+
362
+ const restoreFsOps = _setFlatPhaseMigrationFsOpsForTest({
363
+ rmSync(target, opts) {
364
+ if (target === phasesPath) {
365
+ throw Object.assign(new Error("simulated locked phases/"), { code: "EPERM" });
366
+ }
367
+ return rmSync(target, opts as never);
368
+ },
369
+ });
370
+ t.after(restoreFsOps);
371
+
372
+ await assert.rejects(migrateToFlatPhase(base), /simulated locked/);
373
+
374
+ const afterBackups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
375
+ assert.deepEqual(afterBackups, firstBackups, "rollback must preserve the reused backup");
376
+ assert.ok(existsSync(milestonesPath), "legacy milestones/ should be restored on rollback");
377
+ });
378
+
379
+ test("migration ignores an empty/partial leftover backup and writes a complete one (#1292)", async () => {
380
+ const base = makeTmp();
381
+
382
+ // Simulate a prior first-time backup that crashed mid-cpSync: an empty
383
+ // migrate-<ts>/ leftover with no milestones/ content. It is the newest entry,
384
+ // so mtime-based selection would reuse it as the rollback copy if content were
385
+ // not validated — silently retaining a recovery copy missing the legacy tree.
386
+ const backupRoot = join(base, ".gsd-backups");
387
+ const emptyLeftover = join(backupRoot, "migrate-crashed");
388
+ mkdirSync(emptyLeftover, { recursive: true });
389
+
390
+ await migrateToFlatPhase(base);
391
+
392
+ assert.ok(existsSync(join(base, ".gsd", "phases", "01-foundation")), "flat phase should render");
393
+ // The empty leftover must not have been reused; a fresh, content-bearing
394
+ // backup that actually captured the legacy milestones/ tree must be created.
395
+ const contentBackups = readdirSync(backupRoot)
396
+ .filter((d) => d.startsWith("migrate-"))
397
+ .filter((d) => existsSync(join(backupRoot, d, "M001")));
398
+ assert.ok(
399
+ contentBackups.length >= 1,
400
+ "a complete backup containing the legacy milestones/ tree must be created, not the empty leftover",
401
+ );
402
+ });
403
+
202
404
  test("migrateToFlatPhase preserves slice sidecar artifacts and skips recovery placeholder PLAN", async () => {
203
405
  const base = makeTmp({ withTask: false });
204
406
  const legacySliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
@@ -19,6 +19,11 @@ import {
19
19
  insertMilestone,
20
20
  insertSlice,
21
21
  } from "../gsd-db.ts";
22
+ import { getDb } from "../db/engine.ts";
23
+
24
+ function gateRunCount(gateId: string): number {
25
+ return (getDb().prepare("SELECT count(*) AS cnt FROM gate_runs WHERE gate_id = :gid").get({ ":gid": gateId }) as { cnt: number }).cnt;
26
+ }
22
27
 
23
28
  describe("quality_gates CRUD", () => {
24
29
  let tmpDir: string;
@@ -83,6 +88,30 @@ describe("quality_gates CRUD", () => {
83
88
  assert.ok(results[0].evaluated_at);
84
89
  });
85
90
 
91
+ test("saveGateResult commits the verdict and the gate_runs ledger row together", () => {
92
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
93
+ saveGateResult({
94
+ milestoneId: "M001", sliceId: "S01", gateId: "Q3",
95
+ verdict: "pass", rationale: "OK", findings: "",
96
+ });
97
+ assert.equal(getGateResults("M001", "S01")[0].status, "complete");
98
+ assert.equal(gateRunCount("Q3"), 1, "exactly one ledger row must accompany the verdict");
99
+ });
100
+
101
+ test("saveGateResult rolls back the verdict UPDATE if the ledger INSERT fails", () => {
102
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
103
+ // Force the insertGateRun step to throw after the UPDATE by removing the
104
+ // ledger table. Atomicity means the gate row must stay pending.
105
+ getDb().exec("DROP TABLE gate_runs");
106
+ assert.throws(() => saveGateResult({
107
+ milestoneId: "M001", sliceId: "S01", gateId: "Q3",
108
+ verdict: "pass", rationale: "OK", findings: "",
109
+ }));
110
+ const gate = getGateResults("M001", "S01")[0];
111
+ assert.equal(gate.status, "pending", "gate verdict UPDATE must roll back when the ledger INSERT fails");
112
+ assert.equal(gate.verdict ?? "", "", "verdict must not persist after rollback");
113
+ });
114
+
86
115
  test("saveGateResult throws when the gate row does not exist", () => {
87
116
  assert.throws(
88
117
  () => saveGateResult({
@@ -280,6 +280,31 @@ describe('gsd-db', () => {
280
280
  }
281
281
  });
282
282
 
283
+ test("gsd-db: insertTask stamps completed_at for every complete-alias but not for skipped", () => {
284
+ openDatabase(":memory:");
285
+
286
+ try {
287
+ insertMilestone({ id: "M001", title: "Completion stamp", status: "active" });
288
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", sequence: 1 });
289
+ insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Closed", status: "closed", sequence: 1 });
290
+ insertTask({ id: "T02", sliceId: "S01", milestoneId: "M001", title: "Done", status: "done", sequence: 2 });
291
+ insertTask({ id: "T03", sliceId: "S01", milestoneId: "M001", title: "Skipped", status: "skipped", sequence: 3 });
292
+
293
+ assert.ok(
294
+ getTask("M001", "S01", "T01")?.completed_at,
295
+ "a task imported as 'closed' is terminal and must carry a completed_at",
296
+ );
297
+ assert.ok(getTask("M001", "S01", "T02")?.completed_at, "'done' alias must stamp completed_at");
298
+ assert.equal(
299
+ getTask("M001", "S01", "T03")?.completed_at,
300
+ null,
301
+ "a skipped task was never completed and must have null completed_at",
302
+ );
303
+ } finally {
304
+ closeDatabase();
305
+ }
306
+ });
307
+
283
308
  test('gsd-db: active_decisions view excludes superseded', () => {
284
309
  openDatabase(':memory:');
285
310
 
@@ -0,0 +1,116 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Regression for #1291 — a full markdown re-import must NOT rewrite
3
+ // the completed_at of an already-complete slice or milestone with the current
4
+ // import time. The task path was fixed for this (#1222/#1228) via
5
+ // preserveCompletionMetadata; the slice and milestone backfills in
6
+ // migrateHierarchyToDb had no such guard, so every re-import re-stamped every
7
+ // complete slice/milestone. The fix threads preserveCompletion through
8
+ // updateSliceStatus/updateMilestoneStatus so an existing completed_at survives.
9
+
10
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+
14
+ import {
15
+ openDatabase,
16
+ closeDatabase,
17
+ getAllMilestones,
18
+ getMilestoneSlices,
19
+ updateSliceStatus,
20
+ updateMilestoneStatus,
21
+ } from '../gsd-db.ts';
22
+ import { migrateHierarchyToDb } from '../md-importer.ts';
23
+ import { describe, test, afterEach } from 'node:test';
24
+ import assert from 'node:assert/strict';
25
+
26
+ const tmpDirs: string[] = [];
27
+ afterEach(() => {
28
+ closeDatabase();
29
+ for (const d of tmpDirs) { try { rmSync(d, { recursive: true, force: true }); } catch { /* */ } }
30
+ tmpDirs.length = 0;
31
+ });
32
+
33
+ function createFixtureBase(): string {
34
+ const base = mkdtempSync(join(tmpdir(), 'gsd-preserve-completion-'));
35
+ tmpDirs.push(base);
36
+ return base;
37
+ }
38
+
39
+ function writeFile(base: string, relativePath: string, content: string): void {
40
+ const full = join(base, '.gsd', relativePath);
41
+ mkdirSync(join(full, '..'), { recursive: true });
42
+ writeFileSync(full, content);
43
+ }
44
+
45
+ // All slices [x] → milestone imports complete, slices import complete.
46
+ const ROADMAP_ALL_DONE = `# M001: Finished Milestone
47
+
48
+ **Vision:** Done work.
49
+
50
+ ## Slices
51
+
52
+ - [x] **S01: First Slice** \`risk:low\` \`depends:[]\`
53
+ > After this: Done.
54
+
55
+ - [x] **S02: Second Slice** \`risk:medium\` \`depends:[S01]\`
56
+ > After this: Also done.
57
+ `;
58
+
59
+ const REAL_S01 = '2026-07-05T19:52:04.672Z';
60
+ const REAL_S02 = '2026-07-05T20:36:51.313Z';
61
+ const REAL_M001 = '2026-07-05T21:00:00.000Z';
62
+
63
+ describe('migrateHierarchyToDb: re-import preserves existing completion timestamps (#1291)', () => {
64
+ test('a re-import does not rewrite already-complete slice completed_at', () => {
65
+ const base = createFixtureBase();
66
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_ALL_DONE);
67
+
68
+ openDatabase(join(base, '.gsd', 'gsd.db'));
69
+ migrateHierarchyToDb(base);
70
+
71
+ // Simulate the durable completion timestamps the real run wrote hours apart.
72
+ updateSliceStatus('M001', 'S01', 'complete', REAL_S01);
73
+ updateSliceStatus('M001', 'S02', 'complete', REAL_S02);
74
+
75
+ // A second full re-import (the class of event that bit the reporter).
76
+ migrateHierarchyToDb(base);
77
+
78
+ const slices = getMilestoneSlices('M001');
79
+ const s01 = slices.find((s) => s.id === 'S01')!;
80
+ const s02 = slices.find((s) => s.id === 'S02')!;
81
+ assert.equal(s01.completed_at, REAL_S01, 'S01 completed_at must survive re-import unchanged');
82
+ assert.equal(s02.completed_at, REAL_S02, 'S02 completed_at must survive re-import unchanged');
83
+ });
84
+
85
+ test('a re-import does not rewrite an already-complete milestone completed_at', () => {
86
+ const base = createFixtureBase();
87
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_ALL_DONE);
88
+
89
+ openDatabase(join(base, '.gsd', 'gsd.db'));
90
+ migrateHierarchyToDb(base);
91
+
92
+ updateMilestoneStatus('M001', 'complete', REAL_M001);
93
+
94
+ migrateHierarchyToDb(base);
95
+
96
+ const m001 = getAllMilestones().find((m) => m.id === 'M001')!;
97
+ assert.equal(m001.completed_at, REAL_M001, 'M001 completed_at must survive re-import unchanged');
98
+ });
99
+
100
+ test('an initial import of a complete slice still backfills a completed_at when the row has none', () => {
101
+ // The guard preserves an existing timestamp but must not suppress the
102
+ // legitimate first-time backfill for a slice imported as complete.
103
+ const base = createFixtureBase();
104
+ writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_ALL_DONE);
105
+
106
+ openDatabase(join(base, '.gsd', 'gsd.db'));
107
+ migrateHierarchyToDb(base);
108
+
109
+ const slices = getMilestoneSlices('M001');
110
+ for (const s of slices) {
111
+ assert.ok(s.completed_at, `complete slice ${s.id} should receive a backfilled completed_at on first import`);
112
+ }
113
+ const m001 = getAllMilestones().find((m) => m.id === 'M001')!;
114
+ assert.ok(m001.completed_at, 'complete milestone should receive a backfilled completed_at on first import');
115
+ });
116
+ });
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { createMigrationPlan, splitValidationIssues } from "../migrate/plan.ts";
8
+ import type { ValidationResult } from "../migrate/types.ts";
9
+
10
+ function makeBase(prefix: string): string {
11
+ return mkdtempSync(join(tmpdir(), prefix));
12
+ }
13
+
14
+ function write(path: string, content: string): void {
15
+ mkdirSync(join(path, ".."), { recursive: true });
16
+ writeFileSync(path, content);
17
+ }
18
+
19
+ test("splitValidationIssues separates warnings from fatals without dropping issue order", () => {
20
+ const validation: ValidationResult = {
21
+ valid: false,
22
+ issues: [
23
+ { file: "ROADMAP.md", severity: "warning", message: "missing roadmap" },
24
+ { file: ".planning", severity: "fatal", message: "missing directory" },
25
+ { file: "PROJECT.md", severity: "warning", message: "missing project" },
26
+ ],
27
+ };
28
+
29
+ assert.deepEqual(splitValidationIssues(validation), {
30
+ warnings: [
31
+ { file: "ROADMAP.md", severity: "warning", message: "missing roadmap" },
32
+ { file: "PROJECT.md", severity: "warning", message: "missing project" },
33
+ ],
34
+ fatals: [
35
+ { file: ".planning", severity: "fatal", message: "missing directory" },
36
+ ],
37
+ });
38
+ });
39
+
40
+ test("createMigrationPlan reports a missing .planning source without parsing", async () => {
41
+ const base = makeBase("gsd-migrate-plan-missing-");
42
+ try {
43
+ const result = await createMigrationPlan(base);
44
+
45
+ assert.equal(result.status, "missing-source");
46
+ assert.equal(result.sourcePath, join(base, ".planning"));
47
+ assert.equal(result.targetRoot, base);
48
+ } finally {
49
+ rmSync(base, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ test("createMigrationPlan reports zero-slice migrations as blocked after validation", async () => {
54
+ const base = makeBase("gsd-migrate-plan-blocked-");
55
+ try {
56
+ mkdirSync(join(base, ".planning"), { recursive: true });
57
+ write(join(base, ".planning", "PROJECT.md"), "# Legacy Project\n");
58
+
59
+ const result = await createMigrationPlan(base);
60
+
61
+ assert.equal(result.status, "blocked");
62
+ assert.match(result.message, /zero slices/);
63
+ assert.equal(result.validation.valid, true);
64
+ assert.ok(result.warnings.some((issue) => issue.file === "ROADMAP.md"));
65
+ } finally {
66
+ rmSync(base, { recursive: true, force: true });
67
+ }
68
+ });
@@ -0,0 +1,98 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ buildMigrationPreviewSummary,
9
+ buildReviewPrompt,
10
+ formatMigrationPreviewStats,
11
+ } from "../migrate/presentation.ts";
12
+ import { gsdRoot } from "../paths.ts";
13
+ import type { MigrationPreview } from "../migrate/writer.ts";
14
+
15
+ function previewFixture(overrides: Partial<MigrationPreview> = {}): MigrationPreview {
16
+ return {
17
+ decisions: { total: 2 },
18
+ migrationInputs: {
19
+ milestonePhaseDirs: 3,
20
+ decisions: 2,
21
+ seeds: 1,
22
+ },
23
+ milestoneCount: 1,
24
+ totalSlices: 2,
25
+ totalTasks: 5,
26
+ doneSlices: 1,
27
+ doneTasks: 3,
28
+ sliceCompletionPct: 50,
29
+ taskCompletionPct: 60,
30
+ requirements: {
31
+ active: 1,
32
+ validated: 2,
33
+ deferred: 1,
34
+ outOfScope: 1,
35
+ total: 5,
36
+ },
37
+ ...overrides,
38
+ };
39
+ }
40
+
41
+ test("buildMigrationPreviewSummary includes counts, legacy inputs, and existing-target warning", () => {
42
+ const base = mkdtempSync(join(tmpdir(), "gsd-migrate-presentation-"));
43
+ try {
44
+ mkdirSync(join(base, ".gsd"), { recursive: true });
45
+
46
+ assert.deepEqual(buildMigrationPreviewSummary(previewFixture(), base), [
47
+ "Decisions: 2",
48
+ "Milestones: 1",
49
+ "Slices: 2 (1 done — 50%)",
50
+ "Tasks: 5 (3 done — 60%)",
51
+ "Requirements: 5 (2 validated, 1 active, 1 deferred, 1 out of scope)",
52
+ "Legacy inputs: 3 milestone phase dir(s), 2 decision file(s), 1 seed file(s)",
53
+ "",
54
+ `⚠ A .gsd directory already exists at ${gsdRoot(base)}.`,
55
+ "It will be backed up, deleted, and rewritten fresh before DB import.",
56
+ ]);
57
+ } finally {
58
+ rmSync(base, { recursive: true, force: true });
59
+ }
60
+ });
61
+
62
+ test("formatMigrationPreviewStats uses bullet formatting for review prompts", () => {
63
+ assert.equal(
64
+ formatMigrationPreviewStats(previewFixture({ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, total: 0 } })),
65
+ [
66
+ "- Decisions: 2",
67
+ "- Milestones: 1",
68
+ "- Slices: 2 (1 done — 50%)",
69
+ "- Tasks: 5 (3 done — 60%)",
70
+ "- Legacy inputs: 3 milestone phase dir(s), 2 decision file(s), 1 seed file(s)",
71
+ ].join("\n"),
72
+ );
73
+ });
74
+
75
+ test("buildReviewPrompt interpolates source, target, and preview stats", () => {
76
+ const base = mkdtempSync(join(tmpdir(), "gsd-migrate-prompt-"));
77
+ try {
78
+ const promptsDir = join(base, "prompts");
79
+ mkdirSync(promptsDir, { recursive: true });
80
+ writeFileSync(
81
+ join(promptsDir, "review-migration.md"),
82
+ "Source={{sourcePath}}\nTarget={{gsdPath}}\nStats:\n{{previewStats}}\n",
83
+ );
84
+
85
+ const prompt = buildReviewPrompt({
86
+ sourcePath: "/legacy/.planning",
87
+ gsdPath: "/project/.gsd",
88
+ preview: previewFixture(),
89
+ templatePath: join(promptsDir, "review-migration.md"),
90
+ });
91
+
92
+ assert.match(prompt, /Source=\/legacy\/\.planning/);
93
+ assert.match(prompt, /Target=\/project\/\.gsd/);
94
+ assert.match(prompt, /- Requirements: 5 \(2 validated, 1 active, 1 deferred, 1 out of scope\)/);
95
+ } finally {
96
+ rmSync(base, { recursive: true, force: true });
97
+ }
98
+ });
@@ -19,7 +19,7 @@ import {
19
19
  archiveLegacyPlanningDirectory,
20
20
  verifyMigrationProjection,
21
21
  } from "../migrate/audit.ts";
22
- import { assertMigrationDbReadiness, executeMigrationWrite, importWrittenMigrationToDb } from "../migrate/command.ts";
22
+ import { assertMigrationDbReadiness, executeMigrationWrite, importWrittenMigrationToDb } from "../migrate/execution.ts";
23
23
  import { writeGSDDirectory } from "../migrate/writer.ts";
24
24
  import { closeDatabase, getArtifact } from "../gsd-db.ts";
25
25
  import type { GSDProject } from "../migrate/types.ts";
@@ -18,6 +18,7 @@ import {
18
18
  releaseMilestoneLease,
19
19
  refreshMilestoneLease,
20
20
  getMilestoneLease,
21
+ forceReleaseLeasesForWorker,
21
22
  } from "../db/milestone-leases.ts";
22
23
 
23
24
  function makeBase(): string {
@@ -163,6 +164,60 @@ test("refreshMilestoneLease only succeeds with the matching fencing token", (t)
163
164
  assert.equal(refreshMilestoneLease(w1, "M001", claim.token - 1), false);
164
165
  });
165
166
 
167
+ test("forceReleaseLeasesForWorker frees only the dead worker's leases, not a live peer's", (t) => {
168
+ const base = makeBase();
169
+ t.after(() => cleanup(base));
170
+ openDatabase(join(base, ".gsd", "gsd.db"));
171
+ insertMilestone({ id: "M001", title: "One", status: "active" });
172
+ insertMilestone({ id: "M002", title: "Two", status: "active" });
173
+
174
+ // Distinct project roots → distinct processes (not re-entrant peers).
175
+ const wA = registerAutoWorker({ projectRootRealpath: base });
176
+ const wB = registerAutoWorker({ projectRootRealpath: join(base, "peer") });
177
+ assert.equal(claimMilestoneLease(wA, "M001").ok, true);
178
+ assert.equal(claimMilestoneLease(wB, "M002").ok, true);
179
+
180
+ const freed = forceReleaseLeasesForWorker(wA);
181
+ assert.equal(freed, 1, "exactly A's one held lease is released");
182
+
183
+ assert.equal(getMilestoneLease("M001")!.status, "released", "A's M001 lease is released");
184
+ const m2 = getMilestoneLease("M002")!;
185
+ assert.equal(m2.status, "held", "B's M002 lease must stay held (no over-release)");
186
+ assert.equal(m2.worker_id, wB);
187
+ });
188
+
189
+ test("after force release a fresh claim on the freed milestone gets a larger fencing token", (t) => {
190
+ const base = makeBase();
191
+ t.after(() => cleanup(base));
192
+ openDatabase(join(base, ".gsd", "gsd.db"));
193
+ insertMilestone({ id: "M001", title: "One", status: "active" });
194
+
195
+ const wA = registerAutoWorker({ projectRootRealpath: base });
196
+ const wB = registerAutoWorker({ projectRootRealpath: join(base, "peer") });
197
+ const first = claimMilestoneLease(wA, "M001");
198
+ assert.equal(first.ok, true);
199
+ const releasedToken = first.ok ? first.token : 0;
200
+
201
+ assert.equal(forceReleaseLeasesForWorker(wA), 1);
202
+
203
+ const takeover = claimMilestoneLease(wB, "M001");
204
+ assert.equal(takeover.ok, true);
205
+ if (takeover.ok) {
206
+ assert.ok(takeover.token > releasedToken, "takeover token must exceed the released lease's token");
207
+ }
208
+ assert.equal(getMilestoneLease("M001")!.worker_id, wB);
209
+ });
210
+
211
+ test("forceReleaseLeasesForWorker returns 0 for a worker holding no leases", (t) => {
212
+ const base = makeBase();
213
+ t.after(() => cleanup(base));
214
+ openDatabase(join(base, ".gsd", "gsd.db"));
215
+ insertMilestone({ id: "M001", title: "One", status: "active" });
216
+
217
+ const wA = registerAutoWorker({ projectRootRealpath: base });
218
+ assert.equal(forceReleaseLeasesForWorker(wA), 0);
219
+ });
220
+
166
221
  test("claimMilestoneLease rethrows foreign-key failures instead of treating them as lease contention", (t) => {
167
222
  const base = makeBase();
168
223
  t.after(() => cleanup(base));