@opengsd/gsd-pi 1.5.0 → 1.6.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 (499) hide show
  1. package/README.md +10 -10
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/ask-user-questions.js +1 -1
  4. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
  5. package/dist/resources/extensions/gsd/activity-log.js +1 -1
  6. package/dist/resources/extensions/gsd/auto/closeout.js +97 -5
  7. package/dist/resources/extensions/gsd/auto/orchestrator.js +20 -13
  8. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +6 -3
  9. package/dist/resources/extensions/gsd/auto/unit-phase.js +5 -9
  10. package/dist/resources/extensions/gsd/auto-artifact-paths.js +14 -5
  11. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +11 -7
  12. package/dist/resources/extensions/gsd/auto-dispatch.js +69 -17
  13. package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
  14. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
  15. package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
  16. package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
  17. package/dist/resources/extensions/gsd/auto-start.js +57 -33
  18. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
  19. package/dist/resources/extensions/gsd/auto-timers.js +9 -2
  20. package/dist/resources/extensions/gsd/auto-worktree-branch-lifecycle.js +189 -0
  21. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +133 -0
  22. package/dist/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.js +35 -0
  23. package/dist/resources/extensions/gsd/auto-worktree-creation.js +130 -0
  24. package/dist/resources/extensions/gsd/auto-worktree-entry.js +118 -0
  25. package/dist/resources/extensions/gsd/auto-worktree-merge-already-merged.js +98 -0
  26. package/dist/resources/extensions/gsd/auto-worktree-merge-branch-head.js +52 -0
  27. package/dist/resources/extensions/gsd/auto-worktree-merge-cleanup.js +87 -0
  28. package/dist/resources/extensions/gsd/auto-worktree-merge-code-changes.js +46 -0
  29. package/dist/resources/extensions/gsd/auto-worktree-merge-db-ready.js +68 -0
  30. package/dist/resources/extensions/gsd/auto-worktree-merge-integration-branch.js +51 -0
  31. package/dist/resources/extensions/gsd/auto-worktree-merge-message.js +106 -0
  32. package/dist/resources/extensions/gsd/auto-worktree-merge-pre-teardown.js +64 -0
  33. package/dist/resources/extensions/gsd/auto-worktree-merge-stash.js +168 -0
  34. package/dist/resources/extensions/gsd/auto-worktree-merge.js +302 -0
  35. package/dist/resources/extensions/gsd/auto-worktree-milestone-shelter.js +119 -0
  36. package/dist/resources/extensions/gsd/auto-worktree-path-resolution.js +5 -0
  37. package/dist/resources/extensions/gsd/auto-worktree-path-utils.js +5 -0
  38. package/dist/resources/extensions/gsd/auto-worktree-resource-version.js +41 -0
  39. package/dist/resources/extensions/gsd/auto-worktree-runtime-cleanup.js +99 -0
  40. package/dist/resources/extensions/gsd/auto-worktree-session-registry.js +57 -0
  41. package/dist/resources/extensions/gsd/auto-worktree-sync.js +249 -0
  42. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +120 -0
  43. package/dist/resources/extensions/gsd/auto-worktree.js +20 -2073
  44. package/dist/resources/extensions/gsd/auto.js +106 -34
  45. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -1
  46. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +20 -2
  47. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  48. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +185 -27
  49. package/dist/resources/extensions/gsd/codebase-generator.js +7 -1
  50. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  51. package/dist/resources/extensions/gsd/commands-workflow-templates.js +3 -3
  52. package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
  53. package/dist/resources/extensions/gsd/config-overlay.js +7 -2
  54. package/dist/resources/extensions/gsd/db/engine.js +33 -0
  55. package/dist/resources/extensions/gsd/db/queries.js +9 -9
  56. package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
  57. package/dist/resources/extensions/gsd/db-adapter.js +30 -3
  58. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  59. package/dist/resources/extensions/gsd/doctor-engine-checks.js +93 -11
  60. package/dist/resources/extensions/gsd/flat-phase-migration.js +32 -3
  61. package/dist/resources/extensions/gsd/forensics.js +1 -1
  62. package/dist/resources/extensions/gsd/git-conflict-resolve.js +2 -2
  63. package/dist/resources/extensions/gsd/git-service.js +45 -0
  64. package/dist/resources/extensions/gsd/gsd-db.js +23 -11
  65. package/dist/resources/extensions/gsd/guided-flow.js +22 -17
  66. package/dist/resources/extensions/gsd/journal.js +1 -1
  67. package/dist/resources/extensions/gsd/markdown-renderer.js +34 -79
  68. package/dist/resources/extensions/gsd/md-importer.js +89 -23
  69. package/dist/resources/extensions/gsd/metrics.js +2 -2
  70. package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
  71. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +11 -0
  72. package/dist/resources/extensions/gsd/milestone-settlement.js +1 -1
  73. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  74. package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -1
  75. package/dist/resources/extensions/gsd/paths.js +87 -52
  76. package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
  77. package/dist/resources/extensions/gsd/preferences-models.js +66 -15
  78. package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
  79. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  80. package/dist/resources/extensions/gsd/preferences-validation.js +148 -10
  81. package/dist/resources/extensions/gsd/preferences.js +20 -0
  82. package/dist/resources/extensions/gsd/prompts/system.md +6 -6
  83. package/dist/resources/extensions/gsd/quick.js +12 -3
  84. package/dist/resources/extensions/gsd/rule-registry.js +106 -10
  85. package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
  86. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +2 -1
  87. package/dist/resources/extensions/gsd/state/derive/from-db.js +88 -153
  88. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
  89. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
  90. package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
  91. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  92. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
  93. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
  94. package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
  95. package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
  96. package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
  97. package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
  98. package/dist/resources/extensions/gsd/unit-registry.js +12 -2
  99. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +1 -1
  100. package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
  101. package/dist/resources/extensions/gsd/uok/audit.js +3 -0
  102. package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
  103. package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
  104. package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
  105. package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
  106. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -1
  107. package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
  108. package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
  109. package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
  110. package/dist/resources/extensions/gsd/workspace-git-preflight.js +2 -2
  111. package/dist/resources/extensions/gsd/workspace.js +5 -14
  112. package/dist/resources/extensions/gsd/worktree-command.js +1 -1
  113. package/dist/resources/extensions/gsd/worktree-lifecycle.js +14 -9
  114. package/dist/resources/extensions/gsd/worktree-reentry.js +2 -1
  115. package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
  116. package/dist/resources/extensions/shared/interview-ui.js +62 -9
  117. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  118. package/dist/web/standalone/.next/BUILD_ID +1 -1
  119. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  120. package/dist/web/standalone/.next/build-manifest.json +2 -2
  121. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  122. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  123. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  131. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  132. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  133. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  134. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  135. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  136. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  137. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  139. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  140. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  141. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  142. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  143. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  144. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  145. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  146. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  147. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  148. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  149. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  150. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  151. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  152. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  153. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  154. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  155. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  156. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  157. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  158. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  159. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  160. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  161. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  162. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  163. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  164. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  165. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  166. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  167. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  168. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  169. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  170. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  171. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  172. package/dist/web/standalone/.next/server/app/index.html +1 -1
  173. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  174. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  175. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  176. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  177. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  178. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  179. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  180. package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
  181. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  182. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  183. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  184. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  185. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  186. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  187. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  188. package/dist/worktree-cli-create.d.ts +10 -0
  189. package/dist/worktree-cli-create.js +11 -0
  190. package/dist/worktree-cli-format.d.ts +12 -0
  191. package/dist/worktree-cli-format.js +38 -0
  192. package/dist/worktree-cli-plan.d.ts +16 -0
  193. package/dist/worktree-cli-plan.js +19 -0
  194. package/dist/worktree-cli-session.d.ts +13 -0
  195. package/dist/worktree-cli-session.js +18 -0
  196. package/dist/worktree-cli-status.d.ts +35 -0
  197. package/dist/worktree-cli-status.js +54 -0
  198. package/dist/worktree-cli.d.ts +1 -16
  199. package/dist/worktree-cli.js +28 -122
  200. package/integrations/hermes/open_gsd_hermes/gsd_client.py +25 -36
  201. package/integrations/hermes/pyproject.toml +1 -1
  202. package/integrations/hermes/tests/test_gsd_client_timeout.py +4 -4
  203. package/package.json +6 -6
  204. package/packages/cloud-mcp-gateway/package.json +2 -2
  205. package/packages/contracts/package.json +1 -1
  206. package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
  207. package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
  208. package/packages/daemon/dist/orchestrator-agent.js +113 -0
  209. package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
  210. package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
  211. package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
  212. package/packages/daemon/dist/orchestrator-tools.js +137 -0
  213. package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
  214. package/packages/daemon/dist/orchestrator.d.ts +4 -56
  215. package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
  216. package/packages/daemon/dist/orchestrator.js +13 -303
  217. package/packages/daemon/dist/orchestrator.js.map +1 -1
  218. package/packages/daemon/dist/orchestrator.test.js +11 -1
  219. package/packages/daemon/dist/orchestrator.test.js.map +1 -1
  220. package/packages/daemon/package.json +4 -4
  221. package/packages/gsd-agent-core/dist/export-html/template.js +53 -4
  222. package/packages/gsd-agent-core/package.json +5 -5
  223. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  224. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +3 -2
  225. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  226. package/packages/gsd-agent-modes/package.json +7 -7
  227. package/packages/mcp-server/package.json +4 -4
  228. package/packages/native/package.json +1 -1
  229. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  230. package/packages/pi-agent-core/dist/agent-loop.js +14 -9
  231. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  232. package/packages/pi-agent-core/package.json +1 -1
  233. package/packages/pi-ai/dist/index.d.ts +1 -0
  234. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  235. package/packages/pi-ai/dist/index.js +1 -0
  236. package/packages/pi-ai/dist/index.js.map +1 -1
  237. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  238. package/packages/pi-ai/dist/providers/transform-messages.js +7 -3
  239. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  240. package/packages/pi-ai/dist/tool-result-content.d.ts +3 -0
  241. package/packages/pi-ai/dist/tool-result-content.d.ts.map +1 -0
  242. package/packages/pi-ai/dist/tool-result-content.js +62 -0
  243. package/packages/pi-ai/dist/tool-result-content.js.map +1 -0
  244. package/packages/pi-ai/dist/types.d.ts +8 -20
  245. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  246. package/packages/pi-ai/dist/types.js.map +1 -1
  247. package/packages/pi-ai/package.json +1 -1
  248. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  249. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  250. package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
  251. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  252. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
  253. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  254. package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
  255. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  256. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
  257. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
  258. package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
  259. package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
  260. package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
  261. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  262. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  263. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  264. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  265. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  266. package/packages/pi-coding-agent/dist/index.js +1 -0
  267. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  268. package/packages/pi-coding-agent/package.json +7 -7
  269. package/packages/pi-tui/package.json +2 -2
  270. package/packages/rpc-client/package.json +2 -2
  271. package/pkg/dist/core/export-html/template.js +53 -4
  272. package/pkg/package.json +1 -1
  273. package/src/resources/extensions/ask-user-questions.ts +1 -1
  274. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
  275. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
  276. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
  277. package/src/resources/extensions/gsd/activity-log.ts +1 -1
  278. package/src/resources/extensions/gsd/auto/closeout.ts +114 -7
  279. package/src/resources/extensions/gsd/auto/orchestrator.ts +21 -14
  280. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +6 -3
  281. package/src/resources/extensions/gsd/auto/unit-phase.ts +5 -9
  282. package/src/resources/extensions/gsd/auto-artifact-paths.ts +14 -4
  283. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +13 -7
  284. package/src/resources/extensions/gsd/auto-dispatch.ts +77 -16
  285. package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
  286. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
  287. package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
  288. package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
  289. package/src/resources/extensions/gsd/auto-start.ts +62 -31
  290. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
  291. package/src/resources/extensions/gsd/auto-timers.ts +9 -2
  292. package/src/resources/extensions/gsd/auto-worktree-branch-lifecycle.ts +237 -0
  293. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +166 -0
  294. package/src/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.ts +37 -0
  295. package/src/resources/extensions/gsd/auto-worktree-creation.ts +171 -0
  296. package/src/resources/extensions/gsd/auto-worktree-entry.ts +154 -0
  297. package/src/resources/extensions/gsd/auto-worktree-merge-already-merged.ts +161 -0
  298. package/src/resources/extensions/gsd/auto-worktree-merge-branch-head.ts +83 -0
  299. package/src/resources/extensions/gsd/auto-worktree-merge-cleanup.ts +132 -0
  300. package/src/resources/extensions/gsd/auto-worktree-merge-code-changes.ts +71 -0
  301. package/src/resources/extensions/gsd/auto-worktree-merge-db-ready.ts +112 -0
  302. package/src/resources/extensions/gsd/auto-worktree-merge-integration-branch.ts +99 -0
  303. package/src/resources/extensions/gsd/auto-worktree-merge-message.ts +166 -0
  304. package/src/resources/extensions/gsd/auto-worktree-merge-pre-teardown.ts +93 -0
  305. package/src/resources/extensions/gsd/auto-worktree-merge-stash.ts +213 -0
  306. package/src/resources/extensions/gsd/auto-worktree-merge.ts +391 -0
  307. package/src/resources/extensions/gsd/auto-worktree-milestone-shelter.ts +159 -0
  308. package/src/resources/extensions/gsd/auto-worktree-path-resolution.ts +6 -0
  309. package/src/resources/extensions/gsd/auto-worktree-path-utils.ts +10 -0
  310. package/src/resources/extensions/gsd/auto-worktree-resource-version.ts +44 -0
  311. package/src/resources/extensions/gsd/auto-worktree-runtime-cleanup.ts +119 -0
  312. package/src/resources/extensions/gsd/auto-worktree-session-registry.ts +71 -0
  313. package/src/resources/extensions/gsd/auto-worktree-sync.ts +370 -0
  314. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +161 -0
  315. package/src/resources/extensions/gsd/auto-worktree.ts +69 -2429
  316. package/src/resources/extensions/gsd/auto.ts +114 -47
  317. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -1
  318. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +20 -2
  319. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  320. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +218 -27
  321. package/src/resources/extensions/gsd/codebase-generator.ts +7 -1
  322. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  323. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -3
  324. package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
  325. package/src/resources/extensions/gsd/config-overlay.ts +7 -1
  326. package/src/resources/extensions/gsd/db/engine.ts +36 -0
  327. package/src/resources/extensions/gsd/db/queries.ts +9 -9
  328. package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
  329. package/src/resources/extensions/gsd/db-adapter.ts +29 -3
  330. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  331. package/src/resources/extensions/gsd/doctor-engine-checks.ts +109 -10
  332. package/src/resources/extensions/gsd/flat-phase-migration.ts +34 -3
  333. package/src/resources/extensions/gsd/forensics.ts +1 -1
  334. package/src/resources/extensions/gsd/git-conflict-resolve.ts +2 -2
  335. package/src/resources/extensions/gsd/git-service.ts +53 -0
  336. package/src/resources/extensions/gsd/gsd-db.ts +33 -11
  337. package/src/resources/extensions/gsd/guided-flow.ts +26 -18
  338. package/src/resources/extensions/gsd/journal.ts +1 -1
  339. package/src/resources/extensions/gsd/markdown-renderer.ts +52 -91
  340. package/src/resources/extensions/gsd/md-importer.ts +93 -22
  341. package/src/resources/extensions/gsd/metrics.ts +2 -2
  342. package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
  343. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +15 -0
  344. package/src/resources/extensions/gsd/milestone-settlement.ts +1 -1
  345. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  346. package/src/resources/extensions/gsd/parallel-orchestrator.ts +3 -1
  347. package/src/resources/extensions/gsd/paths.ts +108 -51
  348. package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
  349. package/src/resources/extensions/gsd/preferences-models.ts +74 -20
  350. package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
  351. package/src/resources/extensions/gsd/preferences-types.ts +54 -1
  352. package/src/resources/extensions/gsd/preferences-validation.ts +146 -9
  353. package/src/resources/extensions/gsd/preferences.ts +20 -0
  354. package/src/resources/extensions/gsd/prompts/system.md +6 -6
  355. package/src/resources/extensions/gsd/quick.ts +11 -3
  356. package/src/resources/extensions/gsd/rule-registry.ts +111 -9
  357. package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
  358. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +3 -1
  359. package/src/resources/extensions/gsd/state/derive/from-db.ts +201 -157
  360. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
  361. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
  362. package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
  363. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
  364. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +2 -1
  365. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
  366. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
  367. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +57 -0
  368. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +1 -1
  369. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
  370. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
  371. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +2 -1
  372. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +1 -1
  373. package/src/resources/extensions/gsd/tests/auto-worktree-merge-already-merged.test.ts +177 -0
  374. package/src/resources/extensions/gsd/tests/auto-worktree-merge-branch-head.test.ts +118 -0
  375. package/src/resources/extensions/gsd/tests/auto-worktree-merge-cleanup.test.ts +115 -0
  376. package/src/resources/extensions/gsd/tests/auto-worktree-merge-code-changes.test.ts +76 -0
  377. package/src/resources/extensions/gsd/tests/auto-worktree-merge-db-ready.test.ts +140 -0
  378. package/src/resources/extensions/gsd/tests/auto-worktree-merge-integration-branch.test.ts +123 -0
  379. package/src/resources/extensions/gsd/tests/auto-worktree-merge-message.test.ts +114 -0
  380. package/src/resources/extensions/gsd/tests/auto-worktree-merge-pre-teardown.test.ts +84 -0
  381. package/src/resources/extensions/gsd/tests/auto-worktree-merge-stash.test.ts +70 -0
  382. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-shelter.test.ts +52 -0
  383. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +6 -6
  384. package/src/resources/extensions/gsd/tests/auto-worktree-resource-version.test.ts +59 -0
  385. package/src/resources/extensions/gsd/tests/auto-worktree-untracked-content.test.ts +3 -1
  386. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +22 -0
  387. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
  388. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
  389. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
  390. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +1 -1
  391. package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
  392. package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
  393. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -0
  394. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +174 -0
  395. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +1 -1
  396. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +71 -1
  397. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +46 -1
  398. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
  399. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +40 -0
  400. package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
  401. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +4 -6
  402. package/src/resources/extensions/gsd/tests/integration/auto-stash-merge.test.ts +2 -1
  403. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +3 -3
  404. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +7 -8
  405. package/src/resources/extensions/gsd/tests/integration/feature-branch-lifecycle-integration.test.ts +2 -5
  406. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +76 -0
  407. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +1 -1
  408. package/src/resources/extensions/gsd/tests/integration/merge-preserve-worktree-on-preteardown-error.test.ts +1 -1
  409. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +2 -1
  410. package/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +5 -7
  411. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
  412. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +2 -4
  413. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
  414. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +1 -1
  415. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +1 -1
  416. package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +1 -1
  417. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
  418. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
  419. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +439 -0
  420. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +24 -0
  421. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +1 -1
  422. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -3
  423. package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +1 -1
  424. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  425. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
  426. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
  427. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +1 -4
  428. package/src/resources/extensions/gsd/tests/preferences.test.ts +19 -0
  429. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +5 -0
  430. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
  431. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
  432. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
  433. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
  434. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  435. package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
  436. package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
  437. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
  438. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
  439. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -5
  440. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
  441. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +2 -1
  442. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +2 -1
  443. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +1 -1
  444. package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
  445. package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
  446. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +1 -1
  447. package/src/resources/extensions/gsd/tests/system-flat-phase-layout.test.ts +33 -0
  448. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +4 -4
  449. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +2 -1
  450. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +4 -4
  451. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +196 -0
  452. package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
  453. package/src/resources/extensions/gsd/tests/uat-stuck-loop-orphaned-worktree.test.ts +1 -1
  454. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +26 -1
  455. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
  456. package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
  457. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
  458. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
  459. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -1
  460. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +1 -1
  461. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +1 -3
  462. package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +1 -1
  463. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +1 -1
  464. package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +1 -1
  465. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +1 -1
  466. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +2 -1
  467. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +5 -2
  468. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +1 -1
  469. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +1 -1
  470. package/src/resources/extensions/gsd/tool-contract.ts +29 -0
  471. package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
  472. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
  473. package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
  474. package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
  475. package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
  476. package/src/resources/extensions/gsd/types.ts +23 -4
  477. package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
  478. package/src/resources/extensions/gsd/unit-registry.ts +16 -2
  479. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +1 -1
  480. package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
  481. package/src/resources/extensions/gsd/uok/audit.ts +2 -0
  482. package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
  483. package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
  484. package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
  485. package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
  486. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -1
  487. package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
  488. package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
  489. package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
  490. package/src/resources/extensions/gsd/workspace-git-preflight.ts +2 -2
  491. package/src/resources/extensions/gsd/workspace.ts +5 -21
  492. package/src/resources/extensions/gsd/worktree-command.ts +1 -1
  493. package/src/resources/extensions/gsd/worktree-lifecycle.ts +19 -15
  494. package/src/resources/extensions/gsd/worktree-reentry.ts +2 -1
  495. package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
  496. package/src/resources/extensions/shared/interview-ui.ts +58 -11
  497. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
  498. /package/dist/web/standalone/.next/static/{bCg3aYrFN1DKZr2dqgE7L → 6xYMOzxVhsQgryneJ5KRc}/_buildManifest.js +0 -0
  499. /package/dist/web/standalone/.next/static/{bCg3aYrFN1DKZr2dqgE7L → 6xYMOzxVhsQgryneJ5KRc}/_ssgManifest.js +0 -0
