@opengsd/gsd-pi 1.5.0-dev.553dc518 → 1.5.0-dev.c4c8885f

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 (302) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/ask-user-questions.js +1 -1
  3. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
  4. package/dist/resources/extensions/gsd/activity-log.js +1 -1
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
  6. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -5
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +7 -2
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +69 -17
  9. package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
  11. package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
  12. package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
  13. package/dist/resources/extensions/gsd/auto-start.js +54 -31
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
  15. package/dist/resources/extensions/gsd/auto-timers.js +9 -2
  16. package/dist/resources/extensions/gsd/auto.js +97 -31
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +1 -1
  18. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +67 -6
  19. package/dist/resources/extensions/gsd/codebase-generator.js +7 -1
  20. package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
  21. package/dist/resources/extensions/gsd/config-overlay.js +7 -2
  22. package/dist/resources/extensions/gsd/db/engine.js +33 -0
  23. package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
  24. package/dist/resources/extensions/gsd/db-adapter.js +30 -3
  25. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  26. package/dist/resources/extensions/gsd/flat-phase-migration.js +18 -0
  27. package/dist/resources/extensions/gsd/gsd-db.js +12 -11
  28. package/dist/resources/extensions/gsd/guided-flow.js +16 -7
  29. package/dist/resources/extensions/gsd/journal.js +1 -1
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
  31. package/dist/resources/extensions/gsd/md-importer.js +89 -23
  32. package/dist/resources/extensions/gsd/metrics.js +2 -2
  33. package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
  34. package/dist/resources/extensions/gsd/paths.js +87 -52
  35. package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
  36. package/dist/resources/extensions/gsd/preferences-models.js +66 -15
  37. package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
  38. package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
  39. package/dist/resources/extensions/gsd/preferences.js +20 -0
  40. package/dist/resources/extensions/gsd/rule-registry.js +106 -10
  41. package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
  42. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
  43. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
  44. package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
  45. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
  46. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
  47. package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
  48. package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
  49. package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
  50. package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
  51. package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
  52. package/dist/resources/extensions/gsd/uok/audit.js +3 -0
  53. package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
  54. package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
  55. package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
  56. package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
  57. package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
  58. package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
  59. package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
  60. package/dist/resources/extensions/gsd/workspace.js +5 -14
  61. package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
  62. package/dist/resources/extensions/shared/interview-ui.js +62 -9
  63. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  64. package/dist/web/standalone/.next/BUILD_ID +1 -1
  65. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  66. package/dist/web/standalone/.next/build-manifest.json +2 -2
  67. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  68. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  116. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  118. package/dist/web/standalone/.next/server/app/index.html +1 -1
  119. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  126. package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
  127. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  128. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  130. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  131. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  132. package/dist/worktree-cli-create.d.ts +10 -0
  133. package/dist/worktree-cli-create.js +11 -0
  134. package/dist/worktree-cli-format.d.ts +12 -0
  135. package/dist/worktree-cli-format.js +38 -0
  136. package/dist/worktree-cli-plan.d.ts +16 -0
  137. package/dist/worktree-cli-plan.js +19 -0
  138. package/dist/worktree-cli-session.d.ts +13 -0
  139. package/dist/worktree-cli-session.js +18 -0
  140. package/dist/worktree-cli-status.d.ts +35 -0
  141. package/dist/worktree-cli-status.js +54 -0
  142. package/dist/worktree-cli.d.ts +1 -16
  143. package/dist/worktree-cli.js +28 -122
  144. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  145. package/integrations/hermes/pyproject.toml +1 -1
  146. package/package.json +1 -1
  147. package/packages/cloud-mcp-gateway/package.json +2 -2
  148. package/packages/contracts/package.json +1 -1
  149. package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
  150. package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
  151. package/packages/daemon/dist/orchestrator-agent.js +113 -0
  152. package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
  153. package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
  154. package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
  155. package/packages/daemon/dist/orchestrator-tools.js +137 -0
  156. package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
  157. package/packages/daemon/dist/orchestrator.d.ts +4 -56
  158. package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
  159. package/packages/daemon/dist/orchestrator.js +13 -303
  160. package/packages/daemon/dist/orchestrator.js.map +1 -1
  161. package/packages/daemon/dist/orchestrator.test.js +11 -1
  162. package/packages/daemon/dist/orchestrator.test.js.map +1 -1
  163. package/packages/daemon/package.json +4 -4
  164. package/packages/gsd-agent-core/package.json +5 -5
  165. package/packages/gsd-agent-modes/package.json +7 -7
  166. package/packages/mcp-server/package.json +4 -4
  167. package/packages/native/package.json +1 -1
  168. package/packages/pi-agent-core/package.json +1 -1
  169. package/packages/pi-ai/package.json +1 -1
  170. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  171. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
  173. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
  175. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
  177. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
  179. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
  180. package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
  181. package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
  182. package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
  183. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  184. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  185. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  187. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/index.js +1 -0
  189. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  190. package/packages/pi-coding-agent/package.json +7 -7
  191. package/packages/pi-tui/package.json +2 -2
  192. package/packages/rpc-client/package.json +2 -2
  193. package/pkg/package.json +1 -1
  194. package/src/resources/extensions/ask-user-questions.ts +1 -1
  195. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
  196. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
  197. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
  198. package/src/resources/extensions/gsd/activity-log.ts +1 -1
  199. package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
  200. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -4
  201. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +9 -2
  202. package/src/resources/extensions/gsd/auto-dispatch.ts +77 -16
  203. package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
  204. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
  205. package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
  206. package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
  207. package/src/resources/extensions/gsd/auto-start.ts +58 -29
  208. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
  209. package/src/resources/extensions/gsd/auto-timers.ts +9 -2
  210. package/src/resources/extensions/gsd/auto.ts +103 -30
  211. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +1 -1
  212. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +66 -6
  213. package/src/resources/extensions/gsd/codebase-generator.ts +7 -1
  214. package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
  215. package/src/resources/extensions/gsd/config-overlay.ts +7 -1
  216. package/src/resources/extensions/gsd/db/engine.ts +36 -0
  217. package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
  218. package/src/resources/extensions/gsd/db-adapter.ts +29 -3
  219. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  220. package/src/resources/extensions/gsd/flat-phase-migration.ts +20 -0
  221. package/src/resources/extensions/gsd/gsd-db.ts +20 -11
  222. package/src/resources/extensions/gsd/guided-flow.ts +19 -7
  223. package/src/resources/extensions/gsd/journal.ts +1 -1
  224. package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
  225. package/src/resources/extensions/gsd/md-importer.ts +93 -22
  226. package/src/resources/extensions/gsd/metrics.ts +2 -2
  227. package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
  228. package/src/resources/extensions/gsd/paths.ts +108 -51
  229. package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
  230. package/src/resources/extensions/gsd/preferences-models.ts +74 -20
  231. package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
  232. package/src/resources/extensions/gsd/preferences-types.ts +24 -1
  233. package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
  234. package/src/resources/extensions/gsd/preferences.ts +20 -0
  235. package/src/resources/extensions/gsd/rule-registry.ts +111 -9
  236. package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
  237. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
  238. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
  239. package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
  240. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
  241. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
  242. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
  243. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +57 -0
  244. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
  245. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
  246. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  247. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
  248. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
  249. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
  250. package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
  251. package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
  252. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +27 -0
  253. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
  254. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
  255. package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
  256. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
  257. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
  258. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
  259. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
  260. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  261. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
  262. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
  263. package/src/resources/extensions/gsd/tests/preferences.test.ts +19 -0
  264. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
  265. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
  266. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
  267. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
  268. package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
  269. package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
  270. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
  271. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
  272. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
  273. package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
  274. package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
  275. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +93 -0
  276. package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
  277. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
  278. package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
  279. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
  280. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
  281. package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
  282. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
  283. package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
  284. package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
  285. package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
  286. package/src/resources/extensions/gsd/types.ts +23 -4
  287. package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
  288. package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
  289. package/src/resources/extensions/gsd/uok/audit.ts +2 -0
  290. package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
  291. package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
  292. package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
  293. package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
  294. package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
  295. package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
  296. package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
  297. package/src/resources/extensions/gsd/workspace.ts +5 -21
  298. package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
  299. package/src/resources/extensions/shared/interview-ui.ts +58 -11
  300. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
  301. /package/dist/web/standalone/.next/static/{ZpFDazP-38PXBPiI_LdZf → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
  302. /package/dist/web/standalone/.next/static/{ZpFDazP-38PXBPiI_LdZf → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
@@ -250,6 +250,73 @@ test("migration auto-check canonicalizes a legacy descriptor milestone dir (no f
250
250
  }
251
251
  });
