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,218 @@
1
+ // GSD-2 + Gate 1b recovery counter bound — regression tests for H1 fix (#5012)
2
+ //
3
+ // Verifies that checkAutoStartAfterDiscuss stops emitting plan-blocked recovery
4
+ // hints (with triggerTurn:true) after MAX_PLAN_BLOCKED_RECOVERIES attempts and
5
+ // instead escalates to the user via ctx.ui.notify("error"), breaking the loop.
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import {
14
+ checkAutoStartAfterDiscuss,
15
+ setPendingAutoStart,
16
+ clearPendingAutoStart,
17
+ _getPendingAutoStart,
18
+ } from "../guided-flow.ts";
19
+ import { drainLogs } from "../workflow-logger.ts";
20
+ import {
21
+ openDatabase,
22
+ closeDatabase,
23
+ insertMilestone,
24
+ } from "../gsd-db.ts";
25
+
26
+ // ─── Harness ───────────────────────────────────────────────────────────────
27
+
28
+ interface MockCapture {
29
+ notifies: Array<{ msg: string; level: string }>;
30
+ messages: Array<{ payload: any; options: any }>;
31
+ }
32
+
33
+ function mkCapture(): MockCapture {
34
+ return { notifies: [], messages: [] };
35
+ }
36
+
37
+ function mkCtx(cap: MockCapture): any {
38
+ return {
39
+ ui: {
40
+ notify: (msg: string, level: string) => {
41
+ cap.notifies.push({ msg, level });
42
+ },
43
+ },
44
+ };
45
+ }
46
+
47
+ function mkPi(cap: MockCapture): any {
48
+ return {
49
+ sendMessage: (payload: any, options: any) => {
50
+ cap.messages.push({ payload, options });
51
+ },
52
+ setActiveTools: () => undefined,
53
+ getActiveTools: () => [],
54
+ };
55
+ }
56
+
57
+ function mkBase(): string {
58
+ const base = mkdtempSync(join(tmpdir(), "gsd-gate1b-bound-"));
59
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
60
+ writeFileSync(
61
+ join(base, ".gsd", "milestones", "M001", "M001-CONTEXT.md"),
62
+ "# M001: Bound Test\n\nContext written by discuss phase.\n",
63
+ );
64
+ return base;
65
+ }
66
+
67
+ // ─── Tests ─────────────────────────────────────────────────────────────────
68
+
69
+ describe("Gate 1b recovery bound (H1)", () => {
70
+ let base: string;
71
+ let cap: MockCapture;
72
+
73
+ beforeEach(() => {
74
+ clearPendingAutoStart();
75
+ drainLogs();
76
+ });
77
+
78
+ afterEach(() => {
79
+ closeDatabase();
80
+ clearPendingAutoStart();
81
+ if (base) {
82
+ rmSync(base, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ test("first N-1 invocations increment counter and emit recovery with triggerTurn:true", () => {
87
+ base = mkBase();
88
+ openDatabase(":memory:");
89
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
90
+
91
+ cap = mkCapture();
92
+ setPendingAutoStart(base, {
93
+ basePath: base,
94
+ milestoneId: "M001",
95
+ ctx: mkCtx(cap),
96
+ pi: mkPi(cap),
97
+ });
98
+
99
+ // MAX_PLAN_BLOCKED_RECOVERIES = 3; first two calls should emit recovery
100
+ const resultOne = checkAutoStartAfterDiscuss();
101
+ assert.equal(resultOne, false, "call 1: must return false");
102
+ assert.equal(cap.messages.length, 1, "call 1: exactly one sendMessage");
103
+ assert.equal(cap.messages[0].options.triggerTurn, true, "call 1: triggerTurn must be true");
104
+ assert.equal(cap.messages[0].payload.customType, "gsd-plan-milestone-blocked-recovery");
105
+
106
+ const entryAfterOne = _getPendingAutoStart(base);
107
+ assert.ok(entryAfterOne, "entry must still exist after call 1");
108
+ assert.equal(entryAfterOne.planBlockedRecoveryCount, 1, "counter must be 1 after call 1");
109
+
110
+ const resultTwo = checkAutoStartAfterDiscuss();
111
+ assert.equal(resultTwo, false, "call 2: must return false");
112
+ assert.equal(cap.messages.length, 2, "call 2: second sendMessage emitted");
113
+ assert.equal(cap.messages[1].options.triggerTurn, true, "call 2: triggerTurn must be true");
114
+
115
+ const entryAfterTwo = _getPendingAutoStart(base);
116
+ assert.ok(entryAfterTwo, "entry must still exist after call 2");
117
+ assert.equal(entryAfterTwo.planBlockedRecoveryCount, 2, "counter must be 2 after call 2");
118
+ });
119
+
120
+ test("Nth invocation (at MAX_PLAN_BLOCKED_RECOVERIES) escalates via notify(error) without sendMessage(triggerTurn)", () => {
121
+ base = mkBase();
122
+ openDatabase(":memory:");
123
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
124
+
125
+ cap = mkCapture();
126
+ setPendingAutoStart(base, {
127
+ basePath: base,
128
+ milestoneId: "M001",
129
+ ctx: mkCtx(cap),
130
+ pi: mkPi(cap),
131
+ });
132
+
133
+ // Exhaust the recovery budget (MAX = 3): call 3 times to reach the limit
134
+ checkAutoStartAfterDiscuss(); // count → 1
135
+ checkAutoStartAfterDiscuss(); // count → 2
136
+ checkAutoStartAfterDiscuss(); // count → 3
137
+
138
+ // At count = 3 the counter equals MAX so the next call must escalate
139
+ cap.messages = [];
140
+ cap.notifies = [];
141
+ drainLogs();
142
+
143
+ const resultAtLimit = checkAutoStartAfterDiscuss();
144
+ assert.equal(resultAtLimit, false, "at-limit call: must return false");
145
+
146
+ // Must NOT trigger a new LLM turn
147
+ assert.equal(
148
+ cap.messages.length,
149
+ 0,
150
+ "at-limit call: sendMessage must NOT be called (loop must stop)",
151
+ );
152
+ const triggerMessages = cap.messages.filter((m) => m.options?.triggerTurn);
153
+ assert.equal(triggerMessages.length, 0, "no triggerTurn message after limit");
154
+
155
+ // Must escalate to user via notify("error")
156
+ const errorNotify = cap.notifies.find((n) => n.level === "error");
157
+ assert.ok(errorNotify, "at-limit call: ctx.ui.notify('error') must be called");
158
+ assert.match(
159
+ errorNotify.msg,
160
+ /gsd-debug/i,
161
+ "error notification must direct user to run /gsd-debug",
162
+ );
163
+ assert.match(
164
+ errorNotify.msg,
165
+ /M001/,
166
+ "error notification must include the milestone ID",
167
+ );
168
+
169
+ // Confirm the log records the escalation
170
+ const logs = drainLogs();
171
+ const escalationLog = logs.find(
172
+ (e) => e.component === "guided" && /Gate 1b/.test(e.message) && /escalat/.test(e.message),
173
+ );
174
+ assert.ok(escalationLog, "escalation must be logged via logWarning");
175
+ });
176
+
177
+ test("after clearPendingAutoStart + setPendingAutoStart the counter is reset to 0", () => {
178
+ base = mkBase();
179
+ openDatabase(":memory:");
180
+ insertMilestone({ id: "M001", title: "Bound Test", status: "queued" });
181
+
182
+ cap = mkCapture();
183
+ setPendingAutoStart(base, {
184
+ basePath: base,
185
+ milestoneId: "M001",
186
+ ctx: mkCtx(cap),
187
+ pi: mkPi(cap),
188
+ });
189
+
190
+ // Advance counter to 2
191
+ checkAutoStartAfterDiscuss();
192
+ checkAutoStartAfterDiscuss();
193
+
194
+ const entryBefore = _getPendingAutoStart(base);
195
+ assert.ok(entryBefore, "entry must exist");
196
+ assert.equal(entryBefore.planBlockedRecoveryCount, 2, "counter must be 2 before reset");
197
+
198
+ // Simulate user retry: clear then re-set
199
+ clearPendingAutoStart(base);
200
+ cap = mkCapture();
201
+ setPendingAutoStart(base, {
202
+ basePath: base,
203
+ milestoneId: "M001",
204
+ ctx: mkCtx(cap),
205
+ pi: mkPi(cap),
206
+ });
207
+
208
+ const entryAfter = _getPendingAutoStart(base);
209
+ assert.ok(entryAfter, "entry must exist after re-set");
210
+ assert.equal(entryAfter.planBlockedRecoveryCount, 0, "counter must be 0 after re-set (fresh entry)");
211
+
212
+ // Verify first call after reset emits recovery, not escalation
213
+ const result = checkAutoStartAfterDiscuss();
214
+ assert.equal(result, false, "first call after reset must return false");
215
+ assert.equal(cap.messages.length, 1, "recovery hint must be emitted after reset");
216
+ assert.equal(cap.messages[0].options.triggerTurn, true, "triggerTurn must be true after reset");
217
+ });
218
+ });
@@ -0,0 +1,117 @@
1
+ // GSD-2 + gsd-db failed-open restore: previous workspace connection survives a failed openDatabaseByWorkspace
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 {
10
+ openDatabase,
11
+ openDatabaseByWorkspace,
12
+ closeDatabase,
13
+ isDbAvailable,
14
+ getDbPath,
15
+ _getDbCache,
16
+ } from "../gsd-db.ts";
17
+ import { createWorkspace } from "../workspace.ts";
18
+ import type { GsdWorkspace } from "../workspace.ts";
19
+
20
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
21
+
22
+ function makeProjectDir(base: string): string {
23
+ mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
24
+ return base;
25
+ }
26
+
27
+ // ─── Tests ───────────────────────────────────────────────────────────────────
28
+
29
+ describe("openDatabaseByWorkspace: restores previous connection on failure", () => {
30
+ let tmpA: string;
31
+ let tmpB: string;
32
+
33
+ beforeEach(() => {
34
+ tmpA = mkdtempSync(join(tmpdir(), "gsd-db-restore-a-"));
35
+ tmpB = mkdtempSync(join(tmpdir(), "gsd-db-restore-b-"));
36
+ makeProjectDir(tmpA);
37
+ makeProjectDir(tmpB);
38
+ });
39
+
40
+ afterEach(() => {
41
+ closeDatabase();
42
+ rmSync(tmpA, { recursive: true, force: true });
43
+ rmSync(tmpB, { recursive: true, force: true });
44
+ });
45
+
46
+ test("previous workspace connection stays active after failed switch to non-existent path", () => {
47
+ // Open workspace A successfully
48
+ const wsA = createWorkspace(tmpA);
49
+ const openedA = openDatabaseByWorkspace(wsA);
50
+ assert.ok(openedA, "opening workspace A should succeed");
51
+ assert.ok(isDbAvailable(), "DB should be available after opening A");
52
+
53
+ const pathAfterA = getDbPath();
54
+ assert.ok(pathAfterA, "should have a DB path after opening A");
55
+
56
+ // Attempt to open a workspace pointing to a completely non-existent directory
57
+ // ("/does-not-exist-gsd-test" cannot be created), which will cause openDatabase to throw.
58
+ const fakeWs = {
59
+ identityKey: "fake-key-that-does-not-exist",
60
+ projectRoot: "/does-not-exist-gsd-test-ws-restore",
61
+ worktreeRoot: null,
62
+ mode: "project" as const,
63
+ contract: {
64
+ projectRoot: "/does-not-exist-gsd-test-ws-restore",
65
+ workRoot: "/does-not-exist-gsd-test-ws-restore",
66
+ projectGsd: "/does-not-exist-gsd-test-ws-restore/.gsd",
67
+ projectDb: "/does-not-exist-gsd-test-ws-restore/.gsd/does-not-exist.db",
68
+ worktreeGsd: null,
69
+ isWorktree: false,
70
+ },
71
+ lockRoot: "/does-not-exist-gsd-test-ws-restore",
72
+ } satisfies GsdWorkspace;
73
+
74
+ // This should throw because the path is invalid
75
+ assert.throws(
76
+ () => openDatabaseByWorkspace(fakeWs),
77
+ (err: Error) => err instanceof Error,
78
+ );
79
+
80
+ // After the failure, the previous workspace A connection must be restored
81
+ assert.ok(isDbAvailable(), "DB must still be available (workspace A connection restored)");
82
+ const pathAfterFailure = getDbPath();
83
+ assert.equal(pathAfterFailure, pathAfterA, "DB path must match workspace A's path after failed switch");
84
+ });
85
+
86
+ test("cache still contains workspace A entry after failed switch", () => {
87
+ const wsA = createWorkspace(tmpA);
88
+ openDatabaseByWorkspace(wsA);
89
+
90
+ const fakeWs = {
91
+ identityKey: "fake-key-cache-test",
92
+ projectRoot: "/does-not-exist-gsd-cache-test",
93
+ worktreeRoot: null,
94
+ mode: "project" as const,
95
+ contract: {
96
+ projectRoot: "/does-not-exist-gsd-cache-test",
97
+ workRoot: "/does-not-exist-gsd-cache-test",
98
+ projectGsd: "/does-not-exist-gsd-cache-test/.gsd",
99
+ projectDb: "/does-not-exist-gsd-cache-test/.gsd/no.db",
100
+ worktreeGsd: null,
101
+ isWorktree: false,
102
+ },
103
+ lockRoot: "/does-not-exist-gsd-cache-test",
104
+ } satisfies GsdWorkspace;
105
+
106
+ assert.throws(() => openDatabaseByWorkspace(fakeWs));
107
+
108
+ const cache = _getDbCache();
109
+ assert.ok(cache.has(wsA.identityKey), "cache must retain workspace A's entry after failed switch");
110
+
111
+ // Workspace A's connection is back as the active connection; switching back
112
+ // should succeed from cache (cache hit path) without re-opening.
113
+ const reopened = openDatabaseByWorkspace(wsA);
114
+ assert.ok(reopened, "switching back to workspace A should succeed from cache");
115
+ assert.ok(isDbAvailable(), "DB should be available after re-activating A");
116
+ });
117
+ });
@@ -0,0 +1,226 @@
1
+ // GSD-2 + gsd-db workspace-scoped connection cache tests
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
15
+ import {
16
+ openDatabaseByWorkspace,
17
+ openDatabaseByScope,
18
+ closeDatabaseByWorkspace,
19
+ closeAllDatabases,
20
+ _getDbCache,
21
+ _getAdapter,
22
+ } from "../gsd-db.ts";
23
+
24
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Create a minimal project directory with the artifacts that make
28
+ * createWorkspace() resolve it as a proper project root (not a bare temp dir).
29
+ * Returns the realpath-normalised absolute path.
30
+ */
31
+ function makeProjectDir(): string {
32
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-db-ws-scope-")));
33
+ // hasGsdBootstrapArtifacts checks for .gsd/milestones or .gsd/PREFERENCES.md
34
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
35
+ return dir;
36
+ }
37
+
38
+ /**
39
+ * Create a worktree path inside a project's .gsd/worktrees/<MID>/ layout.
40
+ * createWorkspace() will detect the /.gsd/worktrees/ segment and resolve the
41
+ * project root back to `projectDir`.
42
+ */
43
+ function makeWorktreeDir(projectDir: string, mid: string): string {
44
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", mid);
45
+ mkdirSync(worktreeDir, { recursive: true });
46
+ return worktreeDir;
47
+ }
48
+
49
+ // ─── Suite: same realpath → same identityKey → same DB instance ──────────────
50
+
51
+ describe("openDatabaseByWorkspace: same project reuses connection", () => {
52
+ let projectDir: string;
53
+
54
+ beforeEach(() => {
55
+ projectDir = makeProjectDir();
56
+ });
57
+
58
+ afterEach(() => {
59
+ closeAllDatabases();
60
+ rmSync(projectDir, { recursive: true, force: true });
61
+ });
62
+
63
+ test("two createWorkspace calls with the same path share identityKey", () => {
64
+ const ws1 = createWorkspace(projectDir);
65
+ const ws2 = createWorkspace(projectDir);
66
+ assert.equal(ws1.identityKey, ws2.identityKey);
67
+ });
68
+
69
+ test("openDatabaseByWorkspace returns the same DB adapter for the same project", () => {
70
+ const ws1 = createWorkspace(projectDir);
71
+ const ws2 = createWorkspace(projectDir);
72
+
73
+ const ok1 = openDatabaseByWorkspace(ws1);
74
+ assert.ok(ok1, "first open should succeed");
75
+ const adapter1 = _getAdapter();
76
+
77
+ const ok2 = openDatabaseByWorkspace(ws2);
78
+ assert.ok(ok2, "second open should succeed");
79
+ const adapter2 = _getAdapter();
80
+
81
+ assert.equal(adapter1, adapter2, "same project → same DB adapter instance");
82
+ assert.equal(_getDbCache().size, 1, "only one cache entry for same project");
83
+ });
84
+ });
85
+
86
+ // ─── Suite: different projects → different DB instances ──────────────────────
87
+
88
+ describe("openDatabaseByWorkspace: different projects get separate connections", () => {
89
+ let projectA: string;
90
+ let projectB: string;
91
+
92
+ beforeEach(() => {
93
+ projectA = makeProjectDir();
94
+ projectB = makeProjectDir();
95
+ });
96
+
97
+ afterEach(() => {
98
+ closeAllDatabases();
99
+ rmSync(projectA, { recursive: true, force: true });
100
+ rmSync(projectB, { recursive: true, force: true });
101
+ });
102
+
103
+ test("two different projects produce different identityKeys", () => {
104
+ const wsA = createWorkspace(projectA);
105
+ const wsB = createWorkspace(projectB);
106
+ assert.notEqual(wsA.identityKey, wsB.identityKey);
107
+ });
108
+
109
+ test("opening two different projects stores two cache entries", () => {
110
+ const wsA = createWorkspace(projectA);
111
+ const wsB = createWorkspace(projectB);
112
+
113
+ openDatabaseByWorkspace(wsA);
114
+ const adapterAfterA = _getAdapter();
115
+
116
+ openDatabaseByWorkspace(wsB);
117
+ const adapterAfterB = _getAdapter();
118
+
119
+ assert.notEqual(adapterAfterA, adapterAfterB, "different projects → different adapter instances");
120
+ assert.equal(_getDbCache().size, 2, "two cache entries for two distinct projects");
121
+ });
122
+ });
123
+
124
+ // ─── Suite: sibling worktrees share the same DB instance ─────────────────────
125
+
126
+ describe("openDatabaseByWorkspace: sibling worktrees share DB connection", () => {
127
+ let projectDir: string;
128
+
129
+ beforeEach(() => {
130
+ projectDir = makeProjectDir();
131
+ });
132
+
133
+ afterEach(() => {
134
+ closeAllDatabases();
135
+ rmSync(projectDir, { recursive: true, force: true });
136
+ });
137
+
138
+ test("worktree path resolves to same identityKey as project root", () => {
139
+ const worktreeDir = makeWorktreeDir(projectDir, "M001");
140
+ const wsProject = createWorkspace(projectDir);
141
+ const wsWorktree = createWorkspace(worktreeDir);
142
+ assert.equal(
143
+ wsProject.identityKey,
144
+ wsWorktree.identityKey,
145
+ "project root and sibling worktree share identityKey",
146
+ );
147
+ });
148
+
149
+ test("opening via project path and via worktree path yields the same DB adapter", () => {
150
+ const worktreeDir = makeWorktreeDir(projectDir, "M001");
151
+ const wsProject = createWorkspace(projectDir);
152
+ const wsWorktree = createWorkspace(worktreeDir);
153
+
154
+ openDatabaseByWorkspace(wsProject);
155
+ const adapterProject = _getAdapter();
156
+
157
+ openDatabaseByWorkspace(wsWorktree);
158
+ const adapterWorktree = _getAdapter();
159
+
160
+ assert.equal(
161
+ adapterProject,
162
+ adapterWorktree,
163
+ "sibling worktree reuses the same DB adapter as the project root",
164
+ );
165
+ assert.equal(_getDbCache().size, 1, "only one cache entry for project + sibling worktree");
166
+ });
167
+ });
168
+
169
+ // ─── Suite: closing removes only the targeted cache entry ─────────────────────
170
+
171
+ describe("closeDatabaseByWorkspace: removes only the targeted cache entry", () => {
172
+ let projectA: string;
173
+ let projectB: string;
174
+
175
+ beforeEach(() => {
176
+ projectA = makeProjectDir();
177
+ projectB = makeProjectDir();
178
+ });
179
+
180
+ afterEach(() => {
181
+ closeAllDatabases();
182
+ rmSync(projectA, { recursive: true, force: true });
183
+ rmSync(projectB, { recursive: true, force: true });
184
+ });
185
+
186
+ test("closing workspace A removes only A from the cache", () => {
187
+ const wsA = createWorkspace(projectA);
188
+ const wsB = createWorkspace(projectB);
189
+
190
+ openDatabaseByWorkspace(wsA);
191
+ openDatabaseByWorkspace(wsB);
192
+ assert.equal(_getDbCache().size, 2, "precondition: two cache entries");
193
+
194
+ closeDatabaseByWorkspace(wsA);
195
+
196
+ assert.equal(_getDbCache().size, 1, "one entry remains after closing A");
197
+ assert.ok(!_getDbCache().has(wsA.identityKey), "A's entry is gone");
198
+ assert.ok(_getDbCache().has(wsB.identityKey), "B's entry is still present");
199
+ });
200
+
201
+ test("closing the active workspace via closeDatabaseByWorkspace nulls currentDb", () => {
202
+ const wsA = createWorkspace(projectA);
203
+
204
+ openDatabaseByWorkspace(wsA);
205
+ assert.ok(_getAdapter() !== null, "precondition: adapter is open");
206
+
207
+ // Make wsA the active connection explicitly.
208
+ openDatabaseByWorkspace(wsA);
209
+ closeDatabaseByWorkspace(wsA);
210
+
211
+ // After closing the active connection, the global adapter should be null.
212
+ assert.equal(_getAdapter(), null, "currentDb should be null after closing active workspace");
213
+ assert.equal(_getDbCache().size, 0, "cache should be empty after closing sole entry");
214
+ });
215
+
216
+ test("openDatabaseByScope delegates to workspace correctly", () => {
217
+ const ws = createWorkspace(projectA);
218
+ const scope = scopeMilestone(ws, "M001");
219
+
220
+ const ok = openDatabaseByScope(scope);
221
+ assert.ok(ok, "openDatabaseByScope should succeed");
222
+ assert.ok(_getDbCache().has(ws.identityKey), "cache entry exists after openDatabaseByScope");
223
+
224
+ closeDatabaseByWorkspace(ws);
225
+ });
226
+ });
@@ -11,6 +11,7 @@ import {
11
11
  wasDbOpenAttempted,
12
12
  getDbProvider,
13
13
  getDbStatus,
14
+ SCHEMA_VERSION,
14
15
  insertDecision,
15
16
  getDecisionById,
16
17
  insertRequirement,
@@ -101,7 +102,7 @@ describe('gsd-db', () => {
101
102
  // Check schema_version table
102
103
  const adapter = _getAdapter()!;
103
104
  const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
104
- assert.deepStrictEqual(version?.['version'], 22, 'schema version should be 22');
105
+ assert.deepStrictEqual(version?.['version'], SCHEMA_VERSION, `schema version should be ${SCHEMA_VERSION}`);
105
106
 
106
107
  // Check tables exist by querying them
107
108
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
@@ -0,0 +1,66 @@
1
+ // GSD-2 + gsd-root-canonical: gsdRoot() result is realpath-canonicalized before caching
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ realpathSync,
9
+ rmSync,
10
+ symlinkSync,
11
+ } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { randomUUID } from "node:crypto";
15
+
16
+ import { gsdRoot, _clearGsdRootCache } from "../paths.ts";
17
+
18
+ // ─── Tests ───────────────────────────────────────────────────────────────────
19
+
20
+ describe("gsdRoot: returns realpath-canonicalized result", () => {
21
+ let projectDir: string;
22
+
23
+ beforeEach(() => {
24
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-root-canon-")));
25
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
26
+ _clearGsdRootCache();
27
+ });
28
+
29
+ afterEach(() => {
30
+ _clearGsdRootCache();
31
+ rmSync(projectDir, { recursive: true, force: true });
32
+ });
33
+
34
+ test("gsdRoot from a canonical project path returns a realpath-canonicalized result", () => {
35
+ const result = gsdRoot(projectDir);
36
+ const canonical = realpathSync(join(projectDir, ".gsd"));
37
+ assert.equal(result, canonical, "gsdRoot must return the realpath of the .gsd directory");
38
+ });
39
+
40
+ test("gsdRoot via a symlinked project path returns the realpath-canonicalized .gsd", (t) => {
41
+ // Create a symlink pointing to projectDir
42
+ const linkPath = join(tmpdir(), `gsd-root-link-${randomUUID()}`);
43
+ symlinkSync(projectDir, linkPath);
44
+ t.after(() => {
45
+ try { rmSync(linkPath); } catch { /* ignore */ }
46
+ });
47
+
48
+ _clearGsdRootCache();
49
+
50
+ const result = gsdRoot(linkPath);
51
+ // The canonical .gsd is under the realpath of projectDir, not the symlink
52
+ const canonicalGsd = realpathSync(join(projectDir, ".gsd"));
53
+
54
+ assert.equal(
55
+ result,
56
+ canonicalGsd,
57
+ `gsdRoot via symlink ("${linkPath}") must return the realpath'd .gsd ("${canonicalGsd}"), not a symlink-based path`,
58
+ );
59
+
60
+ // Also verify that the result does NOT contain the symlink in its path
61
+ assert.ok(
62
+ !result.startsWith(linkPath),
63
+ `gsdRoot result must not start with the symlink path "${linkPath}"`,
64
+ );
65
+ });
66
+ });