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
@@ -15,7 +15,7 @@ import { appendOverride, appendKnowledge } from "./files.js";
15
15
  import { formatDoctorIssuesForPrompt, formatDoctorReport, formatDoctorReportJson, runGSDDoctor, selectDoctorScope, filterDoctorIssues, } from "./doctor.js";
16
16
  import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
17
17
  import { getAutoWorktreePath } from "./auto-worktree.js";
18
- import { projectRoot } from "./commands/context.js";
18
+ import { currentDirectoryRoot, projectRoot } from "./commands/context.js";
19
19
  import { loadPrompt } from "./prompt-loader.js";
20
20
  const UPDATE_REGISTRY_URL = "https://registry.npmjs.org/gsd-pi/latest";
21
21
  const UPDATE_FETCH_TIMEOUT_MS = 5000;
@@ -169,7 +169,7 @@ export async function handleCapture(args, ctx) {
169
169
  ctx.ui.notify('Usage: /gsd capture "your thought here"', "warning");
170
170
  return;
171
171
  }
172
- const basePath = projectRoot();
172
+ const basePath = currentDirectoryRoot();
173
173
  // Ensure .gsd/ exists — capture should work even without a milestone
174
174
  const gsdDir = gsdRoot(basePath);
175
175
  if (!existsSync(gsdDir)) {
@@ -220,7 +220,7 @@ export async function handleTriage(ctx, pi, basePath) {
220
220
  }, { triggerTurn: true });
221
221
  }
222
222
  export async function handleSteer(change, ctx, pi) {
223
- const basePath = projectRoot();
223
+ const basePath = currentDirectoryRoot();
224
224
  const state = await deriveState(basePath);
225
225
  const mid = state.activeMilestone?.id ?? "none";
226
226
  const sid = state.activeSlice?.id ?? "none";
@@ -284,7 +284,7 @@ export async function handleKnowledge(args, ctx) {
284
284
  return;
285
285
  }
286
286
  const type = typeArg;
287
- const basePath = projectRoot();
287
+ const basePath = currentDirectoryRoot();
288
288
  const state = await deriveState(basePath);
289
289
  const scope = state.activeMilestone?.id
290
290
  ? `${state.activeMilestone.id}${state.activeSlice ? `/${state.activeSlice.id}` : ""}`
@@ -310,7 +310,7 @@ Examples:
310
310
  return;
311
311
  }
312
312
  const [hookName, unitType, unitId] = parts;
313
- const basePath = projectRoot();
313
+ const basePath = currentDirectoryRoot();
314
314
  // Import the hook trigger function
315
315
  const { triggerHookManually, formatHookStatus, getHookStatus } = await import("./post-unit-hooks.js");
316
316
  const { dispatchHookUnit } = await import("./auto.js");
@@ -13,7 +13,7 @@ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "nod
13
13
  import { join } from "node:path";
14
14
  import { gsdRoot } from "./paths.js";
15
15
  import { loadJsonFileOrNull } from "./json-persistence.js";
16
- import { projectRoot } from "./commands/context.js";
16
+ import { currentDirectoryRoot } from "./commands/context.js";
17
17
  // ─── Helpers ────────────────────────────────────────────────────────────────
