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
@@ -0,0 +1,176 @@
1
+ // GSD-2 + Unit tests for the workspace registry that replaced the originalBase singleton
2
+
3
+ import { describe, test, beforeEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { execFileSync } from "node:child_process";
9
+
10
+ import {
11
+ getAutoWorktreeOriginalBase,
12
+ getActiveAutoWorktreeContext,
13
+ _resetAutoWorktreeOriginalBaseForTests,
14
+ createAutoWorktree,
15
+ enterAutoWorktree,
16
+ teardownAutoWorktree,
17
+ } from "../auto-worktree.ts";
18
+
19
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
20
+
21
+ // Safe: all inputs below are hardcoded test strings, not user input.
22
+ function git(subArgs: string[], cwd: string): void {
23
+ execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
24
+ }
25
+
26
+ function createTempRepo(t: { after: (fn: () => void) => void }): string {
27
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "awreg-test-")));
28
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
29
+ git(["init"], dir);
30
+ git(["config", "user.email", "test@test.com"], dir);
31
+ git(["config", "user.name", "Test"], dir);
32
+ writeFileSync(join(dir, "README.md"), "# test\n");
33
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
34
+ git(["add", "."], dir);
35
+ git(["commit", "-m", "init"], dir);
36
+ git(["branch", "-M", "main"], dir);
37
+ return dir;
38
+ }
39
+
40
+ // ─── Tests ───────────────────────────────────────────────────────────────────
41
+
42
+ describe("auto-worktree workspace registry", () => {
43
+ const savedCwd = process.cwd();
44
+
45
+ beforeEach(() => {
46
+ _resetAutoWorktreeOriginalBaseForTests();
47
+ process.chdir(savedCwd);
48
+ });
49
+
50
+ test("getAutoWorktreeOriginalBase() is null at baseline", () => {
51
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
52
+ });
53
+
54
+ test("getActiveAutoWorktreeContext() is null at baseline", () => {
55
+ assert.strictEqual(getActiveAutoWorktreeContext(), null);
56
+ });
57
+
58
+ test("_resetAutoWorktreeOriginalBaseForTests() clears the registry — idempotent", () => {
59
+ _resetAutoWorktreeOriginalBaseForTests();
60
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
61
+ _resetAutoWorktreeOriginalBaseForTests();
62
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
63
+ });
64
+
65
+ test("behavioral equivalence: createAutoWorktree populates registry; teardown clears it", (t) => {
66
+ const tempDir = createTempRepo(t);
67
+ const msDir = join(tempDir, ".gsd", "milestones", "M001");
68
+ mkdirSync(msDir, { recursive: true });
69
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
70
+ git(["add", "."], tempDir);
71
+ git(["commit", "-m", "add milestone"], tempDir);
72
+
73
+ // Before entering: registry must be empty
74
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
75
+ "originalBase is null before entering worktree");
76
+
77
+ createAutoWorktree(tempDir, "M001");
78
+
79
+ // After enter: getAutoWorktreeOriginalBase must equal tempDir
80
+ assert.strictEqual(
81
+ getAutoWorktreeOriginalBase(),
82
+ tempDir,
83
+ "getAutoWorktreeOriginalBase() returns projectRoot after createAutoWorktree",
84
+ );
85
+
86
+ // getActiveAutoWorktreeContext must return the correct shape
87
+ const ctx = getActiveAutoWorktreeContext();
88
+ assert.ok(ctx !== null, "context is non-null inside worktree");
89
+ assert.strictEqual(ctx.originalBase, tempDir, "context.originalBase matches tempDir");
90
+ assert.strictEqual(ctx.worktreeName, "M001", "context.worktreeName is M001");
91
+ assert.strictEqual(ctx.branch, "milestone/M001", "context.branch is milestone/M001");
92
+
93
+ // Teardown: registry must be cleared
94
+ teardownAutoWorktree(tempDir, "M001");
95
+
96
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
97
+ "getAutoWorktreeOriginalBase() is null after teardown");
98
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
99
+ "getActiveAutoWorktreeContext() is null after teardown");
100
+
101
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
102
+ });
103
+
104
+ test("behavioral equivalence: enterAutoWorktree also populates registry", (t) => {
105
+ const tempDir = createTempRepo(t);
106
+ const msDir = join(tempDir, ".gsd", "milestones", "M002");
107
+ mkdirSync(msDir, { recursive: true });
108
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M002 Context\n");
109
+ git(["add", "."], tempDir);
110
+ git(["commit", "-m", "add milestone"], tempDir);
111
+
112
+ createAutoWorktree(tempDir, "M002");
113
+
114
+ // Simulate leaving the worktree (crash/pause)
115
+ _resetAutoWorktreeOriginalBaseForTests();
116
+ process.chdir(tempDir);
117
+
118
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
119
+ "registry is empty after manual reset");
120
+
121
+ // Re-enter via enterAutoWorktree
122
+ enterAutoWorktree(tempDir, "M002");
123
+
124
+ assert.strictEqual(
125
+ getAutoWorktreeOriginalBase(),
126
+ tempDir,
127
+ "getAutoWorktreeOriginalBase() returns projectRoot after enterAutoWorktree",
128
+ );
129
+ const ctx = getActiveAutoWorktreeContext();
130
+ assert.ok(ctx !== null, "context is non-null after re-entry");
131
+ assert.strictEqual(ctx.originalBase, tempDir);
132
+ assert.strictEqual(ctx.worktreeName, "M002");
133
+ assert.strictEqual(ctx.branch, "milestone/M002");
134
+
135
+ teardownAutoWorktree(tempDir, "M002");
136
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
137
+ });
138
+
139
+ test("single-occupancy: entering a new workspace replaces the previous one", (t) => {
140
+ const dir1 = createTempRepo(t);
141
+ const dir2 = createTempRepo(t);
142
+
143
+ // Set up milestone in dir1
144
+ const ms1Dir = join(dir1, ".gsd", "milestones", "M010");
145
+ mkdirSync(ms1Dir, { recursive: true });
146
+ writeFileSync(join(ms1Dir, "CONTEXT.md"), "# M010\n");
147
+ git(["add", "."], dir1);
148
+ git(["commit", "-m", "add milestone"], dir1);
149
+
150
+ // Set up milestone in dir2
151
+ const ms2Dir = join(dir2, ".gsd", "milestones", "M020");
152
+ mkdirSync(ms2Dir, { recursive: true });
153
+ writeFileSync(join(ms2Dir, "CONTEXT.md"), "# M020\n");
154
+ git(["add", "."], dir2);
155
+ git(["commit", "-m", "add milestone"], dir2);
156
+
157
+ // Enter dir1/M010
158
+ createAutoWorktree(dir1, "M010");
159
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir1,
160
+ "registry holds dir1 after entering M010");
161
+
162
+ // Tear down dir1 cleanly
163
+ teardownAutoWorktree(dir1, "M010");
164
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry cleared after M010 teardown");
165
+
166
+ // Enter dir2/M020 — registry should now hold dir2 only
167
+ createAutoWorktree(dir2, "M020");
168
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir2,
169
+ "registry holds dir2 after entering M020 (single-occupancy preserved)");
170
+ assert.notStrictEqual(getAutoWorktreeOriginalBase(), dir1,
171
+ "dir1 is no longer in the registry");
172
+
173
+ teardownAutoWorktree(dir2, "M020");
174
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
175
+ });
176
+ });
@@ -41,7 +41,7 @@ const VALID_PARAMS = {
41
41
  ],