@@ -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);
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
12
12
  import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, readdirSync, rmSync } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { tmpdir } from "node:os";
15
- import { syncWorktreeStateBack } from "../auto-worktree.ts";
15
+ import { syncGsdStateToWorktree, syncWorktreeStateBack } from "../auto-worktree-sync.ts";
16
16
 
17
17
  test("#2684: syncWorktreeStateBack does not overwrite project PREFERENCES.md", () => {
18
18
  const mainBase = mkdtempSync(join(tmpdir(), "gsd-wt-prefs-main-"));
@@ -58,8 +58,6 @@ test("syncGsdStateToWorktree copies canonical PREFERENCES.md", async () => {
58
58
  "---\nversion: 1\n---\n\npost_unit_hooks:\n - name: notify\n command: echo done\n",
59
59
  );
60
60
 
61
- // Import and call syncGsdStateToWorktree
62
- const { syncGsdStateToWorktree } = await import("../auto-worktree.ts");
63
61
  syncGsdStateToWorktree(srcBase, dstBase);
64
62
 
65
63
  // Verify PREFERENCES.md was copied
@@ -93,7 +91,6 @@ test("syncGsdStateToWorktree falls back to legacy lowercase preferences.md", asy
93
91
  "---\nversion: 1\n---\n\ngit:\n auto_push: true\n",
94
92
  );
95
93
 
96
- const { syncGsdStateToWorktree } = await import("../auto-worktree.ts");
97
94
  const result = syncGsdStateToWorktree(srcBase, dstBase);
98
95
 
99
96
  const copiedEntries = readdirSync(dstGsd)
@@ -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
  "---",
@@ -79,6 +79,11 @@ const PREF_SAMPLE_VALUES: Record<string, unknown> = {
79
79
  min_request_interval_ms: 250,
80
80
  stale_commit_threshold_minutes: 15,
81
81
  context_management: { enabled: true },
82
+ tool_call_loop_guard: {
83
+ enabled: true,
84
+ identical_args: { enabled: true, max_consecutive_calls: 4 },
85
+ repeated_tool: { enabled: true, default_cap: 6, repeatable_cap: 15, exempt_tools: ["ctx_execute"] },
86
+ },
82
87
  experimental: { rtk: true },
83
88
  codebase: { indexing: "auto" },
84
89
  slice_parallel: { enabled: true, max_workers: 2 },
@@ -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'));
@@ -5,7 +5,7 @@
5
5
 
6
6
  import assert from 'node:assert/strict';
7
7
  import { test, describe, beforeEach } from "node:test";
8
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { emitJournalEvent } from "../journal.ts";
@@ -544,3 +544,97 @@ describe("RuleRegistry", () => {
544
544
  assert.deepStrictEqual(result.matchedRule, "<no-match>", "matchedRule is '<no-match>' on fallback");
545
545
  });
546
546
  });
547
+
548
+ describe("resolveHookArtifactPath", () => {
549
+ test("resolves a phase-level artifact from the .gsd/phases layout", () => {
550
+ const originalGsdHome = process.env.GSD_HOME;
551
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-phase-"));
552
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
553
+ try {
554
+ process.env.GSD_HOME = tempGsdHome;
555
+ const phaseDir = join(realpathSync(projectRoot), ".gsd", "phases", "50-some-phase");
556
+ mkdirSync(phaseDir, { recursive: true });
557
+ const artifactPath = join(phaseDir, "BROWSER-RUNTIME-EVIDENCE.md");
558
+ writeFileSync(artifactPath, "---\nverdict: advisory\n---\n", "utf-8");
559
+
560
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "BROWSER-RUNTIME-EVIDENCE.md");
561
+ assert.equal(resolved, artifactPath, "resolves the canonical phase-level artifact");
562
+ } finally {
563
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
564
+ else process.env.GSD_HOME = originalGsdHome;
565
+ rmSync(projectRoot, { recursive: true, force: true });
566
+ rmSync(tempGsdHome, { recursive: true, force: true });
567
+ }
568
+ });
569
+
570
+ test("falls back to the legacy milestones/slices/tasks layout", () => {
571
+ const originalGsdHome = process.env.GSD_HOME;
572
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-"));
573
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
574
+ try {
575
+ process.env.GSD_HOME = tempGsdHome;
576
+ const tasksDir = join(projectRoot, ".gsd", "milestones", "M050", "slices", "S02", "tasks");
577
+ mkdirSync(tasksDir, { recursive: true });
578
+ const artifactPath = join(tasksDir, "T01-REVIEW.md");
579
+ writeFileSync(artifactPath, "---\nverdict: pass\n---\n", "utf-8");
580
+
581
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
582
+ assert.equal(resolved, artifactPath, "resolves the task-prefixed legacy artifact");
583
+ } finally {
584
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
585
+ else process.env.GSD_HOME = originalGsdHome;
586
+ rmSync(projectRoot, { recursive: true, force: true });
587
+ rmSync(tempGsdHome, { recursive: true, force: true });
588
+ }
589
+ });
590
+
591
+ test("legacy: nested task artifact wins over a milestone-root file of the same name", () => {
592
+ const originalGsdHome = process.env.GSD_HOME;
593
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-root-"));
594
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
595
+ try {
596
+ process.env.GSD_HOME = tempGsdHome;
597
+ const milestoneDir = join(projectRoot, ".gsd", "milestones", "M050");
598
+ const tasksDir = join(milestoneDir, "slices", "S02", "tasks");
599
+ mkdirSync(tasksDir, { recursive: true });
600
+ // The correct task-scoped artifact lives in the nested slices/tasks tree.
601
+ const nestedPath = join(tasksDir, "T01-REVIEW.md");
602
+ writeFileSync(nestedPath, "---\nverdict: pass\n---\n", "utf-8");
603
+ // A same-named decoy at the legacy milestone root must NOT win — before the
604
+ // fix, resolveMilestonePath returned the milestone root and it was probed
605
+ // ahead of the nested task path (#1264 Bugbot follow-up).
606
+ writeFileSync(join(milestoneDir, "REVIEW.md"), "---\nverdict: failed\n---\n", "utf-8");
607
+
608
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
609
+ assert.equal(resolved, nestedPath, "task-scoped legacy path wins over the milestone-root file");
610
+ } finally {
611
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
612
+ else process.env.GSD_HOME = originalGsdHome;
613
+ rmSync(projectRoot, { recursive: true, force: true });
614
+ rmSync(tempGsdHome, { recursive: true, force: true });
615
+ }
616
+ });
617
+
618
+ test("legacy: missing-artifact fallback points at the nested task path, not the milestone root", () => {
619
+ const originalGsdHome = process.env.GSD_HOME;
620
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-miss-"));
621
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
622
+ try {
623
+ process.env.GSD_HOME = tempGsdHome;
624
+ const milestoneDir = join(projectRoot, ".gsd", "milestones", "M050");
625
+ mkdirSync(milestoneDir, { recursive: true });
626
+ // Content-bearing legacy milestone dir (a non-META file) so resolveMilestonePath
627
+ // returns it, but the requested gate artifact does not exist anywhere.
628
+ writeFileSync(join(milestoneDir, "M050-ROADMAP.md"), "# roadmap\n", "utf-8");
629
+
630
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
631
+ const expected = join(milestoneDir, "slices", "S02", "tasks", "T01-REVIEW.md");
632
+ assert.equal(resolved, expected, "diagnostic fallback uses the nested task path for a legacy task-scoped unit");
633
+ } finally {
634
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
635
+ else process.env.GSD_HOME = originalGsdHome;
636
+ rmSync(projectRoot, { recursive: true, force: true });
637
+ rmSync(tempGsdHome, { recursive: true, force: true });
638
+ }
639
+ });
640
+ });