@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
@@ -17,6 +17,7 @@ import {
17
17
  insertVerificationEvidence,
18
18
  insertGateRow,
19
19
  getGateResults,
20
+ saveReworkBrief,
20
21
  updateMilestoneStatus,
21
22
  updateSliceStatus,
22
23
  SCHEMA_VERSION,
@@ -507,6 +508,77 @@ console.log('\n=== complete-task: disabled hard-blocker escalation rolls back co
507
508
  cleanup(dbPath);
508
509
  }
509
510
 
511
+ // ═══════════════════════════════════════════════════════════════════════════
512
+ // complete-task: rollback reverts applied rework resolutions
513
+ // ═══════════════════════════════════════════════════════════════════════════
514
+
515
+ console.log('\n=== complete-task: rollback reverts applied rework resolutions ===');
516
+ {
517
+ const dbPath = tempDbPath();
518
+ openDatabase(dbPath);
519
+
520
+ const { basePath } = createTempProject();
521
+ writeProjectPreferences(basePath, 'phases:\n mid_execution_escalation: false\n');
522
+
523
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
524
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
525
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
526
+ saveReworkBrief({
527
+ briefId: 'RB-001',
528
+ milestoneId: 'M001',
529
+ sliceId: 'S01',
530
+ taskId: 'T01',
531
+ findings: [{
532
+ findingId: 'F1',
533
+ severity: 'blocking',
534
+ description: 'Compile regression',
535
+ requiredFix: 'Fix compile error',
536
+ verificationCommands: ['pnpm run typecheck:extensions'],
537
+ }],
538
+ });
539
+
540
+ // Completion resolves the blocking finding but then rolls back because the
541
+ // hard-blocker escalation is disabled. The rollback must also revert the
542
+ // finding. Otherwise a retry would see no unresolved blocking findings and
543
+ // silently skip the blocking gate.
544
+ const result = await withWorkingDirectory(basePath, () => handleCompleteTask({
545
+ ...makeValidParams(),
546
+ blockerDiscovered: true,
547
+ reworkResolution: [{
548
+ findingId: 'F1',
549
+ status: 'resolved',
550
+ evidence: 'Fixed compile error and reran pnpm run typecheck:extensions.',
551
+ }],
552
+ escalation: {
553
+ question: 'Should execution pause for the hard blocker?',
554
+ options: makeEscalationOptions(),
555
+ recommendation: 'pause',
556
+ recommendationRationale: 'The blocker should not be silently advanced.',
557
+ continueWithDefault: false,
558
+ },
559
+ }, basePath));
560
+
561
+ assertTrue('error' in result, 'completion should roll back when hard-blocker escalation is disabled');
562
+ assertEq(getTask('M001', 'S01', 'T01')?.status, 'pending', 'task status should roll back to pending');
563
+
564
+ const finding = _getAdapter()!.prepare(
565
+ "SELECT status, evidence, decision_ref FROM rework_brief_findings WHERE brief_id = 'RB-001' AND finding_id = 'F1'"
566
+ ).get() as { status: string; evidence: string; decision_ref: string };
567
+ assertEq(finding.status, 'pending', 'rework finding should revert to pending after rollback');
568
+ assertEq(finding.evidence, '', 'rework finding evidence should be cleared after rollback');
569
+
570
+ // Retry-safety invariant: the blocking gate must fire again because the
571
+ // finding is unresolved once more.
572
+ const retry = await withWorkingDirectory(basePath, () => handleCompleteTask(makeValidParams(), basePath));
573
+ assertTrue('error' in retry, 'retry without reworkResolution should be blocked again');
574
+ if ('error' in retry) {
575
+ assertMatch(retry.error, /unresolved blocking rework finding/i, 'retry should re-trigger the blocking gate');
576
+ }
577
+
578
+ cleanupDir(basePath);
579
+ cleanup(dbPath);
580
+ }
581
+
510
582
  // ═══════════════════════════════════════════════════════════════════════════
511
583
  // complete-task: soft escalation with mid-execution escalation disabled
512
584
  // ═══════════════════════════════════════════════════════════════════════════
@@ -838,6 +910,220 @@ console.log('\n=== complete-task: closes Q5/Q6/Q7 gates (pass vs omitted) ===');
838
910
  cleanup(dbPath);
839
911
  }