252
252
 
253
+ test("migration auto-check recognizes suffixed flat-phase projection directories", async () => {
254
+ const base = makeBase();
255
+ try {
256
+ await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
257
+ assert.equal(await ensureDbOpen(base), true);
258
+
259
+ insertMilestone({
260
+ id: "M001-re4q3k",
261
+ title: "Clean GSD Continuation Readiness",
262
+ status: "complete",
263
+ planning: { vision: "Keep continuation state readable." },
264
+ });
265
+ insertSlice({
266
+ id: "S01",
267
+ milestoneId: "M001-re4q3k",
268
+ title: "Readiness slice",
269
+ status: "complete",
270
+ risk: "low",
271
+ depends: [],
272
+ demo: "Readable continuation state.",
273
+ sequence: 1,
274
+ });
275
+ insertTask({
276
+ id: "T01",
277
+ sliceId: "S01",
278
+ milestoneId: "M001-re4q3k",
279
+ title: "Readiness task",
280
+ status: "complete",
281
+ });
282
+ insertMilestone({
283
+ id: "M002-a1rwmq",
284
+ title: "Support Command Policy Hardening",
285
+ status: "complete",
286
+ planning: { vision: "Harden support command policy." },
287
+ });
288
+ insertSlice({
289
+ id: "S01",
290
+ milestoneId: "M002-a1rwmq",
291
+ title: "Policy slice",
292
+ status: "complete",
293
+ risk: "low",
294
+ depends: [],
295
+ demo: "Policy is hardened.",
296
+ sequence: 1,
297
+ });
298
+ insertTask({
299
+ id: "T01",
300
+ sliceId: "S01",
301
+ milestoneId: "M002-a1rwmq",
302
+ title: "Policy task",
303
+ status: "complete",
304
+ });
305
+
306
+ const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.ts");
307
+ const rebuild = await rebuildMarkdownProjectionsFromDb(base);
308
+ assert.ok(rebuild.rendered > 0, "expected flat-phase markdown projections to render");
309
+
310
+ const result = await checkMarkdownHierarchyAgainstDb(base);
311
+ assert.equal(result.action, "none");
312
+ assert.equal(result.reason, "in-sync");
313
+ assert.deepEqual(result.markdown, { milestones: 2, slices: 2, tasks: 2 });
314
+ assert.deepEqual(result.beforeDb, { milestones: 2, slices: 2, tasks: 2 });
315
+ } finally {
316
+ cleanup(base);
317
+ }
318
+ });
319
+
253
320
  test("migration auto-check refreshes a stale open DB handle before comparing", async () => {
254
321
  const base = makeBase();
255
322
  try {
@@ -372,6 +439,30 @@ test("migration auto-check still compares a roadmapless milestone that HAS a DB
372
439
  }
373
440
  });
374
441
 
442
+ test("migration auto-check aligns a roadmapless milestone dir to its suffixed DB row (no false drift)", async () => {
443
+ const base = makeBase();
444
+ try {
445
+ await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
446
+ assert.equal(await ensureDbOpen(base), true);
447
+ // Team-mode suffixed milestone whose markdown dir has no ROADMAP yet (e.g. a
448
+ // freshly handed-off / zero-slice milestone). Markdown discovers the bare
449
+ // "M001"; the DB stores the authoritative suffixed "M001-re4q3k". The
450
+ // bare-to-suffixed alignment must rewrite EVERY markdown set, including the
451
+ // roadmapless subset, so the discussion-phase exclusion sees the DB row and
452
+ // does not decrement the milestone count into false drift.
453
+ insertMilestone({ id: "M001-re4q3k", title: "Suffixed milestone", status: "queued" });
454
+ writeScratchMilestoneDir(base, "M001", "M001-CONTEXT.md");
455
+
456
+ const result = await checkMarkdownHierarchyAgainstDb(base);
457
+ assert.equal(result.action, "none");
458
+ assert.equal(result.reason, "in-sync");
459
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 0, tasks: 0 });
460
+ assert.deepEqual(result.beforeDb, { milestones: 1, slices: 0, tasks: 0 });
461
+ } finally {
462
+ cleanup(base);
463
+ }
464
+ });
465
+
375
466
  test("rebuildMarkdownProjectionsFromDb realigns markdown when DB holds extra rows", async () => {
376
467
  const base = makeBase();
377
468
  try {
@@ -13,6 +13,7 @@ import {
13
13
  isCompletedMilestoneTerminal,
14
14
  repairMissingMilestoneSummaryProjection,
15
15
  } from "../milestone-closeout.js";
16
+ import { targetMilestoneFile } from "../paths.js";
16
17
  import type { DispatchContext } from "../auto-dispatch.js";
17
18
 
18
19
  /** Build a minimal DispatchContext for the dispatch-policy branches under test. */
@@ -196,7 +197,7 @@ test("repairMissingMilestoneSummaryProjection succeeds when milestone dir does n
196
197
  const repair = await repairMissingMilestoneSummaryProjection(base, "M042");
197
198
  assert.equal(repair.ok, true, "repair should report success when handler creates the SUMMARY");
198
199
  assert.ok(
199
- existsSync(join(base, ".gsd", "phases", "42-m042", "42-SUMMARY.md")),
200
+ existsSync(targetMilestoneFile(base, "M042", "SUMMARY", "Done")),
200
201
  "repair should write the SUMMARY artifact to the canonical projection path",
201
202
  );
202
203
  });
@@ -65,7 +65,6 @@ const ALLOWED_IMPORTERS = new Set([
65
65
  "gsd/visualizer-data.ts",
66
66
  // prompt context text (display strings injected into unit prompts)
67
67
  "gsd/auto-prompts.ts",
68
- // cold-path maintenance command (branch cleanup messaging)
69
68
  "gsd/commands-maintenance.ts",
70
69
  // display-only GitHub issue/PR body sync
71
70
  "github-sync/sync.ts",
@@ -16,6 +16,7 @@ import {
16
16
  runPreDispatchHooks,
17
17
  persistHookState,
18
18
  restoreHookState,
19
+ reconcileRestoredHookDispatch,
19
20
  clearPersistedHookState,
20
21
  getHookStatus,
21
22
  formatHookStatus,
@@ -225,6 +226,233 @@ test('Blocking hook restored from disk does not trust artifact without clean hoo
225
226
  }
226
227
  });
227
228
 
229
+ test('Restore reconciliation re-enqueues the lost hook dispatch (#1246)', () => {
230
+ resetHookState();
231
+ const base = createFixtureBase();
232
+ try {
233
+ writeHookPreferences(base, ` - name: plan-review
234
+ after:
235
+ - plan-slice
236
+ prompt: Review the plan for {milestoneId}/{sliceId}
237
+ artifact: PLAN-REVIEW.md
238
+ criticality: blocking
239
+ max_cycles: 2
240
+ `);
241
+ // Trigger unit completes: activeHook set + persisted, dispatch enqueued.
242
+ const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
243
+ assert.ok(dispatch, "gate dispatches on trigger unit completion");
244
+ assert.equal(dispatch.unitType, "hook/plan-review");
245
+ persistHookState(base);
246
+
247
+ // Pause/resume: activeHook restored, but the session-local sidecar queue is
248
+ // gone (never persisted).
249
+ resetHookState();
250
+ restoreHookState(base);
251
+ assert.ok(getActiveHook(), "activeHook restored from disk");
252
+
253
+ // Reconciliation re-enqueues the missing dispatch so the hook actually runs.
254
+ const sidecarQueue: any[] = [];
255
+ reconcileRestoredHookDispatch(base, sidecarQueue);
256
+ assert.equal(sidecarQueue.length, 1, "lost hook dispatch is re-enqueued");
257
+ assert.equal(sidecarQueue[0].kind, "hook");
258
+ assert.equal(sidecarQueue[0].unitType, "hook/plan-review");
259
+ assert.equal(sidecarQueue[0].unitId, "M002/S01");
260
+ assert.match(sidecarQueue[0].prompt, /Review the plan for M002\/S01/);
261
+ } finally {
262
+ resetHookState();
263
+ invalidateAllCaches();
264
+ rmSync(base, { recursive: true, force: true });
265
+ }
266
+ });
267
+
268
+ test('Restore reconciliation is a no-op when the dispatch is already queued (#1246)', () => {
269
+ resetHookState();
270
+ const base = createFixtureBase();
271
+ try {
272
+ writeHookPreferences(base, ` - name: plan-review
273
+ after:
274
+ - plan-slice
275
+ prompt: Review the plan
276
+ artifact: PLAN-REVIEW.md
277
+ criticality: blocking
278
+ max_cycles: 2
279
+ `);
280
+ const dispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
281
+ assert.ok(dispatch, "gate dispatches on trigger unit completion");
282
+ persistHookState(base);
283
+ resetHookState();
284
+ restoreHookState(base);
285
+
286
+ const sidecarQueue: any[] = [
287
+ { kind: "hook", unitType: "hook/plan-review", unitId: "M002/S01", prompt: "already here" },
288
+ ];
289
+ reconcileRestoredHookDispatch(base, sidecarQueue);
290
+ assert.equal(sidecarQueue.length, 1, "does not duplicate an existing hook dispatch");
291
+ assert.equal(sidecarQueue[0].prompt, "already here");
292
+ } finally {
293
+ resetHookState();
294
+ invalidateAllCaches();
295
+ rmSync(base, { recursive: true, force: true });
296
+ }
297
+ });
298
+
299
+ test('Restore reconciliation is a no-op with no active hook (#1246)', () => {
300
+ resetHookState();
301
+ const base = createFixtureBase();
302
+ try {
303
+ const sidecarQueue: any[] = [];
304
+ reconcileRestoredHookDispatch(base, sidecarQueue);
305
+ assert.equal(sidecarQueue.length, 0, "nothing enqueued when no active hook");
306
+ } finally {
307
+ resetHookState();
308
+ invalidateAllCaches();
309
+ rmSync(base, { recursive: true, force: true });
310
+ }
311
+ });
312
+
313
+ test('Blocking hook re-dispatches once after a lost dispatch at default max_cycles', () => {
314
+ resetHookState();
315
+ const base = createFixtureBase();
316
+ try {
317
+ // Default max_cycles (1): a cycle is charged at dispatch time.
318
+ writeHookPreferences(base, ` - name: plan-review
319
+ after:
320
+ - plan-slice
321
+ prompt: Review plan
322
+ artifact: PLAN-REVIEW.md
323
+ criticality: blocking
324
+ `);
325
+
326
+ const firstDispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
327
+ assert.ok(firstDispatch, "gate dispatches and consumes its cycle at dispatch");
328
+ assert.equal(firstDispatch.unitType, "hook/plan-review");
329
+ persistHookState(base);
330
+
331
+ // Simulate a lost dispatch: the hook never produced its own unit-end, the
332
+ // trigger unit resumes instead. The dispatch-consumed cycle is refunded once
333
+ // so the gate re-dispatches rather than hard-blocking on a hook that never ran.
334
+ resetHookState();
335
+ restoreHookState(base);
336
+ const redispatch = checkPostUnitHooks("plan-slice", "M002/S01", base);
337
+ assert.ok(redispatch, "lost dispatch re-dispatches the gate instead of blocking");
338
+ assert.equal(redispatch.unitType, "hook/plan-review");
339
+ assert.deepStrictEqual(consumeGateBlock(), null, "no gate block on the first lost dispatch");
340
+ persistHookState(base);
341
+
342
+ // A second lost dispatch consumes the budget and blocks — the refund is one-shot.
343
+ resetHookState();
344
+ restoreHookState(base);
345
+ const blocked = checkPostUnitHooks("plan-slice", "M002/S01", base);
346
+ assert.deepStrictEqual(blocked, null, "second lost dispatch exhausts the budget");
347
+ const block = consumeGateBlock();
348
+ assert.ok(block, "gate block recorded after the one-shot re-dispatch is used");
349
+ assert.equal(block.hookName, "plan-review");
350
+ assert.match(block.reason, /gate cycle budget exhausted/);
351
+ } finally {
352
+ resetHookState();
353
+ invalidateAllCaches();
354
+ rmSync(base, { recursive: true, force: true });
355
+ }
356
+ });
357
+
358
+ test('Blocking hook lost-dispatch refund is one-shot and bounded at max_cycles=2', () => {
359
+ resetHookState();
360
+ const base = createFixtureBase();
361
+ try {
362
+ writeHookPreferences(base, ` - name: plan-review
363
+ after:
364
+ - plan-slice
365
+ prompt: Review plan
366
+ artifact: PLAN-REVIEW.md
367
+ criticality: blocking
368
+ max_cycles: 2
369
+ `);
370
+
371
+ // Simulate a lost dispatch: persist, drop in-memory state, restore, then the
372
+ // trigger unit resumes instead of the hook producing its own unit-end.
373
+ const lose = () => {
374
+ persistHookState(base);
375
+ resetHookState();
376
+ restoreHookState(base);
377
+ return checkPostUnitHooks("plan-slice", "M002/S01", base);
378
+ };
379
+
380
+ assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
381
+
382
+ // Refund is one-shot, but max_cycles=2 still has real budget, so lost#2 also
383
+ // re-dispatches by consuming a genuine cycle (not another refund).
384
+ assert.ok(lose(), "lost#1 re-dispatches (one-shot refund)");
385
+ assert.equal(consumeGateBlock(), null, "no block after lost#1");
386
+ assert.ok(lose(), "lost#2 re-dispatches from real budget");
387
+ assert.equal(consumeGateBlock(), null, "no block after lost#2");
388
+
389
+ // Total dispatches are bounded to max_cycles + 1 (the single forgiven loss),
390
+ // so the third lost dispatch blocks. redispatchedGateKeys guarantees this
391
+ // terminates rather than looping forever on repeated lost resumes.
392
+ assert.equal(lose(), null, "lost#3 exhausts the bounded budget");
393
+ const block = consumeGateBlock();
394
+ assert.ok(block, "gate block recorded on lost#3");
395
+ assert.match(block.reason, /gate cycle budget exhausted/);
396
+ } finally {
397
+ resetHookState();
398
+ invalidateAllCaches();
399
+ rmSync(base, { recursive: true, force: true });
400
+ }
401
+ });
402
+
403
+ test('Blocking hook lost dispatch does not steal a real-run cycle at max_cycles=2', () => {
404
+ resetHookState();
405
+ const base = createFixtureBase();
406
+ try {
407
+ writeHookPreferences(base, ` - name: plan-review
408
+ after:
409
+ - plan-slice
410
+ prompt: Review plan
411
+ artifact: PLAN-REVIEW.md
412
+ criticality: blocking
413
+ max_cycles: 2
414
+ `);
415
+
416
+ // Dispatch, then lose it once (the one-shot refund is spent here).
417
+ assert.ok(checkPostUnitHooks("plan-slice", "M002/S01", base), "initial dispatch");
418
+ persistHookState(base);
419
+ resetHookState();
420
+ restoreHookState(base);
421
+ assert.ok(
422
+ checkPostUnitHooks("plan-slice", "M002/S01", base),
423
+ "lost dispatch re-dispatches (refund spent)",
424
+ );
425
+
426
+ // The hook now actually runs, each run reporting a failed verdict. Because a
427
+ // lost dispatch must not consume the hook's real-run budget, max_cycles=2
428
+ // still permits two genuine runs before the gate blocks — spending the
429
+ // refund early did not rob a later real cycle.
430
+ mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S01"), { recursive: true });
431
+ writeFileSync(
432
+ resolveHookArtifactPath(base, "M002/S01", "PLAN-REVIEW.md"),
433
+ "---\nverdict: failed\n---\n\nRejected.\n",
434
+ "utf-8",
435
+ );
436
+ assert.ok(
437
+ checkPostUnitHooks("hook/plan-review", "M002/S01", base),
438
+ "real run #1 (failed verdict) re-dispatches",
439
+ );
440
+ assert.equal(consumeGateBlock(), null, "no block after the first real run");
441
+ assert.equal(
442
+ checkPostUnitHooks("hook/plan-review", "M002/S01", base),
443
+ null,
444
+ "real run #2 (failed verdict) exhausts the real budget",
445
+ );
446
+ const block = consumeGateBlock();
447
+ assert.ok(block, "gate block recorded only after two real runs");
448
+ assert.match(block.reason, /gate cycle budget exhausted/);
449
+ } finally {
450
+ resetHookState();
451
+ invalidateAllCaches();
452
+ rmSync(base, { recursive: true, force: true });
453
+ }
454
+ });
455
+
228
456
  test('Blocking hook needs-rework verdict requests trigger unit retry', () => {
229
457
  resetHookState();
230
458
  const base = createFixtureBase();
@@ -454,4 +682,45 @@ test('triggerHookManually: with configured hook', () => {
454
682
  }
455
683
  });
456
684
 
685
+ // ─── Hook dispatch results omit a primary-only model (#1229) ───────────────
686
+ // Both the auto-mode (checkPostUnitHooks → _startHook) and manual
687
+ // (triggerHookManually) dispatch paths must NOT carry a resolved primary model
688
+ // on the dispatch result. Emitting the primary alone discards the configured
689
+ // fallbacks[] chain: the auto path would re-apply it over a fallback already
690
+ // selected by selectAndApplyModel, and the manual path (dispatchHookUnit) now
691
+ // resolves the full chain itself. The dispatch result therefore leaves `model`
692
+ // unset so downstream resolution honors fallbacks.
693
+ test('Dispatch results omit primary-only model so fallbacks survive (#1229)', () => {
694
+ resetHookState();
695
+ const base = createFixtureBase();
696
+ try {
697
+ writeHookPreferences(base, ` - name: code-review
698
+ after:
699
+ - execute-task
700
+ prompt: Review the change
701
+ artifact: REVIEW-PASS.md
702
+ model:
703
+ model: primary-model
704
+ fallbacks:
705
+ - fallback-a
706
+ - fallback-b
707
+ `);
708
+
709
+ const autoDispatch = checkPostUnitHooks("execute-task", "M001/S01/T01", base);
710
+ assert.ok(autoDispatch, "auto-mode dispatches the configured hook");
711
+ assert.equal(autoDispatch.unitType, "hook/code-review", "auto dispatch is hook-prefixed");
712
+ assert.equal(autoDispatch.model, undefined, "auto dispatch does not carry a primary-only model");
713
+
714
+ resetHookState();
715
+
716
+ const manualDispatch = triggerHookManually("code-review", "execute-task", "M001/S01/T01", base);
717
+ assert.ok(manualDispatch, "manual trigger dispatches the configured hook");
718
+ assert.equal(manualDispatch.unitType, "hook/code-review", "manual dispatch is hook-prefixed");
719
+ assert.equal(manualDispatch.model, undefined, "manual dispatch does not carry a primary-only model");
720
+ } finally {
721
+ resetHookState();
722
+ rmSync(base, { recursive: true, force: true });
723
+ }
724
+ });
725
+
457
726
  });
@@ -260,7 +260,14 @@ test("post-unit blocking gate pauses auto-mode on needs-attention verdict", asyn
260
260
  const result = await postUnitPostVerification(pctx);
261
261
  assert.equal(result, "stopped");
262
262
  assert.equal(pauseAuto.mock.callCount(), 1);
263
- assert.match(notifications.join("\n"), /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01/);
263
+ // Regression (#1245): the block message must name BOTH the hook's trigger
264
+ // unit (execute-task M001/S01/T01) and the just-completed unit that surfaced
265
+ // the block (s.currentUnit = hook/review-arbiter M001/S01/T01), so the pause
266
+ // is unambiguous to triage. The pre-fix message stopped at the trigger unit.
267
+ assert.match(
268
+ notifications.join("\n"),
269
+ /Post-unit gate "review-arbiter" blocked execute-task M001\/S01\/T01 \(detected on completion of hook\/review-arbiter M001\/S01\/T01\)/,
270
+ );
264
271
  assert.match(notifications.join("\n"), /\/gsd status/);
265
272
  } finally {
266
273
  process.chdir(originalCwd);
@@ -24,6 +24,7 @@ import {
24
24
  loadProjectGSDPreferences,
25
25
  parsePreferencesMarkdown,
26
26
  renderPreferencesForSystemPrompt,
27
+ renderLanguageDirectiveForPrompt,
27
28
  clearGSDPreferencesCache,
28
29
  _resetParseWarningFlag,
29
30
  } from "../preferences.ts";
@@ -1674,6 +1675,24 @@ test("language: renderPreferencesForSystemPrompt omits language line when not se
1674
1675
  assert.ok(!output.includes("Always respond in"), `expected no language line in output, got:\n${output}`);
1675
1676
  });
1676
1677
 
1678
+ test("language: renderLanguageDirectiveForPrompt emits directive when language set", () => {
1679
+ const output = renderLanguageDirectiveForPrompt({ language: "Chinese" });
1680
+ assert.ok(output.includes("## Response Language"), `expected language heading, got:\n${output}`);
1681
+ assert.ok(output.includes("Always respond in Chinese"), `expected language instruction, got:\n${output}`);
1682
+ });
1683
+
1684
+ test("language: renderLanguageDirectiveForPrompt returns empty when language unset", () => {
1685
+ assert.equal(renderLanguageDirectiveForPrompt({}), "");
1686
+ assert.equal(renderLanguageDirectiveForPrompt(undefined), "");
1687
+ });
1688
+
1689
+ test("language: renderLanguageDirectiveForPrompt sanitizes newlines and caps length", () => {
1690
+ const output = renderLanguageDirectiveForPrompt({ language: `Chinese\ninjected${"x".repeat(80)}` });
1691
+ assert.ok(!output.includes("\ninjected"), `expected newlines stripped, got:\n${output}`);
1692
+ // "Chinese injected" + padding, capped to 50 chars after newline replacement.
1693
+ assert.ok(output.includes("Always respond in Chinese injected"), `got:\n${output}`);
1694
+ });
1695
+
1677
1696
  test("language: parses from markdown frontmatter", () => {
1678
1697
  const content = [
1679
1698
  "---",
@@ -160,6 +160,60 @@ describe('readUatGateVerdict — DB fallback for orphaned ASSESSMENT', () => {
160
160
  assert.equal(result, null, 'roadmap-scoped assessments must not satisfy the UAT gate');
161
161
  });
162
162
 
163
+ test('a backfilled assessment does NOT satisfy the UAT gate (#1258)', async () => {
164
+ // The milestone-validation backfill fabricates a `verdict: PASS` ASSESSMENT
165
+ // for completed slices that never produced a real UAT result (e.g.
166
+ // artifact-driven UAT that was never dispatched). Filed under scope
167
+ // 'backfill', that placeholder must never be read as a genuine UAT sign-off —
168
+ // otherwise "never checked" is silently treated as "passed". Even with the
169
+ // fabricated file present on disk, the gate must fall through to the
170
+ // authoritative run-uat row (absent here) and return null.
171
+ const file = canonicalAssessmentPath(basePath);
172
+ fs.mkdirSync(path.dirname(file), { recursive: true });
173
+ fs.writeFileSync(file, RUNTIME_PASS_BODY);
174
+ insertAssessment({
175
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
176
+ milestoneId: MID,
177
+ sliceId: SLICE,
178
+ status: 'pass',
179
+ scope: 'backfill',
180
+ fullContent: RUNTIME_PASS_BODY,
181
+ });
182
+
183
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
184
+
185
+ assert.equal(result, null, 'backfilled assessments must not satisfy the UAT gate');
186
+ });
187
+
188
+ test('a genuine run-uat row still resolves even when a backfilled file exists (#1258)', async () => {
189
+ // If a real UAT was later recorded, its authoritative run-uat row must win
190
+ // over the fabricated placeholder rather than being masked by it.
191
+ const file = canonicalAssessmentPath(basePath);
192
+ fs.mkdirSync(path.dirname(file), { recursive: true });
193
+ fs.writeFileSync(file, RUNTIME_PASS_BODY);
194
+ insertAssessment({
195
+ path: `.gsd/milestones/${MID}/slices/${SLICE}/${SLICE}-ASSESSMENT.md`,
196
+ milestoneId: MID,
197
+ sliceId: SLICE,
198
+ status: 'pass',
199
+ scope: 'backfill',
200
+ fullContent: RUNTIME_PASS_BODY,
201
+ });
202
+ insertAssessment({
203
+ path: `.gsd/phases/01-some-feature/01-01-ASSESSMENT.md`,
204
+ milestoneId: MID,
205
+ sliceId: SLICE,
206
+ status: 'pass',
207
+ scope: 'run-uat',
208
+ fullContent: RUNTIME_PASS_BODY,
209
+ });
210
+
211
+ const result = await readUatGateVerdict(basePath, MID, SLICE);
212
+
213
+ assert.ok(result, 'a genuine run-uat sign-off must resolve');
214
+ assert.equal(result!.verdict, 'pass');
215
+ });
216
+
163
217
  test('returns null when no assessment and no file exist (fallback does not hallucinate)', async () => {
164
218
  const result = await readUatGateVerdict(basePath, MID, SLICE);
165
219
  assert.equal(result, null);
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
3
+ import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
@@ -394,6 +394,54 @@ test('handleReassessRoadmap invalidates stale milestone-validation when roadmap
394
394
  }
395
395
  });
396
396
 
397
+ test('handleReassessRoadmap deletes flat-phase compatibility validation files when roadmap changes (#2957)', async (t) => {
398
+ const base = makeTmpBase();
399
+ t.after(() => cleanup(base));
400
+ openDatabase(join(base, '.gsd', 'gsd.db'));
401
+
402
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
403
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice One', status: 'complete', demo: 'Demo' });
404
+
405
+ const legacyValidationPath = join(base, '.gsd', 'phases', '01-test', 'M001-VALIDATION.md');
406
+ const canonicalValidationPath = join(base, '.gsd', 'phases', '01-test', '01-VALIDATION.md');
407
+ const staleValidation = '---\nverdict: pass\n---\n\n# Stale validation\n';
408
+ writeFileSync(legacyValidationPath, staleValidation, 'utf-8');
409
+ writeFileSync(canonicalValidationPath, staleValidation, 'utf-8');
410
+ insertAssessment({
411
+ path: legacyValidationPath,
412
+ milestoneId: 'M001',
413
+ sliceId: null,
414
+ taskId: null,
415
+ status: 'pass',
416
+ scope: 'milestone-validation',
417
+ fullContent: staleValidation,
418
+ });
419
+
420
+ const result = await handleReassessRoadmap({
421
+ milestoneId: 'M001',
422
+ completedSliceId: 'S01',
423
+ verdict: 'confirmed',
424
+ assessment: 'S01 completed. Adding S02 changes roadmap structure.',
425
+ sliceChanges: {
426
+ modified: [],
427
+ added: [
428
+ {
429
+ sliceId: 'S02',
430
+ title: 'New Slice Two',
431
+ risk: 'low',
432
+ depends: ['S01'],
433
+ demo: 'Demo two.',
434
+ },
435
+ ],
436
+ removed: [],
437
+ },
438
+ }, base);
439
+
440
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
441
+ assert.equal(existsSync(legacyValidationPath), false, 'stale compatibility validation artifact should be deleted');
442
+ assert.equal(existsSync(canonicalValidationPath), false, 'canonical validation artifact should not remain after reassess');
443
+ });
444
+
397
445
  test('handleReassessRoadmap does NOT invalidate validation when no roadmap structural changes (#2957)', async () => {
398
446
  const base = makeTmpBase();
399
447
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -125,6 +125,58 @@ test('handleReopenSlice: rejects slice in a closed milestone', async () => {
125
125
  }
126
126
  });
127
127
 
128
+ test('handleReopenSlice: recovers a desynced slice (open status, completed tasks) — #1205', async () => {
129
+ const base = makeTmpBase();
130
+ openDatabase(join(base, '.gsd', 'gsd.db'));
131
+ try {
132
+ // Desync signature: slice left "pending" by a UAT→planning fallback while
133
+ // its tasks stayed "complete" from the first execution pass.
134
+ insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
135
+ insertSlice({ id: 'S01', milestoneId: 'M001', status: 'pending' });
136
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
137
+ insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
138
+
139
+ const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S01' }, base);
140
+
141
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
142
+ assert.equal(result.tasksReset, 2, 'both completed tasks should be reset');
143
+
144
+ const slice = getSlice('M001', 'S01');
145
+ assert.equal(slice!.status, 'in_progress', 'slice should be reopened to in_progress');
146
+ const tasks = getSliceTasks('M001', 'S01');
147
+ assert.ok(tasks.every(t => t.status === 'pending'), 'all tasks should be reset to pending');
148
+ } finally {
149
+ cleanup(base);
150
+ }
151
+ });
152
+
153
+ test('handleReopenSlice: rejects an in-flight slice with mixed task progress and preserves it — #1205', async () => {
154
+ const base = makeTmpBase();
155
+ openDatabase(join(base, '.gsd', 'gsd.db'));
156
+ try {
157
+ // Normal in-flight slice, NOT the #1205 desync: one task finished while
158
+ // another is still in progress. The desync guard must not treat a single
159
+ // completed task as reopenable, or a full reset would wipe live progress.
160
+ insertMilestone({ id: 'M001', title: 'Active', status: 'active' });
161
+ insertSlice({ id: 'S01', milestoneId: 'M001', status: 'in_progress' });
162
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', status: 'complete' });
163
+ insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', status: 'in_progress' });
164
+
165
+ const result = await handleReopenSlice({ milestoneId: 'M001', sliceId: 'S01' }, base);
166
+
167
+ assert.ok('error' in result, 'in-flight slice with unfinished tasks must be rejected');
168
+ assert.match(result.error, /not complete/);
169
+
170
+ // Progress must be untouched — nothing reset to pending.
171
+ const byId = Object.fromEntries(getSliceTasks('M001', 'S01').map(t => [t.id, t.status]));
172
+ assert.equal(byId['T01'], 'complete', 'completed task must stay complete');
173
+ assert.equal(byId['T02'], 'in_progress', 'in-progress task must stay in_progress');
174
+ assert.equal(getSlice('M001', 'S01')!.status, 'in_progress', 'slice status must be untouched');
175
+ } finally {
176
+ cleanup(base);
177
+ }
178
+ });
179
+
128
180
  test('handleReopenSlice: rejects reopening a slice that is not complete', async () => {
129
181
  const base = makeTmpBase();
130
182
  openDatabase(join(base, '.gsd', 'gsd.db'));