gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.d8826a445

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 (300) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  9. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  10. package/dist/resources/extensions/gsd/auto-dispatch.js +7 -58
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +14 -28
  12. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  13. package/dist/resources/extensions/gsd/auto-worktree.js +244 -216
  14. package/dist/resources/extensions/gsd/auto.js +86 -7
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  16. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  17. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  18. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  19. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  20. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  21. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  22. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  23. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  24. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  25. package/dist/resources/extensions/gsd/db-writer.js +106 -95
  26. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  27. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  28. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  29. package/dist/resources/extensions/gsd/gsd-db.js +268 -8
  30. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  31. package/dist/resources/extensions/gsd/guided-flow.js +141 -32
  32. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  33. package/dist/resources/extensions/gsd/metrics.js +287 -1
  34. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  35. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  36. package/dist/resources/extensions/gsd/paths.js +114 -9
  37. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  39. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  40. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  41. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  42. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  44. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  45. package/dist/resources/extensions/gsd/rethink.js +2 -2
  46. package/dist/resources/extensions/gsd/state.js +91 -372
  47. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  48. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  49. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  50. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  51. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  52. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  53. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  54. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  55. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  56. package/dist/resources/extensions/gsd/workspace.js +59 -0
  57. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  58. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  59. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  60. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  61. package/dist/web/standalone/.next/BUILD_ID +1 -1
  62. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  63. package/dist/web/standalone/.next/build-manifest.json +2 -2
  64. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  65. package/dist/web/standalone/.next/required-server-files.json +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/index.html +1 -1
  115. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  116. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  117. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  118. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  119. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  122. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  123. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  124. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  126. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  127. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  128. package/dist/web/standalone/server.js +1 -1
  129. package/package.json +1 -1
  130. package/packages/mcp-server/README.md +2 -11
  131. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  132. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  133. package/packages/mcp-server/dist/remote-questions.js +28 -0
  134. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  135. package/packages/mcp-server/dist/server.d.ts +28 -0
  136. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  137. package/packages/mcp-server/dist/server.js +94 -4
  138. package/packages/mcp-server/dist/server.js.map +1 -1
  139. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  140. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  141. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  142. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  143. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  144. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  145. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  146. package/packages/mcp-server/src/remote-questions.ts +35 -0
  147. package/packages/mcp-server/src/server.ts +129 -6
  148. package/packages/mcp-server/src/workflow-tools.ts +62 -3
  149. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  150. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  151. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  152. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  153. package/src/resources/extensions/gsd/auto-dispatch.ts +14 -62
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +15 -27
  155. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  156. package/src/resources/extensions/gsd/auto-worktree.ts +286 -251
  157. package/src/resources/extensions/gsd/auto.ts +102 -7
  158. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  159. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  160. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  161. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  162. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  163. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  164. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  165. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  166. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  167. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  168. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  169. package/src/resources/extensions/gsd/db-writer.ts +123 -94
  170. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  171. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  172. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  173. package/src/resources/extensions/gsd/gsd-db.ts +269 -8
  174. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  175. package/src/resources/extensions/gsd/guided-flow.ts +181 -32
  176. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  177. package/src/resources/extensions/gsd/metrics.ts +321 -1
  178. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  179. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  180. package/src/resources/extensions/gsd/paths.ts +122 -9
  181. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  182. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  183. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  184. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  185. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  186. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  187. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  188. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  189. package/src/resources/extensions/gsd/rethink.ts +2 -2
  190. package/src/resources/extensions/gsd/state.ts +91 -389
  191. package/src/resources/extensions/gsd/templates/project.md +10 -0
  192. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  193. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  194. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  195. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  196. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  197. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  198. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  199. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  200. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  201. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  202. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  203. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  204. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  205. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  206. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  207. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  208. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  209. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  210. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  211. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  212. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  213. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  214. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  215. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  216. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  217. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  218. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  219. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  220. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  221. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  222. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  223. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  224. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  225. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  226. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  227. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  228. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  229. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  230. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  231. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  232. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  233. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  234. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  235. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  236. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  237. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  238. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  239. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  240. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  241. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  242. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  243. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  244. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  245. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  246. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  247. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  248. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  249. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  250. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  251. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  252. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  253. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  254. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  255. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  256. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  257. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  258. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  259. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  260. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  261. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  262. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  263. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  264. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  265. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  266. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  267. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  268. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  269. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  270. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  271. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  272. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  273. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  274. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  275. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  276. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  277. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +26 -3
  278. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  279. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  280. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  281. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  282. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  283. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  284. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  285. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  286. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  287. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  288. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  289. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  290. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  291. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  292. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  293. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  294. package/src/resources/extensions/gsd/workspace.ts +95 -0
  295. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  296. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  297. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  298. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  299. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  300. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