840
912
 
913
+
914
+ // ═══════════════════════════════════════════════════════════════════════════
915
+ // complete-task: unresolved blocking rework findings reject completion
916
+ // ═══════════════════════════════════════════════════════════════════════════
917
+
918
+ console.log('\n=== complete-task: rejects unresolved blocking rework findings ===');
919
+ {
920
+ const dbPath = tempDbPath();
921
+ openDatabase(dbPath);
922
+ const { basePath } = createTempProject();
923
+
924
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
925
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
926
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
927
+
928
+ saveReworkBrief({
929
+ briefId: 'RB-001',
930
+ milestoneId: 'M001',
931
+ sliceId: 'S01',
932
+ taskId: 'T01',
933
+ findings: [{
934
+ findingId: 'F1',
935
+ severity: 'blocking',
936
+ description: 'Compile regression',
937
+ requiredFix: 'Fix compile error',
938
+ verificationCommands: ['pnpm run typecheck:extensions'],
939
+ }],
940
+ });
941
+
942
+ const result = await handleCompleteTask(makeValidParams(), basePath);
943
+ assertTrue('error' in result, 'completion should reject unresolved blocking rework findings');
944
+ if ('error' in result) {
945
+ assertMatch(result.error, /unresolved blocking rework finding/i, 'error should explain unresolved rework');
946
+ assertMatch(result.error, /F1/, 'error should name the unresolved finding id');
947
+ }
948
+
949
+ cleanupDir(basePath);
950
+ cleanup(dbPath);
951
+ }
952
+
953
+
954
+ console.log('\n=== complete-task: accepts resolved blocking rework evidence ===');
955
+ {
956
+ const dbPath = tempDbPath();
957
+ openDatabase(dbPath);
958
+ const { basePath } = createTempProject();
959
+
960
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
961
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
962
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
963
+ saveReworkBrief({
964
+ briefId: 'RB-001',
965
+ milestoneId: 'M001',
966
+ sliceId: 'S01',
967
+ taskId: 'T01',
968
+ findings: [{
969
+ findingId: 'F1',
970
+ severity: 'blocking',
971
+ description: 'Compile regression',
972
+ requiredFix: 'Fix compile error',
973
+ verificationCommands: ['pnpm run typecheck:extensions'],
974
+ }],
975
+ });
976
+
977
+ const result = await handleCompleteTask({
978
+ ...makeValidParams(),
979
+ reworkResolution: [{
980
+ findingId: 'F1',
981
+ status: 'resolved',
982
+ evidence: 'Fixed compile error and reran pnpm run typecheck:extensions.',
983
+ }],
984
+ }, basePath);
985
+
986
+ assertTrue(!('error' in result), 'completion should accept resolved blocking rework evidence');
987
+ assertEq(getTask('M001', 'S01', 'T01')?.status, 'complete', 'task should complete after resolving rework');
988
+ const finding = _getAdapter()!.prepare("SELECT status, evidence FROM rework_brief_findings WHERE brief_id = 'RB-001' AND finding_id = 'F1'").get() as { status: string; evidence: string };
989
+ assertEq(finding.status, 'resolved', 'finding status should be updated');
990
+ assertMatch(finding.evidence, /Fixed compile error/, 'resolution evidence should be persisted');
991
+
992
+ cleanupDir(basePath);
993
+ cleanup(dbPath);
994
+ }
995
+
996
+ console.log('\n=== complete-task: accepts deferred blocking rework with decision reference ===');
997
+ {
998
+ const dbPath = tempDbPath();
999
+ openDatabase(dbPath);
1000
+ const { basePath } = createTempProject();
1001
+
1002
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
1003
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
1004
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
1005
+ saveReworkBrief({
1006
+ briefId: 'RB-001',
1007
+ milestoneId: 'M001',
1008
+ sliceId: 'S01',
1009
+ taskId: 'T01',
1010
+ findings: [{
1011
+ findingId: 'F1',
1012
+ severity: 'blocking',
1013
+ description: 'Compile regression',
1014
+ requiredFix: 'Fix compile error',
1015
+ verificationCommands: ['pnpm run typecheck:extensions'],
1016
+ }],
1017
+ });
1018
+
1019
+ const result = await handleCompleteTask({
1020
+ ...makeValidParams(),
1021
+ reworkResolution: [{
1022
+ findingId: 'F1',
1023
+ status: 'deferred-with-override',
1024
+ evidence: 'Maintainer accepted temporary deferral.',
1025
+ decisionRef: 'DEC-2026-07-07-rework-deferral',
1026
+ }],
1027
+ }, basePath);
1028
+
1029
+ assertTrue(!('error' in result), 'completion should accept deferred blocking rework with decision reference');
1030
+ const finding = _getAdapter()!.prepare("SELECT status, decision_ref FROM rework_brief_findings WHERE brief_id = 'RB-001' AND finding_id = 'F1'").get() as { status: string; decision_ref: string };
1031
+ assertEq(finding.status, 'deferred-with-override', 'finding status should record the override');
1032
+ assertEq(finding.decision_ref, 'DEC-2026-07-07-rework-deferral', 'decision reference should be persisted');
1033
+
1034
+ cleanupDir(basePath);
1035
+ cleanup(dbPath);
1036
+ }
1037
+
1038
+ console.log('\n=== complete-task: invalid duplicate rework resolution cannot overwrite a valid one ===');
1039
+ {
1040
+ const dbPath = tempDbPath();
1041
+ openDatabase(dbPath);
1042
+ const { basePath } = createTempProject();
1043
+
1044
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
1045
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
1046
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
1047
+ saveReworkBrief({
1048
+ briefId: 'RB-001',
1049
+ milestoneId: 'M001',
1050
+ sliceId: 'S01',
1051
+ taskId: 'T01',
1052
+ findings: [{
1053
+ findingId: 'F1',
1054
+ severity: 'blocking',
1055
+ description: 'Compile regression',
1056
+ requiredFix: 'Fix compile error',
1057
+ verificationCommands: ['pnpm run typecheck:extensions'],
1058
+ }],
1059
+ });
1060
+
1061
+ // The guard admits F1 because a satisfying entry exists, but a later
1062
+ // non-satisfying duplicate for the same finding must NOT overwrite the valid
1063
+ // resolution and leave F1 resolved with no evidence.
1064
+ const result = await handleCompleteTask({
1065
+ ...makeValidParams(),
1066
+ reworkResolution: [
1067
+ { findingId: 'F1', status: 'resolved', evidence: 'Fixed compile error and reran pnpm run typecheck:extensions.' },
1068
+ { findingId: 'F1', status: 'resolved', evidence: '' },
1069
+ ],
1070
+ }, basePath);
1071
+
1072
+ assertTrue(!('error' in result), 'completion should succeed on the valid resolution');
1073
+ const finding = _getAdapter()!.prepare(
1074
+ "SELECT status, evidence FROM rework_brief_findings WHERE brief_id = 'RB-001' AND finding_id = 'F1'"
1075
+ ).get() as { status: string; evidence: string };
1076
+ assertEq(finding.status, 'resolved', 'finding should remain resolved');
1077
+ assertMatch(finding.evidence, /Fixed compile error/, 'valid evidence must survive the invalid duplicate');
1078
+
1079
+ cleanupDir(basePath);
1080
+ cleanup(dbPath);
1081
+ }
1082
+
1083
+ console.log('\n=== complete-task: invalid deferred duplicate cannot strip decisionRef ===');
1084
+ {
1085
+ const dbPath = tempDbPath();
1086
+ openDatabase(dbPath);
1087
+ const { basePath } = createTempProject();
1088
+
1089
+ insertMilestone({ id: 'M001', title: 'Test Milestone' });
1090
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' });
1091
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
1092
+ saveReworkBrief({
1093
+ briefId: 'RB-001',
1094
+ milestoneId: 'M001',
1095
+ sliceId: 'S01',
1096
+ taskId: 'T01',
1097
+ findings: [{
1098
+ findingId: 'F1',
1099
+ severity: 'blocking',
1100
+ description: 'Compile regression',
1101
+ requiredFix: 'Fix compile error',
1102
+ verificationCommands: ['pnpm run typecheck:extensions'],
1103
+ }],
1104
+ });
1105
+
1106
+ // A deferred-with-override duplicate that omits decisionRef is non-satisfying
1107
+ // and must not overwrite the valid deferral.
1108
+ const result = await handleCompleteTask({
1109
+ ...makeValidParams(),
1110
+ reworkResolution: [
1111
+ { findingId: 'F1', status: 'deferred-with-override', evidence: 'Maintainer accepted temporary deferral.', decisionRef: 'DEC-2026-07-07-rework-deferral' },
1112
+ { findingId: 'F1', status: 'deferred-with-override', evidence: 'Maintainer accepted temporary deferral.' },
1113
+ ],
1114
+ }, basePath);
1115
+
1116
+ assertTrue(!('error' in result), 'completion should succeed on the valid deferral');
1117
+ const finding = _getAdapter()!.prepare(
1118
+ "SELECT status, decision_ref FROM rework_brief_findings WHERE brief_id = 'RB-001' AND finding_id = 'F1'"
1119
+ ).get() as { status: string; decision_ref: string };
1120
+ assertEq(finding.status, 'deferred-with-override', 'finding should remain deferred-with-override');
1121
+ assertEq(finding.decision_ref, 'DEC-2026-07-07-rework-deferral', 'valid decisionRef must survive the invalid duplicate');
1122
+
1123
+ cleanupDir(basePath);
1124
+ cleanup(dbPath);
1125
+ }
1126
+
841
1127
  // ═══════════════════════════════════════════════════════════════════════════