42
42
  };
43
43
 
44
- describe("complete-task rollback cleans up verification_evidence (#2724)", () => {
44
+ describe("complete-task projection failures keep DB completion committed", () => {
45
45
  let base: string;
46
46
 
47
47
  afterEach(() => {
@@ -75,7 +75,7 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
75
75
  assert.equal(rows.length, 2, "should have 2 evidence rows after success");
76
76
  });
77
77
 
78
- it("deletes verification_evidence rows on disk-render rollback", async () => {
78
+ it("keeps task completion and verification_evidence when disk projection write fails", async () => {
79
79
  base = makeTmpBase();
80
80
  openDatabase(join(base, ".gsd", "gsd.db"));
81
81
  insertMilestone({ id: "M001" });
@@ -87,20 +87,19 @@ describe("complete-task rollback cleans up verification_evidence (#2724)", () =>
87
87
  writeFileSync(tasksDir, "not-a-directory");
88
88
 
89
89
  const result = await handleCompleteTask(VALID_PARAMS, base);
90
- assert.ok("error" in result, "should return error when disk write fails");
90
+ assert.ok(!("error" in result), `unexpected error: ${"error" in result ? result.error : ""}`);
91
+ assert.equal(result.stale, true, "result should report stale projection");
91
92
 
92
- // Task should be rolled back to pending
93
93
  const adapter = _getAdapter()!;
94
94
  const task = adapter.prepare(
95
95
  `SELECT status FROM tasks WHERE milestone_id = 'M001' AND slice_id = 'S01' AND id = 'T01'`,
96
96
  ).get() as { status: string } | undefined;
97
97
  assert.ok(task, "task row should still exist");
98
- assert.equal(task!.status, "pending", "task status should be rolled back to pending");
98
+ assert.equal(task!.status, "complete", "task status should remain complete");
99
99
 
100
- // Verification evidence should be cleaned up — no orphaned rows
101
100
  const evidenceRows = adapter.prepare(
102
101
  `SELECT * FROM verification_evidence WHERE task_id = 'T01' AND slice_id = 'S01' AND milestone_id = 'M001'`,
103
102
  ).all();
104
- assert.equal(evidenceRows.length, 0, "verification_evidence should be empty after rollback");
103
+ assert.equal(evidenceRows.length, 2, "verification_evidence should remain committed");
105
104
  });
106
105
  });
@@ -403,12 +403,11 @@ console.log('\n=== complete-task: handler idempotency ===');
403
403
  const r1 = await handleCompleteTask(params, basePath);
404
404
  assertTrue(!('error' in r1), 'first call should succeed');
405
405
 
406
- // Verify complete-task did not duplicate T01. State reconciliation may import
407
- // the remaining plan task from disk so the DB stays aligned with S01-PLAN.md.
406
+ // Verify complete-task did not duplicate T01. S01-PLAN.md is a projection,
407
+ // so the remaining plan task is not imported implicitly.
408
408
  const tasks = getSliceTasks('M001', 'S01');
409
- assertEq(tasks.length, 2, 'should have T01 plus reconciled T02 after first call');
409
+ assertEq(tasks.length, 1, 'should only have the completed DB task after first call');
410
410
  assertEq(tasks.filter(t => t.id === 'T01').length, 1, 'should have exactly one T01 row after first call');
411
- assertEq(tasks.find(t => t.id === 'T02')?.status, 'pending', 'T02 should be reconciled as pending');
412
411
 
413
412
  // Second call with same params — state machine guard rejects (task is already complete)
414
413
  const r2 = await handleCompleteTask(params, basePath);
@@ -419,7 +418,7 @@ console.log('\n=== complete-task: handler idempotency ===');
419
418
 
420
419
  // Still no duplicate rows from the rejected second call.
421
420
  const tasksAfter = getSliceTasks('M001', 'S01');
422
- assertEq(tasksAfter.length, 2, 'should still have T01 plus reconciled T02 after rejected second call');
421
+ assertEq(tasksAfter.length, 1, 'should still only have T01 after rejected second call');
423
422
  assertEq(tasksAfter.filter(t => t.id === 'T01').length, 1, 'should still have exactly one T01 row');
424
423
 
425
424
  cleanupDir(basePath);
@@ -447,10 +446,13 @@ console.log('\n=== complete-task: handler with missing plan file ===');
447
446
  const params = makeValidParams();
448
447
  const result = await handleCompleteTask(params, basePath);
449
448
 
450
- // Should succeed even without plan file just skip checkbox toggle
449
+ // Should succeed and regenerate the missing plan projection from DB.
451
450
  assertTrue(!('error' in result), 'handler should succeed without plan file');
452
451
  if (!('error' in result)) {
453
452
  assertTrue(fs.existsSync(result.summaryPath), 'summary should be written even without plan file');
453
+ const planPath = path.join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md');
454
+ assertTrue(fs.existsSync(planPath), 'missing plan projection should be regenerated from DB');
455
+ assertTrue(fs.readFileSync(planPath, 'utf-8').includes('[x] **T01:'), 'regenerated plan should reflect DB task completion');
454
456
  }
455
457
 
456
458
  cleanupDir(basePath);
@@ -1,15 +1,9 @@
1
1
  /**
2
- * Behavioural regression test for #4129.
2
+ * Behavioural regression test for DB-authoritative task completion.
3
3
  *
4
- * When deriveStateFromDb's reconcileSliceTasks finds a SUMMARY.md on disk
5
- * for a task whose DB row is still pending, it flips the row to "complete".
6
- * Before #4129, the call to updateTaskStatus omitted the completedAt
7
- * timestamp, leaving completed_at NULL forever.
8
- *
9
- * The fix passes new Date().toISOString() as the 5th argument; this test
10
- * exercises that path end-to-end and asserts the column is populated.
11
- *
12
- * Refs #4829 (rewrite from positional source-grep).
4
+ * A task SUMMARY.md on disk is a projection, not a completion command.
5
+ * deriveStateFromDb must not flip a pending DB task to complete or invent a
6
+ * completed_at timestamp from disk evidence.
13
7
  */
14
8
 
15
9
  import { describe, test, beforeEach, afterEach } from 'node:test';
@@ -46,27 +40,26 @@ function setupProject(): void {
46
40
  `# M001\n\n## Slices\n\n- [ ] **S01: Slice** \`risk:low\` \`depends:[]\`\n - After this: works\n`,
47
41
  );
48
42
 
49
- // Plan file for the slice so reconcile can populate task list if DB is empty.
43
+ // Plan file for the slice. It is a projection and must not drive DB state.
50
44
  writeFileSync(
51
45
  join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'S01-PLAN.md'),
52
46
  `# S01: Slice\n\n## Tasks\n\n- [ ] **T01: Test task** \`est:30m\`\n - Do: x\n - Verify: y\n`,
53
47
  );
54
48
 
55
- // The summary file: this is the on-disk evidence that flips the task
56
- // status to "complete" inside reconcileSliceTasks.
49
+ // The summary file is a projection and must not complete the task.
57
50
  writeFileSync(
58
51
  join(basePath, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks', 'T01-SUMMARY.md'),
59
52
  '---\nid: T01\nparent: S01\nmilestone: M001\nblocker_discovered: false\n---\n# T01\n',
60
53
  );
61
54
  }
62
55
 
63
- describe('completed_at reconcile (#4129)', () => {
56
+ describe('completed_at DB-authoritative derivation', () => {
64
57
  beforeEach(() => {
65
58
  setupProject();
66
59
  openDatabase(join(basePath, '.gsd', 'gsd.db'));
67
60
  insertMilestone({ id: 'M001', title: 'M001', status: 'active' });
68
61
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice', status: 'active' });
69
- // Task is "pending" in DB, but SUMMARY.md exists on disk → reconcile flips it.
62
+ // Task is "pending" in DB, even though SUMMARY.md exists on disk.
70
63
  insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Test task', status: 'pending' });
71
64
  invalidateStateCache();
72
65
  });
@@ -76,24 +69,16 @@ describe('completed_at reconcile (#4129)', () => {
76
69
  try { rmSync(basePath, { recursive: true, force: true }); } catch { /* */ }
77
70
  });
78
71
 
79
- test('reconcileSliceTasks sets completed_at when flipping a pending task to complete via SUMMARY.md', async () => {
72
+ test('deriveStateFromDb does not set completed_at from a disk SUMMARY projection', async () => {
80
73
  const before = getTask('M001', 'S01', 'T01');
81
74
  assert.strictEqual(before?.status, 'pending', 'task starts pending');
82
75
  assert.strictEqual(before?.completed_at, null, 'task starts with completed_at NULL');
83
76
 
84
- // Trigger the reconcile path (state.ts reconcileSliceTasks).
77
+ // Derive runtime state. Disk SUMMARY.md must not mutate the DB row.
85
78
  await deriveStateFromDb(basePath);
86
79
 
87
80
  const after = getTask('M001', 'S01', 'T01');
88
- assert.strictEqual(after?.status, 'complete', 'task should be flipped to complete');
89
- assert.ok(
90
- typeof after?.completed_at === 'string' && after.completed_at.length > 0,
91
- `completed_at must be populated by reconcileSliceTasks (#4129); got ${JSON.stringify(after?.completed_at)}`,
92
- );
93
- // Sanity: timestamp parses as a valid ISO date.
94
- assert.ok(
95
- !Number.isNaN(Date.parse(after!.completed_at!)),
96
- `completed_at should be a valid ISO timestamp, got ${after!.completed_at}`,
97
- );
81
+ assert.strictEqual(after?.status, 'pending', 'task remains pending');
82
+ assert.strictEqual(after?.completed_at, null, 'completed_at remains NULL');
98
83
  });
99
84
  });
@@ -58,13 +58,26 @@ test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
58
58
  );
59
59
  });
60
60
 
61
- test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_STATE_FILES", () => {
61
+ test("syncStateToProjectRoot should back-sync completed-units.json", () => {
62
+ const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
63
+ const syncSrc = readFileSync(syncSrcPath, "utf-8");
64
+ const fnIdx = syncSrc.indexOf("export function syncStateToProjectRoot(");
65
+ assert.ok(fnIdx !== -1, "syncStateToProjectRoot exists");
66
+ const fnBlock = syncSrc.slice(fnIdx, syncSrc.indexOf("// ─── Resource Staleness", fnIdx));
67
+
68
+ assert.ok(
69
+ fnBlock.includes('"completed-units.json"'),
70
+ "syncStateToProjectRoot should copy completed-units.json back to the project root",
71
+ );
72
+ });
73
+
74
+ test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_DIAGNOSTIC_FILES", () => {
62
75
  const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
63
76
  const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
64
77
 
65
- // Find the ROOT_STATE_FILES constant (single source of truth for both sync directions)
66
- const constIdx = autoWorktreeSrc.indexOf("ROOT_STATE_FILES");
67
- assert.ok(constIdx !== -1, "ROOT_STATE_FILES constant exists");
78
+ // Find the ROOT_DIAGNOSTIC_FILES constant used for worktree copy-back.
79
+ const constIdx = autoWorktreeSrc.indexOf("ROOT_DIAGNOSTIC_FILES");
80
+ assert.ok(constIdx !== -1, "ROOT_DIAGNOSTIC_FILES constant exists");
68
81
 
69
82
  // Get the array content
70
83
  const arrayStart = autoWorktreeSrc.indexOf("[", constIdx);
@@ -73,7 +86,7 @@ test("#2313: syncWorktreeStateBack should include metrics.json in ROOT_STATE_FIL
73
86
 
74
87
  assert.ok(
75
88
  rootFilesBlock.includes("metrics.json"),
76
- "metrics.json should be in ROOT_STATE_FILES list",
89
+ "metrics.json should be in ROOT_DIAGNOSTIC_FILES list",
77
90
  );
78
91
  });
79
92
 
@@ -47,7 +47,7 @@ const symlinkResult = resolveProjectRootDbPath(symlinkPath);
47
47
  assertEq(
48
48
  symlinkResult,
49
49
  join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
50
- "/.gsd/projects/<hash>/worktrees/ resolves to hash-level DB (#2517, updated for #2952)",
50
+ "/.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
51
51
  );
52
52
 
53
53
  // Windows-style separators for symlink layout
@@ -57,7 +57,7 @@ if (sep === "\\") {
57
57
  assertEq(
58
58
  winResult,
59
59
  join("C:\\Users\\dev\\project\\.gsd\\projects\\abc123def", "gsd.db"),
60
- "Windows /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB",
60
+ "Windows /.gsd/projects/<hash>/worktrees/ resolves to external project state DB",
61
61
  );
62
62
  } else {
63
63
  // On non-Windows, test forward-slash variant explicitly
@@ -66,7 +66,7 @@ if (sep === "\\") {
66
66
  assertEq(
67
67
  fwdResult,
68
68
  join("/home/user/myproject/.gsd/projects/abc123def", "gsd.db"),
69
- "Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to hash-level DB on POSIX",
69
+ "Forward-slash /.gsd/projects/<hash>/worktrees/ resolves to external project state DB on POSIX",
70
70
  );
71
71
  }
72
72
 
@@ -76,7 +76,7 @@ const deepResult = resolveProjectRootDbPath(deepSymlinkPath);
76
76
  assertEq(
77
77
  deepResult,
78
78
  join("/home/user/myproject/.gsd/projects/deadbeef42", "gsd.db"),
79
- "Deep /.gsd/projects/<hash>/worktrees/ path resolves to hash-level DB (#2952)",
79
+ "Deep /.gsd/projects/<hash>/worktrees/ path resolves to external project state DB",
80
80
  );
81
81
 
82
82
  // Non-worktree path should be unchanged
@@ -0,0 +1,152 @@
1
+ // GSD-2 + db-writer path containment: regression tests for path.relative-based traversal guard
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
8
+
9
+ import { openDatabase, closeDatabase } from "../gsd-db.ts";
10
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
11
+ import {
12
+ saveArtifactToDbForWorkspace,
13
+ saveArtifactToDbByScope,
14
+ } from "../db-writer.ts";
15
+
16
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
17
+
18
+ function makeProjectDir(base: string): string {
19
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
20
+ return base;
21
+ }
22
+
23
+ // ─── Tests ───────────────────────────────────────────────────────────────────
24
+
25
+ describe("saveArtifactToDbForWorkspace: path.relative containment guard", () => {
26
+ let tmp: string;
27
+ let projectDir: string;
28
+
29
+ beforeEach(() => {
30
+ tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-fw-"));
31
+ projectDir = makeProjectDir(tmp);
32
+ openDatabase(join(projectDir, ".gsd", "gsd.db"));
33
+ });
34
+
35
+ afterEach(() => {
36
+ closeDatabase();
37
+ rmSync(tmp, { recursive: true, force: true });
38
+ });
39
+
40
+ // Attack: /foo/.gsd-other/file resolves to a path that startsWith("/foo/.gsd")
41
+ // but is NOT inside /foo/.gsd/. The path.relative fix correctly detects this.
42
+ test("rejects sibling directory that startsWith would have accepted", async () => {
43
+ // Create a sibling directory next to .gsd that shares the prefix
44
+ const sibling = join(projectDir, ".gsd-other");
45
+ mkdirSync(sibling, { recursive: true });
46
+
47
+ const ws = createWorkspace(projectDir);
48
+ // Craft an opts.path that traverses out of .gsd into .gsd-other
49
+ // resolve(gsdDir, "../.gsd-other/evil.md") === projectDir + "/.gsd-other/evil.md"
50
+ // which startsWith(projectDir + "/.gsd") because ".gsd-other" starts with ".gsd"
51
+ const traversalPath = "../.gsd-other/evil.md";
52
+
53
+ await assert.rejects(
54
+ () =>
55
+ saveArtifactToDbForWorkspace(ws, {
56
+ path: traversalPath,
57
+ artifact_type: "CONTEXT",
58
+ content: "attack",
59
+ }),
60
+ /path escapes \.gsd\/ directory/,
61
+ );
62
+ });
63
+
64
+ test("rejects absolute path input", async () => {
65
+ const ws = createWorkspace(projectDir);
66
+ await assert.rejects(
67
+ () =>
68
+ saveArtifactToDbForWorkspace(ws, {
69
+ path: "/etc/passwd",
70
+ artifact_type: "CONTEXT",
71
+ content: "attack",
72
+ }),
73
+ /path escapes \.gsd\/ directory/,
74
+ );
75
+ });
76
+
77
+ test("accepts a legitimate path inside .gsd/", async () => {
78
+ const ws = createWorkspace(projectDir);
79
+ // Should not throw — CONTEXT.md inside .gsd is valid
80
+ await assert.doesNotReject(() =>
81
+ saveArtifactToDbForWorkspace(ws, {
82
+ path: "CONTEXT.md",
83
+ artifact_type: "CONTEXT",
84
+ content: "# Context\n",
85
+ }),
86
+ );
87
+ });
88
+ });
89
+
90
+ describe("saveArtifactToDbByScope: path.relative containment guard", () => {
91
+ let tmp: string;
92
+ let projectDir: string;
93
+
94
+ beforeEach(() => {
95
+ tmp = mkdtempSync(join(tmpdir(), "gsd-path-contain-bs-"));
96
+ projectDir = makeProjectDir(tmp);
97
+ openDatabase(join(projectDir, ".gsd", "gsd.db"));
98
+ });
99
+
100
+ afterEach(() => {
101
+ closeDatabase();
102
+ rmSync(tmp, { recursive: true, force: true });
103
+ });
104
+
105
+ test("rejects sibling directory that startsWith would have accepted", async () => {
106
+ const sibling = join(projectDir, ".gsd-other");
107
+ mkdirSync(sibling, { recursive: true });
108
+
109
+ const ws = createWorkspace(projectDir);
110
+ const scope = scopeMilestone(ws, "M001");
111
+ const traversalPath = "../.gsd-other/evil.md";
112
+
113
+ await assert.rejects(
114
+ () =>
115
+ saveArtifactToDbByScope(scope, {
116
+ path: traversalPath,
117
+ artifact_type: "CONTEXT",
118
+ content: "attack",
119
+ }),
120
+ /path escapes \.gsd\/ directory/,
121
+ );
122
+ });
123
+
124
+ test("rejects absolute path input", async () => {
125
+ const ws = createWorkspace(projectDir);
126
+ const scope = scopeMilestone(ws, "M001");
127
+ await assert.rejects(
128
+ () =>
129
+ saveArtifactToDbByScope(scope, {
130
+ path: "/etc/passwd",
131
+ artifact_type: "CONTEXT",
132
+ content: "attack",
133
+ }),
134
+ /path escapes \.gsd\/ directory/,
135
+ );
136
+ });
137
+
138
+ test("accepts a legitimate milestone-relative path inside .gsd/", async () => {
139
+ mkdirSync(join(projectDir, ".gsd", "milestones", "M001"), {
140
+ recursive: true,
141
+ });
142
+ const ws = createWorkspace(projectDir);
143
+ const scope = scopeMilestone(ws, "M001");
144
+ await assert.doesNotReject(() =>
145
+ saveArtifactToDbByScope(scope, {
146
+ path: "milestones/M001/M001-CONTEXT.md",
147
+ artifact_type: "CONTEXT",
148
+ content: "# Context\n",
149
+ }),
150
+ );
151
+ });
152
+ });