18
18
  function activityDir(basePath) {
19
19
  return join(gsdRoot(basePath), "activity");
@@ -227,7 +227,7 @@ function summarizeDebugLog(filePath) {
227
227
  }
228
228
  // ─── Main Handler ───────────────────────────────────────────────────────────
229
229
  export async function handleLogs(args, ctx) {
230
- const basePath = projectRoot();
230
+ const basePath = currentDirectoryRoot();
231
231
  const parts = args.trim().split(/\s+/).filter(Boolean);
232
232
  const subCmd = parts[0] ?? "";
233
233
  // /gsd logs clear
@@ -16,7 +16,7 @@
16
16
  import { existsSync, mkdirSync } from "node:fs";
17
17
  import { join, relative } from "node:path";
18
18
  import { loadPrompt } from "./prompt-loader.js";
19
- import { projectRoot } from "./commands/context.js";
19
+ import { currentDirectoryRoot } from "./commands/context.js";
20
20
  // ─── Constants ────────────────────────────────────────────────────────────────
21
21
  export const DEFAULT_FOCUS = "tech+arch";
22
22
  export const VALID_FOCUS_AREAS = ["tech", "arch", "quality", "concerns", "tech+arch"];
@@ -67,7 +67,7 @@ export function checkExistingDocuments(paths) {
67
67
  }
68
68
  // ─── Command handler ──────────────────────────────────────────────────────────
69
69
  export async function handleScan(args, ctx, pi) {
70
- const basePath = projectRoot();
70
+ const basePath = currentDirectoryRoot();
71
71
  const { focus } = parseScanArgs(args);
72
72
  const outputDir = join(basePath, ".gsd", "codebase");
73
73
  const outputPaths = buildScanOutputPaths(focus, basePath);
@@ -13,7 +13,7 @@ import { getLedger, getProjectTotals, aggregateByModel, formatCost, formatTokenC
13
13
  import { nativeGetCurrentBranch, nativeDetectMainBranch } from "./native-git-bridge.js";
14
14
  import { formatDuration } from "../shared/format-utils.js";
15
15
  import { parseEvalReviewFrontmatter } from "./eval-review-schema.js";
16
- import { projectRoot } from "./commands/context.js";
16
+ import { currentDirectoryRoot } from "./commands/context.js";
17
17
  function git(basePath, args) {
18
18
  return execFileSync("git", args, { cwd: basePath, encoding: "utf-8" }).trim();
19
19
  }
@@ -176,7 +176,7 @@ function generatePRContent(basePath, milestoneId, milestoneTitle) {
176
176
  return { title, body: sections.join("\n") };
177
177
  }
178
178
  export async function handleShip(args, ctx, _pi) {
179
- const basePath = projectRoot();
179
+ const basePath = currentDirectoryRoot();
180
180
  const dryRun = args.includes("--dry-run");
181
181
  const draft = args.includes("--draft");
182
182
  const force = args.includes("--force");
@@ -13,7 +13,7 @@ import { createGitService, runGit } from "./git-service.js";
13
13
  import { isAutoActive, isAutoPaused } from "./auto.js";
14
14
  import { getErrorMessage } from "./error-utils.js";
15
15
  import { resolvePlugin } from "./workflow-plugins.js";
16
- import { projectRoot } from "./commands/context.js";
16
+ import { currentDirectoryRoot } from "./commands/context.js";
17
17
  // ─── Helpers ─────────────────────────────────────────────────────────────────
18
18
  /**
19
19
  * Generate a URL-friendly slug from text.
@@ -143,7 +143,7 @@ export async function handleStart(args, ctx, pi) {
143
143
  // ─── Resume detection ───────────────────────────────────────────────────
144
144
  // /gsd start --resume or /gsd start resume → resume in-progress workflow
145
145
  if (trimmed === "--resume" || trimmed === "resume") {
146
- const basePath = projectRoot();
146
+ const basePath = currentDirectoryRoot();
147
147
  const inProgress = findInProgressWorkflows(basePath);
148
148
  if (inProgress.length === 0) {
149
149
  ctx.ui.notify("No in-progress workflows found.", "info");
@@ -182,7 +182,7 @@ export async function handleStart(args, ctx, pi) {
182
182
  }
183
183
  // Show in-progress workflows when /gsd start is called with no args
184
184
  if (!trimmed) {
185
- const basePath = projectRoot();
185
+ const basePath = currentDirectoryRoot();
186
186
  const inProgress = findInProgressWorkflows(basePath);
187
187
  if (inProgress.length > 0) {
188
188
  const wf = inProgress[0];
@@ -257,7 +257,7 @@ export async function handleStart(args, ctx, pi) {
257
257
  // ─── Resolved template ───────────────────────────────────────────────────
258
258
  const templateId = match.id;
259
259
  const template = match.template;
260
- const basePath = projectRoot();
260
+ const basePath = currentDirectoryRoot();
261
261
  const date = new Date().toISOString().split("T")[0];
262
262
  // Load the workflow template content — prefer a project/global plugin
263
263
  // override if one exists (same name, .md format).
@@ -437,7 +437,7 @@ export function dispatchMarkdownPhasePlugin(plugin, description, ctx, pi) {
437
437
  return;
438
438
  }
439
439
  const templateId = plugin.name;
440
- const basePath = projectRoot();
440
+ const basePath = currentDirectoryRoot();
441
441
  const date = new Date().toISOString().split("T")[0];
442
442
  let workflowContent;
443
443
  try {
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // Critical invariant: generated markdown must round-trip through
9
9
  // parseDecisionsTable() and parseRequirementsSections() with field fidelity.
10
- import { resolve } from 'node:path';
10
+ import { isAbsolute, relative, resolve } from 'node:path';
11
11
  import { readFileSync, existsSync, statSync } from 'node:fs';
12
12
  import { resolveGsdRootFile } from './paths.js';
13
13
  import { saveFile } from './files.js';
@@ -16,6 +16,7 @@ import { logWarning, logError } from './workflow-logger.js';
16
16
  import { invalidateStateCache } from './state.js';
17
17
  import { clearPathCache } from './paths.js';
18
18
  import { clearParseCache } from './files.js';
19
+ import { createWorkspace, scopeMilestone } from './workspace.js';
19
20
  // ─── Freeform Detection ───────────────────────────────────────────────────
20
21
  /**
21
22
  * Detect whether a DECISIONS.md file is in canonical table format
@@ -273,22 +274,6 @@ export async function saveRequirementToDb(fields, basePath) {
273
274
  ORDER BY id
274
275
  LIMIT 1`)
275
276
  .get({ ':description': fields.description });
276
- const previousRow = existingRow
277
- ? {
278
- id: existingRow['id'],
279
- class: existingRow['class'],
280
- status: existingRow['status'],
281
- description: existingRow['description'],
282
- why: existingRow['why'],
283
- source: existingRow['source'],
284
- primary_owner: existingRow['primary_owner'],
285
- supporting_slices: existingRow['supporting_slices'],
286
- validation: existingRow['validation'],
287
- notes: existingRow['notes'],
288
- full_content: existingRow['full_content'],
289
- superseded_by: existingRow['superseded_by'] ?? null,
290
- }
291
- : null;
292
277
  const row = adapter
293
278
  .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
294
279
  .get();
@@ -313,9 +298,9 @@ export async function saveRequirementToDb(fields, basePath) {
313
298
  superseded_by: existingRow?.['superseded_by'] ?? null,
314
299
  };
315
300
  db.upsertRequirement(requirement);
316
- return { id: nextId, isNew: !existingRow, previousRow };
301
+ return { id: nextId };
317
302
  });
318
- const { id, isNew, previousRow } = txResult;
303
+ const { id } = txResult;
319
304
  // Fetch all requirements for full file regeneration
320
305
  const adapter = db._getAdapter();
321
306
  let allRequirements = [];
@@ -343,19 +328,7 @@ export async function saveRequirementToDb(fields, basePath) {
343
328
  await saveFile(filePath, md);
344
329
  }
345
330
  catch (diskErr) {
346
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveRequirementToDb', error: String(diskErr.message) });
347
- try {
348
- if (isNew) {
349
- db.deleteRequirementById(id);
350
- }
351
- else if (previousRow) {
352
- db.upsertRequirement(previousRow);
353
- }
354
- }
355
- catch (rollbackErr) {
356
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveRequirementToDb', id, error: String(rollbackErr.message) });
357
- }
358
- throw diskErr;
331
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement remains committed', { fn: 'saveRequirementToDb', id, error: String(diskErr.message) });
359
332
  }
360
333
  invalidateStateCache();
361
334
  clearPathCache();
@@ -463,14 +436,7 @@ export async function saveDecisionToDb(fields, basePath) {
463
436
  await saveFile(filePath, md);
464
437
  }
465
438
  catch (diskErr) {
466
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveDecisionToDb', error: String(diskErr.message) });
467
- try {
468
- db.deleteDecisionById(id);
469
- }
470
- catch (rollbackErr) {
471
- logError('manifest', 'SPLIT BRAIN: disk write failed AND DB rollback failed — DB has orphaned row', { fn: 'saveDecisionToDb', id, error: String(rollbackErr.message) });
472
- }
473
- throw diskErr;
439
+ logWarning('projection', 'DECISIONS.md projection write failed; DB decision remains committed', { fn: 'saveDecisionToDb', id, error: String(diskErr.message) });
474
440
  }
475
441
  // #2661: When a decision defers a slice, update the slice status in the DB
476
442
  // so the dispatcher skips it. Without this, STATE.md and DECISIONS.md are
@@ -584,34 +550,7 @@ export function extractDeferredSliceRef(fields) {
584
550
  export async function updateRequirementInDb(id, updates, basePath) {
585
551
  try {
586
552
  const db = await import('./gsd-db.js');
587
- let existing = db.getRequirementById(id);
588
- // If requirement doesn't exist in DB, seed the entire requirements table
589
- // from REQUIREMENTS.md first (#3346). This handles the standard workflow
590
- // where requirements are authored in markdown during discussion but never
591
- // imported into the database — making gsd_requirement_update always fail
592
- // with "not_found" at milestone completion.
593
- if (!existing) {
594
- const reqFilePath = resolveGsdRootFile(basePath, 'REQUIREMENTS');
595
- try {
596
- const content = readFileSync(reqFilePath, 'utf-8');
597
- const { parseRequirementsSections } = await import('./md-importer.js');
598
- const parsed = parseRequirementsSections(content);
599
- if (parsed.length > 0) {
600
- logWarning('manifest', `Seeding ${parsed.length} requirements from REQUIREMENTS.md into DB (first update triggers import)`, { fn: 'updateRequirementInDb' });
601
- for (const req of parsed) {
602
- // Only seed if not already in DB (avoid overwriting concurrent inserts)
603
- if (!db.getRequirementById(req.id)) {
604
- db.upsertRequirement(req);
605
- }
606
- }
607
- // Re-check after seeding
608
- existing = db.getRequirementById(id);
609
- }
610
- }
611
- catch {
612
- // REQUIREMENTS.md missing or unparseable — fall through to skeleton
613
- }
614
- }
553
+ const existing = db.getRequirementById(id);
615
554
  const base = existing ?? {
616
555
  id,
617
556
  class: '',
@@ -662,11 +601,7 @@ export async function updateRequirementInDb(id, updates, basePath) {
662
601
  await saveFile(filePath, md);
663
602
  }
664
603
  catch (diskErr) {
665
- logError('manifest', 'disk write failed, reverting DB row', { fn: 'updateRequirementInDb', error: String(diskErr.message) });
666
- if (existing) {
667
- db.upsertRequirement(existing);
668
- }
669
- throw diskErr;
604
+ logWarning('projection', 'REQUIREMENTS.md projection write failed; DB requirement update remains committed', { fn: 'updateRequirementInDb', id, error: String(diskErr.message) });
670
605
  }
671
606
  // Invalidate file-read caches so deriveState() sees the updated markdown.
672
607
  // Do NOT clear the artifacts table — we just wrote to it intentionally.
@@ -680,41 +615,102 @@ export async function updateRequirementInDb(id, updates, basePath) {
680
615
  }
681
616
  }
682
617
  /**
683
- * Save an artifact to DB and write the corresponding markdown file to disk.
618
+ * Save a root-level artifact (no milestone) to DB and write to disk,
619
+ * routing path construction through workspace.contract.projectGsd directly.
620
+ * Use this instead of saveArtifactToDbByScope when milestone_id is absent.
621
+ */
622
+ export async function saveArtifactToDbForWorkspace(workspace, opts) {
623
+ try {
624
+ const db = await import('./gsd-db.js');
625
+ const gsdDir = workspace.contract.projectGsd;
626
+ const fullPath = resolve(gsdDir, opts.path);
627
+ const rel0 = relative(gsdDir, fullPath);
628
+ if (rel0.startsWith('..') || isAbsolute(rel0)) {
629
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbForWorkspace: path escapes .gsd/ directory: ${opts.path}`);
630
+ }
631
+ let contentToPersist = opts.content;
632
+ if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
633
+ const activeRequirements = db.getActiveRequirements();
634
+ if (activeRequirements.length === 0) {
635
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbForWorkspace: REQUIREMENTS final save requires active DB-backed requirements');
636
+ }
637
+ contentToPersist = generateRequirementsMd(activeRequirements);
638
+ }
639
+ let skipDiskWrite = false;
640
+ if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
641
+ const existingSize = statSync(fullPath).size;
642
+ const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
643
+ if (existingSize > 0 && newSize < existingSize * 0.5) {
644
+ 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 });
645
+ skipDiskWrite = true;
646
+ }
647
+ }
648
+ db.insertArtifact({
649
+ path: opts.path,
650
+ artifact_type: opts.artifact_type,
651
+ milestone_id: null,
652
+ slice_id: null,
653
+ task_id: null,
654
+ full_content: contentToPersist,
655
+ });
656
+ if (!skipDiskWrite) {
657
+ try {
658
+ await saveFile(fullPath, contentToPersist);
659
+ }
660
+ catch (diskErr) {
661
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbForWorkspace', path: opts.path, error: String(diskErr.message) });
662
+ }
663
+ }
664
+ invalidateStateCache();
665
+ clearPathCache();
666
+ clearParseCache();
667
+ }
668
+ catch (err) {
669
+ logError('manifest', 'saveArtifactToDbForWorkspace failed', { fn: 'saveArtifactToDbForWorkspace', error: String(err.message) });
670
+ throw err;
671
+ }
672
+ }
673
+ /**
674
+ * Save an artifact to DB and write the corresponding markdown file to disk,
675
+ * routing all path construction through the workspace contract.
676
+ *
684
677
  * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
685
- * The full file path is computed as basePath + '.gsd/' + path.
678
+ * The full file path is computed as scope.workspace.contract.projectGsd + '/' + path.
686
679
  */
687
- export async function saveArtifactToDb(opts, basePath) {
680
+ export async function saveArtifactToDbByScope(scope, opts) {
681
+ // Guard: an empty milestoneId produces malformed paths (milestoneDir = join(gsd, "milestones", "")).
682
+ // Callers that have no milestone should use saveArtifactToDbForWorkspace instead.
683
+ if (!scope.milestoneId) {
684
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: milestoneId is empty — use saveArtifactToDbForWorkspace for root artifacts`);
685
+ }
688
686
  try {
689
687
  const db = await import('./gsd-db.js');
688
+ // Use contract.projectGsd as the canonical .gsd directory — never a hand-rolled basePath join.
689
+ const gsdDir = scope.workspace.contract.projectGsd;
690
+ const fullPath = resolve(gsdDir, opts.path);
690
691
  // Guard against path traversal before any reads/writes
691
- const gsdDir = resolve(basePath, '.gsd');
692
- const fullPath = resolve(basePath, '.gsd', opts.path);
693
- if (!fullPath.startsWith(gsdDir)) {
694
- throw new GSDError(GSD_IO_ERROR, `saveArtifactToDb: path escapes .gsd/ directory: ${opts.path}`);
692
+ const rel1 = relative(gsdDir, fullPath);
693
+ if (rel1.startsWith('..') || isAbsolute(rel1)) {
694
+ throw new GSDError(GSD_IO_ERROR, `saveArtifactToDbByScope: path escapes .gsd/ directory: ${opts.path}`);
695
695
  }
696
696
  let contentToPersist = opts.content;
697
697
  if (opts.artifact_type === 'REQUIREMENTS' && opts.path === 'REQUIREMENTS.md') {
698
698
  const activeRequirements = db.getActiveRequirements();
699
699
  if (activeRequirements.length === 0) {
700
- throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDb: REQUIREMENTS final save requires active DB-backed requirements');
700
+ throw new GSDError(GSD_STALE_STATE, 'saveArtifactToDbByScope: REQUIREMENTS final save requires active DB-backed requirements');
701
701
  }
702
702
  contentToPersist = generateRequirementsMd(activeRequirements);
703
703
  }
704
- // Shrinkage guard: if the file already exists and the new content is
705
- // significantly smaller (<50%), preserve the richer file on disk and
706
- // store its content in the DB instead of the abbreviated version. Root
707
- // canonical artifacts are exempt because their content is rendered from
708
- // canonical DB state, and cleanup/consolidation is often intentionally much
709
- // smaller than a malformed accumulated file.
710
- let dbContent = contentToPersist;
704
+ // Shrinkage guard: if the projection file already exists and the new
705
+ // content is significantly smaller (<50%), preserve the richer file on
706
+ // disk, but keep the DB row authoritative with the caller-provided content.
707
+ // Root canonical artifacts are exempt (rendered from canonical DB state).
711
708
  let skipDiskWrite = false;
712
709
  if (!isRootCanonicalArtifact(opts) && existsSync(fullPath)) {
713
710
  const existingSize = statSync(fullPath).size;
714
711
  const newSize = Buffer.byteLength(contentToPersist, 'utf-8');
715
712
  if (existingSize > 0 && newSize < existingSize * 0.5) {
716
- logWarning('manifest', `new content (${newSize}B) is <50% of existing file (${existingSize}B), preserving disk file`, { fn: 'saveArtifactToDb', path: opts.path });
717
- dbContent = readFileSync(fullPath, 'utf-8');
713
+ 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 });
718
714
  skipDiskWrite = true;
719
715
  }
720
716
  }
@@ -724,7 +720,7 @@ export async function saveArtifactToDb(opts, basePath) {
724
720
  milestone_id: opts.milestone_id ?? null,
725
721
  slice_id: opts.slice_id ?? null,
726
722
  task_id: opts.task_id ?? null,
727
- full_content: dbContent,
723
+ full_content: contentToPersist,
728
724
  });
729
725
  // Write the file to disk (only if we're not preserving a richer existing file)
730
726
  if (!skipDiskWrite) {
@@ -732,9 +728,7 @@ export async function saveArtifactToDb(opts, basePath) {
732
728
  await saveFile(fullPath, contentToPersist);
733
729
  }
734
730
  catch (diskErr) {
735
- logError('manifest', 'disk write failed, rolling back DB row', { fn: 'saveArtifactToDb', error: String(diskErr.message) });
736
- db.deleteArtifactByPath(opts.path);
737
- throw diskErr;
731
+ logWarning('projection', 'artifact projection write failed; DB artifact remains committed', { fn: 'saveArtifactToDbByScope', path: opts.path, error: String(diskErr.message) });
738
732
  }
739
733
  }
740
734
  // Invalidate file-read caches so deriveState() sees the updated markdown.
@@ -744,7 +738,24 @@ export async function saveArtifactToDb(opts, basePath) {
744
738
  clearParseCache();
745
739
  }
746
740
  catch (err) {
747
- logError('manifest', 'saveArtifactToDb failed', { fn: 'saveArtifactToDb', error: String(err.message) });
741
+ logError('manifest', 'saveArtifactToDbByScope failed', { fn: 'saveArtifactToDbByScope', error: String(err.message) });
748
742
  throw err;
749
743
  }
750
744
  }
745
+ /**
746
+ * Save an artifact to DB and write the corresponding markdown file to disk.
747
+ * The path is relative to .gsd/ (e.g. "milestones/M001/slices/S06/tasks/T01-SUMMARY.md").
748
+ * The full file path is computed as basePath + '.gsd/' + path.
749
+ *
750
+ * @deprecated Use saveArtifactToDbByScope instead, which routes through the
751
+ * workspace contract for canonical path resolution.
752
+ * TODO(C-future): remove this legacy wrapper once all callers are migrated.
753
+ */
754
+ export async function saveArtifactToDb(opts, basePath) {
755
+ const workspace = createWorkspace(basePath);
756
+ const milestoneId = opts.milestone_id;
757
+ if (milestoneId) {
758
+ return saveArtifactToDbByScope(scopeMilestone(workspace, milestoneId), opts);
759
+ }
760
+ return saveArtifactToDbForWorkspace(workspace, opts);
761
+ }
@@ -0,0 +1,155 @@
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
+ const POLICY = {
38
+ gsd_plan_slice: {
39
+ toolName: "gsd_plan_slice",
40
+ unitType: "plan-slice",
41
+ verdict: "good",
42
+ rationale: "Self-contained, no user prompts, atomic DB tx; existing slice-parallel-orchestrator pattern transfers cleanly.",
43
+ constraints: [
44
+ "Lock the slice from further user discussion once dispatched (context is frozen at dispatch time).",
45
+ "Foreground must not derive state for that slice while the transaction is in flight.",
46
+ "Foreground must await background completion before any tool reads the planned tasks/gates.",
47
+ ],
48
+ },
49
+ gsd_execute: {
50
+ toolName: "gsd_execute",
51
+ // No `unitType` set on purpose — the underlying tool is safe, but the
52
+ // unit-level orchestrations that invoke it (`execute-task`,
53
+ // `execute-task-simple`, `reactive-execute`) wrap additional prompt and
54
+ // harness work whose safety is a separate analysis. Default-deny those
55
+ // units until that analysis is recorded; adding `unitType` here would
56
+ // promote them silently.
57
+ verdict: "good",
58
+ rationale: "No DB writes; UUID-isolated stdout/stderr/meta files; existing reactive-execute parallel-subagent precedent.",
59
+ },
60
+ gsd_validate_milestone: {
61
+ toolName: "gsd_validate_milestone",
62
+ unitType: "validate-milestone",
63
+ verdict: "good",
64
+ rationale: "Verdict pre-computed by parallel reviewers; atomic DB tx plus isolated VALIDATION.md write; no user interaction.",
65
+ },
66
+ gsd_reassess_roadmap: {
67
+ toolName: "gsd_reassess_roadmap",
68
+ unitType: "reassess-roadmap",
69
+ verdict: "good",
70
+ rationale: "Narrower mutation scope than plan_milestone; structural guards prevent modification of completed slices.",
71
+ },
72
+ gsd_doctor: {
73
+ toolName: "gsd_doctor",
74
+ verdict: "risky",
75
+ rationale: "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.",
76
+ constraints: [
77
+ "Background only with fix=false (diagnostic-only).",
78
+ "Apply fixes synchronously, only when no foreground unit is dispatched.",
79
+ ],
80
+ },
81
+ gsd_plan_milestone: {
82
+ toolName: "gsd_plan_milestone",
83
+ unitType: "plan-milestone",
84
+ verdict: "risky",
85
+ rationale: "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.",
86
+ },
87
+ gsd_replan_slice: {
88
+ toolName: "gsd_replan_slice",
89
+ unitType: "replan-slice",
90
+ verdict: "risky",
91
+ rationale: "Blocks the replanning→executing state transition on a gate that waits for S##-REPLAN.md; background failure leaves the flow stuck.",
92
+ },
93
+ gsd_plan_task: {
94
+ toolName: "gsd_plan_task",
95
+ verdict: "no",
96
+ rationale: "plan-slice prompt explicitly forbids calling gsd_plan_task separately; per-task granularity multiplies manifest writes and projection re-renders with no payoff.",
97
+ },
98
+ };
99
+ // Alias map keyed on the secondary name; resolves to the canonical entry above.
100
+ // Sourced from packages/mcp-server/src/workflow-tools.ts alias registrations
101
+ // (gsd_milestone_validate, gsd_roadmap_reassess, gsd_slice_replan, gsd_task_plan).
102
+ const ALIASES = {
103
+ gsd_milestone_validate: "gsd_validate_milestone",
104
+ gsd_roadmap_reassess: "gsd_reassess_roadmap",
105
+ gsd_slice_replan: "gsd_replan_slice",
106
+ gsd_task_plan: "gsd_plan_task",
107
+ };
108
+ function resolveCanonical(name) {
109
+ return ALIASES[name] ?? name;
110
+ }
111
+ export function getDelegationVerdict(toolName) {
112
+ return POLICY[resolveCanonical(toolName)] ?? null;
113
+ }
114
+ export function isBackgroundable(toolName) {
115
+ const entry = getDelegationVerdict(toolName);
116
+ return entry?.verdict === "good";
117
+ }
118
+ export function listBackgroundableTools() {
119
+ return Object.values(POLICY)
120
+ .filter((entry) => entry.verdict === "good")
121
+ .map((entry) => entry.toolName)
122
+ .sort();
123
+ }
124
+ export function getVerdictByUnitType(unitType) {
125
+ for (const entry of Object.values(POLICY)) {
126
+ if (entry.unitType === unitType)
127
+ return entry;
128
+ }
129
+ return null;
130
+ }
131
+ /**
132
+ * Annotates a dispatch action in place with `backgroundable: true` when its
133
+ * unitType has a `good` verdict in the policy. Stop/skip actions pass through
134
+ * unchanged. Default-deny: unknown unit types resolve to `false`.
135
+ *
136
+ * **Mutation contract.** The `backgroundable` field is written directly onto
137
+ * the passed action object. This is intentional — every dispatch path in
138
+ * `auto-dispatch.ts` constructs a fresh action object per `where(ctx)` /
139
+ * `evaluateDispatch(ctx)` invocation, so in-place mutation cannot leak across
140
+ * dispatch cycles. Future dispatch rules MUST follow that convention: never
141
+ * cache or share `DispatchAction` objects across calls. If you need to cache,
142
+ * either freeze the cached object (`Object.freeze`) and clone on read, or
143
+ * stop calling `annotateBackgroundable` on the shared instance. The annotator
144
+ * always recomputes from the policy on every call (no internal cache), so
145
+ * repeated invocations on the same object will overwrite stale values
146
+ * deterministically — see the `annotateBackgroundable recomputes on each call`
147
+ * test for the contract pin.
148
+ */
149
+ export function annotateBackgroundable(action) {
150
+ if (action.action !== "dispatch")
151
+ return action;
152
+ const verdict = getVerdictByUnitType(action.unitType);
153
+ action.backgroundable = verdict?.verdict === "good";
154
+ return action;
155
+ }
@@ -45,25 +45,21 @@ export function getPriorSliceCompletionBlocker(base, _mainBranch, unitType, unit
45
45
  // completion, which is wrong when the SUMMARY is a failure-path report
46
46
  // (verification FAILED, blocker placeholder, etc.). Resolve as follows:
47
47
  // 1. When DB is available and status is closed → skip (authoritative).
48
- // 2. When SUMMARY exists but looks like a failure/blocker report →
49
- // do not short-circuit; fall through to the slice-level check so
50
- // the guard can still block dependents of an active milestone.
51
- // 3. Otherwise (SUMMARY without failure markers) → skip. Preserves
52
- // the #1716 contract where a completed milestone with unchecked
53
- // remediation slices is still treated as done.
54
- const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
48
+ // 2. When DB is unavailable, legacy SUMMARY.md fallback may skip.
49
+ // DB-backed projects must not treat SUMMARY.md as authoritative.
55
50
  if (isDbAvailable()) {
56
51
  const milestoneRow = getMilestone(mid);
57
52
  if (milestoneRow && isClosedStatus(milestoneRow.status))
58
53
  continue;
59
54
  }
60
- if (summaryPath) {
55
+ else {
56
+ const summaryPath = resolveMilestoneFile(base, mid, "SUMMARY");
61
57
  let summaryContent = null;
62
58
  try {
63
- summaryContent = readFileSync(summaryPath, "utf-8");
59
+ summaryContent = summaryPath ? readFileSync(summaryPath, "utf-8") : null;
64
60
  }
65
61
  catch { /* ignore */ }
66
- if (!summaryContent || classifyMilestoneSummaryContent(summaryContent) !== "failure") {
62
+ if (summaryContent && classifyMilestoneSummaryContent(summaryContent) !== "failure") {
67
63
  continue;
68
64
  }
69
65
  }
@@ -1,12 +1,12 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { isDbAvailable, _getAdapter } from "./gsd-db.js";
4
- import { resolveMilestoneFile } from "./paths.js";
4
+ import { resolveGsdPathContract, resolveMilestoneFile } from "./paths.js";
5
5
  import { deriveState } from "./state.js";
6
6
  import { readEvents } from "./workflow-events.js";
7
7
  import { renderAllProjections } from "./workflow-projections.js";
8
8
  export async function checkEngineHealth(basePath, issues, fixesApplied) {
9
- const dbPath = join(basePath, ".gsd", "gsd.db");
9
+ const dbPath = resolveGsdPathContract(basePath).projectDb;
10
10
  if (!isDbAvailable() && existsSync(dbPath)) {
11
11
  issues.push({
12
12
  severity: "warning",