842
1128
 
843
1129
  report();
@@ -0,0 +1,186 @@
1
+ // GSD Extension - Migration guard tests (plan 026).
2
+ // Covers the newer-DB refusal in migrateSchema, whole-chain rollback atomicity
3
+ // via the _setMigrationFaultForTest seam, the legacy-data guard in initSchema,
4
+ // and getCurrentSchemaVersion's empty-table NULL handling.
5
+
6
+ import { describe, test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { createRequire } from 'node:module';
12
+
13
+ import {
14
+ openDatabase,
15
+ closeDatabase,
16
+ isDbAvailable,
17
+ SCHEMA_VERSION,
18
+ _setMigrationFaultForTest,
19
+ } from '../gsd-db.ts';
20
+ import { createDbAdapter } from '../db-adapter.ts';
21
+ import { getCurrentSchemaVersion } from '../db-schema-metadata.ts';
22
+
23
+ const _require = createRequire(import.meta.url);
24
+
25
+ function tempDbPath(): string {
26
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-migrate-guards-'));
27
+ return path.join(dir, 'gsd.db');
28
+ }
29
+
30
+ function cleanup(dbPath: string): void {
31
+ closeDatabase();
32
+ fs.rmSync(path.dirname(dbPath), { recursive: true, force: true });
33
+ }
34
+
35
+ interface RawDb {
36
+ exec(sql: string): void;
37
+ prepare(sql: string): { get(...args: unknown[]): Record<string, unknown> | undefined; all(...args: unknown[]): Array<Record<string, unknown>> };
38
+ close(): void;
39
+ }
40
+
41
+ function openRawSqliteForTest(dbPath: string): RawDb {
42
+ try {
43
+ const mod = _require('node:sqlite') as { DatabaseSync: new (path: string) => RawDb };
44
+ return new mod.DatabaseSync(dbPath);
45
+ } catch {
46
+ type SqliteCtor = new (path: string) => RawDb;
47
+ const mod = _require('better-sqlite3') as SqliteCtor | { default: SqliteCtor };
48
+ const DatabaseCtor: SqliteCtor = typeof mod === 'function' ? mod : mod.default;
49
+ return new DatabaseCtor(dbPath);
50
+ }
51
+ }
52
+
53
+ function rawColumnNames(raw: RawDb, table: string): string[] {
54
+ return raw.prepare(`PRAGMA table_info(${table})`).all().map((r) => r['name'] as string);
55
+ }
56
+
57
+ function rawHasTable(raw: RawDb, table: string): boolean {
58
+ const row = raw.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(table);
59
+ return Boolean(row);
60
+ }
61
+
62
+ function rawMaxVersion(raw: RawDb): number | null {
63
+ const row = raw.prepare('SELECT MAX(version) as v FROM schema_version').get();
64
+ return (row?.['v'] as number | null) ?? null;
65
+ }
66
+
67
+ describe('db-engine migrate guards', () => {
68
+ test('getCurrentSchemaVersion returns 0 on an empty schema_version table', () => {
69
+ const sqlite = _require('node:sqlite') as { DatabaseSync: new (path: string) => unknown };
70
+ const adapter = createDbAdapter(new sqlite.DatabaseSync(':memory:'));
71
+ try {
72
+ adapter.exec('CREATE TABLE schema_version (version INTEGER NOT NULL, applied_at TEXT NOT NULL)');
73
+ assert.equal(getCurrentSchemaVersion(adapter), 0);
74
+ } finally {
75
+ adapter.close();
76
+ }
77
+ });
78
+
79
+ test('refuses to open a DB whose schema is newer than this binary', () => {
80
+ const dbPath = tempDbPath();
81
+ try {
82
+ assert.ok(openDatabase(dbPath), 'fresh open should succeed');
83
+ closeDatabase();
84
+
85
+ const raw = openRawSqliteForTest(dbPath);
86
+ raw.exec(
87
+ `INSERT INTO schema_version (version, applied_at) VALUES (${SCHEMA_VERSION + 1}, '2026-01-01T00:00:00Z')`,
88
+ );
89
+ raw.close();
90
+
91
+ assert.throws(() => openDatabase(dbPath), /newer than/);
92
+ assert.equal(isDbAvailable(), false, 'no connection may stay open on refusal');
93
+ } finally {
94
+ cleanup(dbPath);
95
+ }
96
+ });
97
+
98
+ test('a mid-migration failure rolls back to the starting schema version', () => {
99
+ const dbPath = tempDbPath();
100
+ try {
101
+ assert.ok(openDatabase(dbPath), 'fresh open should succeed');
102
+ closeDatabase();
103
+
104
+ // Rewind the DB to v(SCHEMA_VERSION-1): stamp the prior version and drop
105
+ // the objects the last migration adds, so the re-run has real work to redo.
106
+ const startVersion = SCHEMA_VERSION - 2;
107
+ const raw = openRawSqliteForTest(dbPath);
108
+ raw.exec('DELETE FROM schema_version');
109
+ raw.exec(`INSERT INTO schema_version (version, applied_at) VALUES (${startVersion}, '2026-01-01T00:00:00Z')`);
110
+ raw.exec('ALTER TABLE slices DROP COLUMN target_repositories');
111
+ raw.exec('ALTER TABLE tasks DROP COLUMN target_repositories');
112
+ raw.close();
113
+
114
+ _setMigrationFaultForTest(true);
115
+ try {
116
+ assert.throws(() => openDatabase(dbPath), /migration fault injected/);
117
+ } finally {
118
+ _setMigrationFaultForTest(false);
119
+ }
120
+ assert.equal(isDbAvailable(), false, 'no connection may stay open on migration failure');
121
+
122
+ const inspect = openRawSqliteForTest(dbPath);
123
+ try {
124
+ assert.equal(rawMaxVersion(inspect), startVersion, 'version must roll back to the starting version');
125
+ assert.ok(
126
+ !rawColumnNames(inspect, 'slices').includes('target_repositories'),
127
+ 'column added by the failed migration must be rolled back',
128
+ );
129
+ } finally {
130
+ inspect.close();
131
+ }
132
+
133
+ // The DB is intact — a clean re-open migrates to the current version.
134
+ assert.ok(openDatabase(dbPath), 're-open without the fault should succeed');
135
+ closeDatabase();
136
+ const after = openRawSqliteForTest(dbPath);
137
+ try {
138
+ assert.equal(rawMaxVersion(after), SCHEMA_VERSION);
139
+ assert.ok(rawColumnNames(after, 'slices').includes('target_repositories'));
140
+ assert.equal(rawHasTable(after, 'rework_briefs'), true);
141
+ assert.equal(rawHasTable(after, 'rework_brief_findings'), true);
142
+ } finally {
143
+ after.close();
144
+ }
145
+ } finally {
146
+ cleanup(dbPath);
147
+ }
148
+ });
149
+
150
+ test('a DB with data but no version rows migrates from v1 instead of being stamped current', () => {
151
+ const dbPath = tempDbPath();
152
+ try {
153
+ assert.ok(openDatabase(dbPath), 'fresh open should succeed');
154
+ closeDatabase();
155
+
156
+ // Simulate a legacy DB: user data present, version table empty, and a
157
+ // migration-added column missing so we can observe the chain actually run.
158
+ const raw = openRawSqliteForTest(dbPath);
159
+ raw.exec("INSERT INTO milestones (id, title) VALUES ('M001', 'Legacy milestone')");
160
+ raw.exec('DELETE FROM schema_version');
161
+ raw.exec('ALTER TABLE milestones DROP COLUMN vision');
162
+ raw.close();
163
+
164
+ assert.ok(openDatabase(dbPath), 'legacy DB open should succeed');
165
+ closeDatabase();
166
+
167
+ const inspect = openRawSqliteForTest(dbPath);
168
+ try {
169
+ const versions = inspect.prepare('SELECT version FROM schema_version ORDER BY version').all()
170
+ .map((r) => r['version'] as number);
171
+ assert.equal(versions[0], 1, 'baseline v1 must be recorded, not a fresh-install stamp');
172
+ assert.equal(versions[versions.length - 1], SCHEMA_VERSION, 'migration chain must reach the current version');
173
+ assert.ok(
174
+ rawColumnNames(inspect, 'milestones').includes('vision'),
175
+ 'migration-added column must be restored by the chain',
176
+ );
177
+ const row = inspect.prepare('SELECT count(*) as cnt FROM milestones').get();
178
+ assert.equal(row?.['cnt'], 1, 'existing data must survive');
179
+ } finally {
180
+ inspect.close();
181
+ }
182
+ } finally {
183
+ cleanup(dbPath);
184
+ }
185
+ });
186
+ });
@@ -7,6 +7,8 @@ import { join } from 'node:path';
7
7
  import { tmpdir } from 'node:os';
8
8
 
9
9
  import { deriveState, invalidateStateCache, _deriveStateImpl, deriveStateFromDb, isGhostMilestone } from '../state.ts';
10
+ import { readCachedDeriveState, writeCachedDeriveState } from '../state/derive/cache.ts';
11
+ import type { GSDState } from '../types.ts';
10
12
  import {
11
13
  openDatabase,
12
14
  closeDatabase,
@@ -582,6 +584,33 @@ describe('derive-state-db', async () => {
582
584
  }
583
585
  });
584
586
 
587
+ // ─── Test 7b: lock flip must not serve a basePath-keyed cache hit ─────
588
+ // The derive cache is keyed on basePath only, but deriveStateFromDb filters
589
+ // by GSD_MILESTONE_LOCK. auto.ts's capture/restoreMilestoneLockEnv call
590
+ // invalidateStateCache() on every flip; this guards that the cache is
591
+ // genuinely lock-blind (so that invalidate call is load-bearing, not
592
+ // redundant) and that invalidation defeats the stale read.
593
+ test('derive-state-db: a milestone-lock flip needs explicit cache invalidation', () => {
594
+ const prevLock = process.env.GSD_MILESTONE_LOCK;
595
+ try {
596
+ const base = '/tmp/gsd-lock-flip-fixture';
597
+ const stateA = { phase: 'planning' } as unknown as GSDState;
598
+ process.env.GSD_MILESTONE_LOCK = 'M001';
599
+ writeCachedDeriveState(base, stateA);
600
+
601
+ // Lock flips but basePath is unchanged → the cache still hits.
602
+ process.env.GSD_MILESTONE_LOCK = 'M002';
603
+ assert.strictEqual(readCachedDeriveState(base), stateA, 'basePath-only cache ignores the lock flip');
604
+
605
+ // What the lock-env functions do on flip:
606
+ invalidateStateCache();
607
+ assert.strictEqual(readCachedDeriveState(base), null, 'invalidation defeats the stale-lock read');
608
+ } finally {
609
+ if (prevLock === undefined) delete process.env.GSD_MILESTONE_LOCK;
610
+ else process.env.GSD_MILESTONE_LOCK = prevLock;
611
+ }
612
+ });
613
+
585
614
  // ═════════════════════════════════════════════════════════════════════════
586
615
  // New: deriveStateFromDb() cross-validation tests
587
616
  // ═════════════════════════════════════════════════════════════════════════
@@ -80,6 +80,43 @@ function makeDiscussCtx(notifications: Array<{ message: string; level?: string }
80
80
  };
81
81
  }