@@ -8,7 +8,7 @@
8
8
  // Critical invariant: generated markdown must round-trip through
9
9
  // parseDecisionsTable() and parseRequirementsSections() with field fidelity.
10
10
 
11
- import { join, resolve } from 'node:path';
11
+ import { isAbsolute, join, relative, resolve } from 'node:path';
12
12
  import { readFileSync, existsSync, statSync } from 'node:fs';
13
13
  import type { Decision, Requirement } from './types.js';
14
14
  import { resolveGsdRootFile } from './paths.js';
@@ -18,6 +18,8 @@ import { logWarning, logError } from './workflow-logger.js';
18
18
  import { invalidateStateCache } from './state.js';
19
19
  import { clearPathCache } from './paths.js';
20
20
  import { clearParseCache } from './files.js';
21
+ import type { MilestoneScope, GsdWorkspace } from './workspace.js';
22
+ import { createWorkspace, scopeMilestone } from './workspace.js';
21
23
 
22
24
  // ─── Freeform Detection ───────────────────────────────────────────────────
23
25
 
@@ -318,23 +320,6 @@ export async function saveRequirementToDb(
318
320
  LIMIT 1`,
319
321
  )
320
322
  .get({ ':description': fields.description });
321
- const previousRow: Requirement | null = existingRow
322
- ? {
323
- id: existingRow['id'] as string,
324
- class: existingRow['class'] as string,
325
- status: existingRow['status'] as string,
326
- description: existingRow['description'] as string,
327
- why: existingRow['why'] as string,
328
- source: existingRow['source'] as string,
329
- primary_owner: existingRow['primary_owner'] as string,
330
- supporting_slices: existingRow['supporting_slices'] as string,
331
- validation: existingRow['validation'] as string,
332
- notes: existingRow['notes'] as string,
333
- full_content: existingRow['full_content'] as string,
334
- superseded_by: (existingRow['superseded_by'] as string) ?? null,
335
- }
336
- : null;
337
-
338
323
  const row = adapter
339
324
  .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
340
325
  .get();
@@ -361,9 +346,9 @@ export async function saveRequirementToDb(
361
346
  };
362
347
 
363
348
  db.upsertRequirement(requirement);
364
- return { id: nextId, isNew: !existingRow, previousRow };
349
+ return { id: nextId };
365
350
  });
366
- const { id, isNew, previousRow } = txResult;
351
+ const { id } = txResult;
367
352
 
368
353
  // Fetch all requirements for full file regeneration
369
354
  const adapter = db._getAdapter();
@@ -392,17 +377,7 @@ export async function saveRequirementToDb(
392
377
  try {
393
378
  await saveFile(filePath, md);
394
379
  } catch (diskErr) {
395
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveRequirementToDb', error: String((diskErr as Error).message) });
396
- try {
397
- if (isNew) {
398
- db.deleteRequirementById(id);
399
- } else if (previousRow) {
400
- db.upsertRequirement(previousRow);
401
- }
402
- } catch (rollbackErr) {
403
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveRequirementToDb', id, error: String((rollbackErr as Error).message) });
404
- }
405
- throw diskErr;
380
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement remains committed', { fn: 'saveRequirementToDb', id, error: String((diskErr as Error).message) });
406
381
  }
407
382
  invalidateStateCache();
408
383
  clearPathCache();
@@ -538,13 +513,7 @@ export async function saveDecisionToDb(
538
513
  try {
539
514
  await saveFile(filePath, md);
540
515
  } catch (diskErr) {
541
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveDecisionToDb', error: String((diskErr as Error).message) });
542
- try {
543
- db.deleteDecisionById(id);
544
- } catch (rollbackErr) {
545
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveDecisionToDb', id, error: String((rollbackErr as Error).message) });
546
- }
547
- throw diskErr;
516
+ logWarning('projection', 'DECISIONS.md projection write failed; DB decision remains committed', { fn: 'saveDecisionToDb', id, error: String((diskErr as Error).message) });
548
517
  }
549
518
  // #2661: When a decision defers a slice, update the slice status in the DB
550
519
  // so the dispatcher skips it. Without this, STATE.md and DECISIONS.md are
@@ -667,34 +636,7 @@ export async function updateRequirementInDb(
667
636
  try {
668
637
  const db = await import('./gsd-db.js');
669
638
 
670
- let existing = db.getRequirementById(id);
671
-
672
- // If requirement doesn't exist in DB, seed the entire requirements table
673
- // from REQUIREMENTS.md first (#3346). This handles the standard workflow
674
- // where requirements are authored in markdown during discussion but never
675
- // imported into the database — making gsd_requirement_update always fail
676
- // with "not_found" at milestone completion.
677
- if (!existing) {
678
- const reqFilePath = resolveGsdRootFile(basePath, 'REQUIREMENTS');
679
- try {
680
- const content = readFileSync(reqFilePath, 'utf-8');
681
- const { parseRequirementsSections } = await import('./md-importer.js');
682
- const parsed = parseRequirementsSections(content);
683
- if (parsed.length > 0) {
684
- logWarning('manifest', `Seeding ${parsed.length} requirements from REQUIREMENTS.md into DB (first update triggers import)`, { fn: 'updateRequirementInDb' });
685
- for (const req of parsed) {
686
- // Only seed if not already in DB (avoid overwriting concurrent inserts)
687
- if (!db.getRequirementById(req.id)) {
688
- db.upsertRequirement(req);
689
- }
690
- }
691
- // Re-check after seeding
692
- existing = db.getRequirementById(id);
693
- }
694
- } catch {
695
- // REQUIREMENTS.md missing or unparseable — fall through to skeleton
696
- }
697
- }
639
+ const existing = db.getRequirementById(id);
698
640
 
699
641
  const base: Requirement = existing ?? {
700
642
  id,
@@ -750,11 +692,7 @@ export async function updateRequirementInDb(
750
692
  try {
751
693
  await saveFile(filePath, md);
752
694
  } catch (diskErr) {
753
- logError('manifest', 'disk write failed, reverting DB row', { fn: 'updateRequirementInDb', error: String((diskErr as Error).message) });
754
- if (existing) {
755
- db.upsertRequirement(existing);
756
- }
757
- throw diskErr;
695
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement update remains committed', { fn: 'updateRequirementInDb', id, error: String((diskErr as Error).message) });
758
696
  }
759
697
  // Invalidate file-read caches so deriveState() sees the updated markdown.
760
698
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
@@ -779,46 +717,118 @@ export interface SaveArtifactOpts {
779
717
  }
780
718
 
781
719
  /**
782
- * Save an artifact to DB and write the corresponding markdown file to disk.
720
+ * Save a root-level artifact (no milestone) to DB and write to disk,
721
+ * routing path construction through workspace.contract.projectGsd directly.
722
+ * Use this instead of saveArtifactToDbByScope when milestone_id is absent.
723
+ */
724
+ export async function saveArtifactToDbForWorkspace(
725
+ workspace: GsdWorkspace,
726
+ opts: SaveArtifactOpts,
727
+ ): Promise<void> {
728
+ try {
729
+ const db = await import('./gsd-db.js');
730
+
731
+ const gsdDir = workspace.contract.projectGsd;
732
+ const fullPath = resolve(gsdDir, opts.path);
733
+
734
+ const rel0 = relative(gsdDir, fullPath);
735
+ if (rel0.startsWith('..') || isAbsolute(rel0)) {
736
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbForWorkspace: path escapes .gsd/ directory: ${opts.path}`);
737
+ }
738
+
739
+ let contentToPersist = opts.content;
740
+ if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
741
+ const activeRequirements = db.getActiveRequirements();
742
+ if (activeRequirements.length === 0) {
743
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbForWorkspace: REQUIREMENTS final save requires active DB-backed requirements');
744
+ }
745
+ contentToPersist = generateRequirementsMd(activeRequirements);
746
+ }
747
+
748
+ let skipDiskWrite = false;
749
+ if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
750
+ const existingSize = statSync(fullPath).size;
751
+ const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
752
+ if (existingSize > 0 && newSize < existingSize * 0.5) {
753
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbForWorkspace', path: opts.path });
754
+ skipDiskWrite = true;
755
+ }
756
+ }
757
+
758
+ db.insertArtifact({
759
+ path: opts.path,
760
+ artifact_type: opts.artifact_type,
761
+ milestone_id: null,
762
+ slice_id: null,
763
+ task_id: null,
764
+ full_content: contentToPersist,
765
+ });
766
+
767
+ if (!skipDiskWrite) {
768
+ try {
769
+ await saveFile(fullPath, contentToPersist);
770
+ } catch (diskErr) {
771
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbForWorkspace', path: opts.path, error: String((diskErr as Error).message) });
772
+ }
773
+ }
774
+ invalidateStateCache();
775
+ clearPathCache();
776
+ clearParseCache();
777
+ } catch (err) {
778
+ logError('manifest', 'saveArtifactToDbForWorkspace failed', { fn: 'saveArtifactToDbForWorkspace', error: String((err as Error).message) });
779
+ throw err;
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Save an artifact to DB and write the corresponding markdown file to disk,
785
+ * routing all path construction through the workspace contract.
786
+ *
783
787
  * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
784
- * The full file path is computed as basePath + '.gsd/' + path.
788
+ * The full file path is computed as scope.workspace.contract.projectGsd + '/' + path.
785
789
  */
786
- export async function saveArtifactToDb(
790
+ export async function saveArtifactToDbByScope(
791
+ scope: MilestoneScope,
787
792
  opts: SaveArtifactOpts,
788
- basePath: string,
789
793
  ): Promise<void> {
794
+ // Guard: an empty milestoneId produces malformed paths (milestoneDir = join(gsd, "milestones", "")).
795
+ // Callers that have no milestone should use saveArtifactToDbForWorkspace instead.
796
+ if (!scope.milestoneId) {
797
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: milestoneId is empty — use saveArtifactToDbForWorkspace for root artifacts`);
798
+ }
799
+
790
800
  try {
791
801
  const db = await import('./gsd-db.js');
792
802
 
803
+ // Use contract.projectGsd as the canonical .gsd directory — never a hand-rolled basePath join.
804
+ const gsdDir = scope.workspace.contract.projectGsd;
805
+ const fullPath = resolve(gsdDir, opts.path);
806
+
793
807
  // Guard against path traversal before any reads/writes
794
- const gsdDir = resolve(basePath, '.gsd');
795
- const fullPath = resolve(basePath, '.gsd', opts.path);
796
- if (!fullPath.startsWith(gsdDir)) {
797
- throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
808
+ const rel1 = relative(gsdDir, fullPath);
809
+ if (rel1.startsWith('..') || isAbsolute(rel1)) {
810
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: path escapes .gsd/ directory: ${opts.path}`);
798
811
  }
812
+
799
813
  let contentToPersist = opts.content;
800
814
  if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
801
815
  const activeRequirements = db.getActiveRequirements();
802
816
  if (activeRequirements.length === 0) {
803
- throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDb: REQUIREMENTS final save requires active DB-backed requirements');
817
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbByScope: REQUIREMENTS final save requires active DB-backed requirements');
804
818
  }
805
819
  contentToPersist = generateRequirementsMd(activeRequirements);
806
820
  }
807
821
 
808
- // Shrinkage guard: if the file already exists and the new content is
809
- // significantly smaller (<50%), preserve the richer file on disk and
810
- // store its content in the DB instead of the abbreviated version. Root
811
- // canonical artifacts are exempt because their content is rendered from
812
- // canonical DB state, and cleanup/consolidation is often intentionally much
813
- // smaller than a malformed accumulated file.
814
- let dbContent = contentToPersist;
822
+ // Shrinkage guard: if the projection file already exists and the new
823
+ // content is significantly smaller (<50%), preserve the richer file on
824
+ // disk, but keep the DB row authoritative with the caller-provided content.
825
+ // Root canonical artifacts are exempt (rendered from canonical DB state).
815
826
  let skipDiskWrite = false;
816
827
  if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
817
828
  const existingSize = statSync(fullPath).size;
818
829
  const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
819
830
  if (existingSize > 0 && newSize < existingSize * 0.5) {
820
- logWarning('manifest', `new content (${newSize}B) is <50% of existing file (${existingSize}B), preserving disk file`, { fn: 'saveArtifactToDb', path: opts.path });
821
- dbContent = readFileSync(fullPath, 'utf-8');
831
+ logWarning('projection', `new content (${newSize}B) is <50% of existing projection (${existingSize}B), preserving disk file while DB remains authoritative`, { fn: 'saveArtifactToDbByScope', path: opts.path });
822
832
  skipDiskWrite = true;
823
833
  }
824
834
  }