82
82
 
83
+ async function runDiscussTargetFixture(
84
+ target: string,
85
+ milestones: Array<{ id: string; title?: string; status?: string }>,
86
+ writeArtifacts?: (base: string) => void,
87
+ ) {
88
+ const base = mkdtempSync(join(tmpdir(), "gsd-discuss-target-"));
89
+ const notifications: Array<{ message: string; level?: string }> = [];
90
+ const harness = makeDiscussPi();
91
+ try {
92
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
93
+ writeArtifacts?.(base);
94
+ const dbPath = join(base, ".gsd", "gsd.db");
95
+ assert.equal(openDatabase(dbPath), true);
96
+ for (const milestone of milestones) {
97
+ insertMilestone(milestone);
98
+ }
99
+
100
+ await showDiscuss(
101
+ makeDiscussCtx(notifications) as any,
102
+ harness.pi as any,
103
+ base,
104
+ { target },
105
+ );
106
+
107
+ return {
108
+ notifications: [...notifications],
109
+ sent: [...harness.sent],
110
+ };
111
+ } finally {
112
+ harness.restore();
113
+ if (isDbAvailable()) closeDatabase();
114
+ invalidateStateCache();
115
+ clearGuidedUnitContext();
116
+ rmSync(base, { recursive: true, force: true });
117
+ }
118
+ }
119
+
83
120
  describe("discuss target normalization", () => {
84
121
  test("canonicalizes milestone and slice casing", () => {
85
122
  assert.equal(normalizeDiscussTarget("m014"), "M014");
@@ -90,6 +127,92 @@ describe("discuss target normalization", () => {
90
127
  });
91
128
  });
92
129
 
130
+ describe("showDiscuss targeted milestone guardrails (#1320)", () => {
131
+ test("missing, complete, and parked milestone targets get actionable messages", async () => {
132
+ const cases = [
133
+ {
134
+ target: "M006",
135
+ milestones: [{ id: "M005", title: "Current", status: "active" }],
136
+ level: "warning",
137
+ message: /Milestone M006 was not found in the roadmap\. Use \/gsd new-milestone to add it, or \/gsd status to see available milestones\./,
138
+ },
139
+ {
140
+ target: "M006/S01",
141
+ milestones: [{ id: "M005", title: "Current", status: "active" }],
142
+ level: "warning",
143
+ message: /Milestone M006 was not found in the roadmap\. Use \/gsd new-milestone to add it, or \/gsd status to see available milestones\./,
144
+ },
145
+ {
146
+ target: "M006",
147
+ milestones: [{ id: "M006", title: "Done", status: "complete" }],
148
+ level: "info",
149
+ message: /Milestone M006 is already complete\./,
150
+ },
151
+ {
152
+ target: "M006/S01",
153
+ milestones: [{ id: "M006", title: "Done", status: "complete" }],
154
+ level: "info",
155
+ message: /Milestone M006 is already complete\./,
156
+ },
157
+ {
158
+ target: "M006",
159
+ milestones: [{ id: "M006", title: "Deferred", status: "parked" }],
160
+ level: "warning",
161
+ message: /Milestone M006 is parked\. Run \/gsd unpark M006 to reactivate\./,
162
+ },
163
+ {
164
+ target: "M006/S01",
165
+ milestones: [{ id: "M006", title: "Deferred", status: "parked" }],
166
+ level: "warning",
167
+ message: /Milestone M006 is parked\. Run \/gsd unpark M006 to reactivate\./,
168
+ },
169
+ ];
170
+
171
+ for (const testCase of cases) {
172
+ const result = await runDiscussTargetFixture(testCase.target, testCase.milestones);
173
+ assert.equal(result.sent.length, 0, `${testCase.target} must not dispatch`);
174
+ assert.equal(result.notifications.length, 1, `${testCase.target} must emit one notification`);
175
+ assert.equal(result.notifications[0]?.level, testCase.level);
176
+ assert.match(result.notifications[0]?.message ?? "", testCase.message);
177
+ assert.doesNotMatch(result.notifications[0]?.message ?? "", /not discussable/i);
178
+ }
179
+ });
180
+
181
+ test("bare milestone targets match unique-suffix milestone IDs", async () => {
182
+ const result = await runDiscussTargetFixture("M006", [
183
+ { id: "M006-abc123", title: "Unique milestone", status: "active" },
184
+ ]);
185
+
186
+ assert.equal(result.notifications.length, 0);
187
+ assert.equal(result.sent.length, 1, "bare ID must dispatch the suffixed milestone");
188
+ assert.match(String(result.sent[0]?.content), /M006-abc123|Unique milestone|guided-discuss-milestone/i);
189
+ });
190
+
191
+ test("bare slice targets match unique-suffix milestone IDs", async () => {
192
+ const result = await runDiscussTargetFixture(
193
+ "M006/S01",
194
+ [{ id: "M006-abc123", title: "Unique milestone", status: "active" }],
195
+ (base) => {
196
+ mkdirSync(join(base, ".gsd", "milestones", "M006-abc123"), { recursive: true });
197
+ writeFileSync(
198
+ join(base, ".gsd", "milestones", "M006-abc123", "M006-abc123-ROADMAP.md"),
199
+ `# M006-abc123 Roadmap
200
+
201
+ ## Slices
202
+ - [ ] **S01: Unique slice** \`risk:medium\` \`depends:[]\`
203
+ > After this: the suffixed milestone slice can be discussed
204
+ `,
205
+ "utf-8",
206
+ );
207
+ },
208
+ );
209
+
210
+ assert.equal(result.notifications.length, 0);
211
+ assert.equal(result.sent.length, 1, "bare ID slice target must dispatch through the suffixed milestone");
212
+ assert.match(String(result.sent[0]?.content), /M006-abc123|S01|Unique slice|guided-discuss-slice/i);
213
+ });
214
+ });
215
+
93
216
  describe("loadDiscussNormSlices roadmap fallback (#2892)", () => {
94
217
  test("falls back to ROADMAP when DB has no slice rows", async () => {
95
218
  const base = mkdtempSync(join(tmpdir(), "gsd-discuss-slices-"));