@@ -829,7 +839,7 @@ export async function saveArtifactToDb(
829
839
  milestone_id: opts.milestone_id ?? null,
830
840
  slice_id: opts.slice_id ?? null,
831
841
  task_id: opts.task_id ?? null,
832
- full_content: dbContent,
842
+ full_content: contentToPersist,
833
843
  });
834
844
 
835
845
  // Write the file to disk (only if we're not preserving a richer existing file)
@@ -837,9 +847,7 @@ export async function saveArtifactToDb(
837
847
  try {
838
848
  await saveFile(fullPath, contentToPersist);
839
849
  } catch (diskErr) {
840
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveArtifactToDb', error: String((diskErr as Error).message) });
841
- db.deleteArtifactByPath(opts.path);
842
- throw diskErr;
850
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbByScope', path: opts.path, error: String((diskErr as Error).message) });
843
851
  }
844
852
  }
845
853
  // Invalidate file-read caches so deriveState() sees the updated markdown.
@@ -848,7 +856,28 @@ export async function saveArtifactToDb(
848
856
  clearPathCache();
849
857
  clearParseCache();
850
858
  } catch (err) {
851
- logError('manifest', 'saveArtifactToDb failed', { fn: 'saveArtifactToDb', error: String((err as Error).message) });
859
+ logError('manifest', 'saveArtifactToDbByScope failed', { fn: 'saveArtifactToDbByScope', error: String((err as Error).message) });
852
860
  throw err;
853
861
  }
854
862
  }
863
+
864
+ /**
865
+ * Save an artifact to DB and write the corresponding markdown file to disk.
866
+ * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
867
+ * The full file path is computed as basePath + '.gsd/' + path.
868
+ *
869
+ * @deprecated Use saveArtifactToDbByScope instead, which routes through the
870
+ * workspace contract for canonical path resolution.
871
+ * TODO(C-future): remove this legacy wrapper once all callers are migrated.
872
+ */
873
+ export async function saveArtifactToDb(
874
+ opts: SaveArtifactOpts,
875
+ basePath: string,
876
+ ): Promise<void> {
877
+ const workspace = createWorkspace(basePath);
878
+ const milestoneId = opts.milestone_id;
879
+ if (milestoneId) {
880
+ return saveArtifactToDbByScope(scopeMilestone(workspace, milestoneId), opts);
881
+ }
882
+ return saveArtifactToDbForWorkspace(workspace, opts);
883
+ }
@@ -0,0 +1,197 @@
1
+ // Delegation policy — codifies which GSD MCP tools are safe to run as
2
+ // background sub-agents while the foreground /gsd flow continues. Verdicts
3
+ // are derived from the round-1 and round-2 evaluations recorded in this
4
+ // branch's PR description; the rationale field on each entry preserves
5
+ // the reason so future changes have to revisit the analysis explicitly.
6
+ //
7
+ // Default-deny: unknown tools are never backgroundable.
8
+ //
9
+ // ─── Tool-name vs unit-type namespaces ───────────────────────────────────
10
+ // Entries are keyed by canonical MCP tool name (`gsd_*`). The optional
11
+ // `unitType` field is a *secondary* index for the dispatcher's convenience
12
+ // — it bridges this policy to `auto-dispatch.ts`' `DispatchAction.unitType`
13
+ // values. The two namespaces are not 1:1:
14
+ //
15
+ // - Some tools have no corresponding unit type (e.g. `gsd_doctor`,
16
+ // `gsd_plan_task`) and intentionally omit `unitType`.
17
+ // - Some unit types share a tool — e.g. `execute-task`, `execute-task-simple`,
18
+ // and `reactive-execute` all invoke `gsd_execute`. The current shape
19
+ // allows only one `unitType` per entry, so those units fall through to
20
+ // `getVerdictByUnitType() === null` (→ `backgroundable: false`) even
21
+ // though `gsd_execute` itself is GOOD. This is the intended default-deny
22
+ // posture until a future PR wires actual background dispatch and
23
+ // decides whether each unit-level orchestration is safe — the unit
24
+ // wraps a prompt, harness setup, and post-processing on top of the
25
+ // tool, and the tool's safety doesn't transfer automatically.
26
+ //
27
+ // Auto-dispatch produces 20 distinct unit types; only 5 are explicitly
28
+ // classified here. The other 15 default-deny:
29
+ // complete-milestone, complete-slice, discuss-milestone, discuss-project,
30
+ // discuss-requirements, execute-task, execute-task-simple, gate-evaluate,
31
+ // reactive-execute, refine-slice, research-decision, research-milestone,
32
+ // research-project, research-slice, rewrite-docs, run-uat
33
+ //
34
+ // Adding a `unitType` mapping (or a future `unitTypes: string[]`) to an
35
+ // existing entry is the place to lift any of these out of default-deny
36
+ // when the analysis has been done.
37
+
38
+ export type BackgroundabilityVerdict = "good" | "risky" | "no";
39
+
40
+ export interface DelegationPolicyEntry {
41
+ /** Canonical MCP tool name (the verb_object form, e.g. `gsd_plan_slice`). */
42
+ toolName: string;
43
+ /** Workflow unit type from auto-dispatch.ts, when one exists. */
44
+ unitType?: string;
45
+ verdict: BackgroundabilityVerdict;
46
+ /** One-line justification grounded in the evaluation findings. */
47
+ rationale: string;
48
+ /**
49
+ * Constraints the caller MUST satisfy when dispatching this unit in the
50
+ * background. Only populated for `good` and conditional `risky` entries.
51
+ */
52
+ constraints?: string[];
53
+ }
54
+
55
+ const POLICY: Record<string, DelegationPolicyEntry> = {
56
+ gsd_plan_slice: {
57
+ toolName: "gsd_plan_slice",
58
+ unitType: "plan-slice",
59
+ verdict: "good",
60
+ rationale:
61
+ "Self-contained, no user prompts, atomic DB tx; existing slice-parallel-orchestrator pattern transfers cleanly.",
62
+ constraints: [
63
+ "Lock the slice from further user discussion once dispatched (context is frozen at dispatch time).",
64
+ "Foreground must not derive state for that slice while the transaction is in flight.",
65
+ "Foreground must await background completion before any tool reads the planned tasks/gates.",
66
+ ],
67
+ },
68
+ gsd_execute: {
69
+ toolName: "gsd_execute",
70
+ // No `unitType` set on purpose — the underlying tool is safe, but the
71
+ // unit-level orchestrations that invoke it (`execute-task`,
72
+ // `execute-task-simple`, `reactive-execute`) wrap additional prompt and
73
+ // harness work whose safety is a separate analysis. Default-deny those
74
+ // units until that analysis is recorded; adding `unitType` here would
75
+ // promote them silently.
76
+ verdict: "good",
77
+ rationale:
78
+ "No DB writes; UUID-isolated stdout/stderr/meta files; existing reactive-execute parallel-subagent precedent.",
79
+ },
80
+ gsd_validate_milestone: {
81
+ toolName: "gsd_validate_milestone",
82
+ unitType: "validate-milestone",
83
+ verdict: "good",
84
+ rationale:
85
+ "Verdict pre-computed by parallel reviewers; atomic DB tx plus isolated VALIDATION.md write; no user interaction.",
86
+ },
87
+ gsd_reassess_roadmap: {
88
+ toolName: "gsd_reassess_roadmap",
89
+ unitType: "reassess-roadmap",
90
+ verdict: "good",
91
+ rationale:
92
+ "Narrower mutation scope than plan_milestone; structural guards prevent modification of completed slices.",
93
+ },
94
+ gsd_doctor: {
95
+ toolName: "gsd_doctor",
96
+ verdict: "risky",
97
+ rationale:
98
+ "Diagnostic-only mode (fix=false) is safe to background; fix=true writes STATE.md/ROADMAP.md without session-lock coordination and can race the foreground flow.",
99
+ constraints: [
100
+ "Background only with fix=false (diagnostic-only).",
101
+ "Apply fixes synchronously, only when no foreground unit is dispatched.",
102
+ ],
103
+ },
104
+ gsd_plan_milestone: {
105
+ toolName: "gsd_plan_milestone",
106
+ unitType: "plan-milestone",
107
+ verdict: "risky",
108
+ rationale:
109
+ "Inputs require CONTEXT.md from discuss-milestone, so initial questioning is already done by the time it can start; TOCTOU guards and projection coherence make concurrency unsafe.",
110
+ },
111
+ gsd_replan_slice: {
112
+ toolName: "gsd_replan_slice",
113
+ unitType: "replan-slice",
114
+ verdict: "risky",
115
+ rationale:
116
+ "Blocks the replanning→executing state transition on a gate that waits for S##-REPLAN.md; background failure leaves the flow stuck.",
117
+ },
118
+ gsd_plan_task: {
119
+ toolName: "gsd_plan_task",
120
+ verdict: "no",
121
+ rationale:
122
+ "plan-slice prompt explicitly forbids calling gsd_plan_task separately; per-task granularity multiplies manifest writes and projection re-renders with no payoff.",
123
+ },
124
+ };
125
+
126
+ // Alias map keyed on the secondary name; resolves to the canonical entry above.
127
+ // Sourced from packages/mcp-server/src/workflow-tools.ts alias registrations
128
+ // (gsd_milestone_validate, gsd_roadmap_reassess, gsd_slice_replan, gsd_task_plan).
129
+ const ALIASES: Record<string, string> = {
130
+ gsd_milestone_validate: "gsd_validate_milestone",
131
+ gsd_roadmap_reassess: "gsd_reassess_roadmap",
132
+ gsd_slice_replan: "gsd_replan_slice",
133
+ gsd_task_plan: "gsd_plan_task",
134
+ };
135
+
136
+ function resolveCanonical(name: string): string {
137
+ return ALIASES[name] ?? name;
138
+ }
139
+
140
+ export function getDelegationVerdict(toolName: string): DelegationPolicyEntry | null {
141
+ return POLICY[resolveCanonical(toolName)] ?? null;
142
+ }
143
+
144
+ export function isBackgroundable(toolName: string): boolean {
145
+ const entry = getDelegationVerdict(toolName);
146
+ return entry?.verdict === "good";
147
+ }
148
+
149
+ export function listBackgroundableTools(): string[] {
150
+ return Object.values(POLICY)
151
+ .filter((entry) => entry.verdict === "good")
152
+ .map((entry) => entry.toolName)
153
+ .sort();
154
+ }
155
+
156
+ export function getVerdictByUnitType(unitType: string): DelegationPolicyEntry | null {
157
+ for (const entry of Object.values(POLICY)) {
158
+ if (entry.unitType === unitType) return entry;
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Minimal shape of a dispatch action that the annotator needs to operate on.
165
+ * Matches the `dispatch` and non-dispatch variants of auto-dispatch.ts'
166
+ * DispatchAction without depending on it (so this module stays free of
167
+ * workspace-package transitive imports).
168
+ */
169
+ export type AnnotatableDispatchAction =
170
+ | { action: "dispatch"; unitType: string; backgroundable?: boolean; [k: string]: unknown }
171
+ | { action: "stop"; [k: string]: unknown }
172
+ | { action: "skip"; [k: string]: unknown };
173
+
174
+ /**
175
+ * Annotates a dispatch action in place with `backgroundable: true` when its
176
+ * unitType has a `good` verdict in the policy. Stop/skip actions pass through
177
+ * unchanged. Default-deny: unknown unit types resolve to `false`.
178
+ *
179
+ * **Mutation contract.** The `backgroundable` field is written directly onto
180
+ * the passed action object. This is intentional — every dispatch path in
181
+ * `auto-dispatch.ts` constructs a fresh action object per `where(ctx)` /
182
+ * `evaluateDispatch(ctx)` invocation, so in-place mutation cannot leak across
183
+ * dispatch cycles. Future dispatch rules MUST follow that convention: never
184
+ * cache or share `DispatchAction` objects across calls. If you need to cache,
185
+ * either freeze the cached object (`Object.freeze`) and clone on read, or
186
+ * stop calling `annotateBackgroundable` on the shared instance. The annotator
187
+ * always recomputes from the policy on every call (no internal cache), so
188
+ * repeated invocations on the same object will overwrite stale values
189
+ * deterministically — see the `annotateBackgroundable recomputes on each call`
190
+ * test for the contract pin.
191
+ */
192
+ export function annotateBackgroundable<T extends AnnotatableDispatchAction>(action: T): T {
193
+ if (action.action !== "dispatch") return action;
194
+ const verdict = getVerdictByUnitType(action.unitType);
195
+ action.backgroundable = verdict?.verdict === "good";
196
+ return action;
197
+ }
@@ -54,21 +54,16 @@ export function getPriorSliceCompletionBlocker(
54
54
  // completion, which is wrong when the SUMMARY is a failure-path report
55
55
  // (verification FAILED, blocker placeholder, etc.). Resolve as follows:
56
56
  // 1. When DB is available and status is closed → skip (authoritative).
57
- // 2. When SUMMARY exists but looks like a failure/blocker report →
58
- // do not short-circuit; fall through to the slice-level check so
59
- // the guard can still block dependents of an active milestone.
60
- // 3. Otherwise (SUMMARY without failure markers) → skip. Preserves
61
- // the #1716 contract where a completed milestone with unchecked
62
- // remediation slices is still treated as done.
63
- const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
57
+ // 2. When DB is unavailable, legacy SUMMARY.md fallback may skip.
58
+ // DB-backed projects must not treat SUMMARY.md as authoritative.
64
59
  if (isDbAvailable()) {
65
60
  const milestoneRow = getMilestone(mid);
66
61
  if (milestoneRow && isClosedStatus(milestoneRow.status)) continue;
67
- }
68
- if (summaryPath) {
62
+ } else {
63
+ const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
69
64
  let summaryContent: string | null = null;
70
- try { summaryContent = readFileSync(summaryPath, "utf-8"); } catch { /* ignore */ }
71
- if (!summaryContent || classifyMilestoneSummaryContent(summaryContent) !== "failure") {
65
+ try { summaryContent = summaryPath ? readFileSync(summaryPath, "utf-8") : null; } catch { /* ignore */ }
66
+ if (summaryContent && classifyMilestoneSummaryContent(summaryContent) !== "failure") {
72
67
  continue;
73
68
  }
74
69
  }
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
 
4
4
  import type { DoctorIssue } from "./doctor-types.js";
5
5
  import { isDbAvailable, _getAdapter } from "./gsd-db.js";
6
- import { resolveMilestoneFile } from "./paths.js";
6
+ import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
7
7
  import { deriveState } from "./state.js";
8
8
  import { readEvents } from "./workflow-events.js";
9
9
  import { renderAllProjections } from "./workflow-projections.js";
@@ -13,7 +13,7 @@ export async function checkEngineHealth(
13
13
  issues: DoctorIssue[],
14
14
  fixesApplied: string[],
15
15
  ): Promise<void> {
16
- const dbPath = join(basePath, ".gsd", "gsd.db");
16
+ const dbPath = resolveGsdPathContract(basePath).projectDb;
17
17
 
18
18
  if (!isDbAvailable() && existsSync(dbPath)) {
19
19
  issues.push({