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,371 @@
1
+ // GSD-2 + Integration regression suite for workspace collapse (feat/workspace-collapse)
2
+
3
+ import { describe, test, beforeEach, afterEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import {
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ writeFileSync,
9
+ existsSync,
10
+ rmSync,
11
+ realpathSync,
12
+ } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { createWorkspace, scopeMilestone } from "../../workspace.ts";
18
+ import {
19
+ gsdRoot,
20
+ clearPathCache,
21
+ _clearGsdRootCache,
22
+ } from "../../paths.ts";
23
+ import {
24
+ loadWriteGateSnapshot,
25
+ markDepthVerified,
26
+ clearDiscussionFlowState,
27
+ } from "../../bootstrap/write-gate.ts";
28
+ import {
29
+ teardownAutoWorktree,
30
+ _resetAutoWorktreeOriginalBaseForTests,
31
+ } from "../../auto-worktree.ts";
32
+ import {
33
+ openDatabaseByWorkspace,
34
+ closeAllDatabases,
35
+ _getDbCache,
36
+ } from "../../gsd-db.ts";
37
+
38
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
39
+
40
+ function makeProjectDir(): string {
41
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-collapse-int-")));
42
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
43
+ return dir;
44
+ }
45
+
46
+ function git(args: string[], cwd: string): void {
47
+ execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
48
+ }
49
+
50
+ function makeGitRepo(): string {
51
+ const dir = makeProjectDir();
52
+ git(["init"], dir);
53
+ git(["config", "user.email", "test@gsd.test"], dir);
54
+ git(["config", "user.name", "GSD Test"], dir);
55
+ writeFileSync(join(dir, "README.md"), "# test\n");
56
+ git(["add", "README.md"], dir);
57
+ git(["commit", "-m", "init"], dir);
58
+ git(["branch", "-M", "main"], dir);
59
+ return dir;
60
+ }
61
+
62
+ // ─── Test 1: Writer/validator path agreement under cwd-drift ─────────────────
63
+
64
+ describe("workspace-collapse integration: Test 1 — cwd-drift path agreement", () => {
65
+ let projectDir: string;
66
+ let otherDir: string;
67
+ const savedCwd = process.cwd();
68
+
69
+ beforeEach(() => {
70
+ projectDir = makeProjectDir();
71
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cwd-drift-other-")));
72
+ _clearGsdRootCache();
73
+ });
74
+
75
+ afterEach(() => {
76
+ process.chdir(savedCwd);
77
+ rmSync(projectDir, { recursive: true, force: true });
78
+ rmSync(otherDir, { recursive: true, force: true });
79
+ _clearGsdRootCache();
80
+ });
81
+
82
+ test("contextFile() returns same absolute path before and after cwd change", () => {
83
+ const worktreeDir = join(projectDir, ".gsd", "worktrees", "M001");
84
+ mkdirSync(worktreeDir, { recursive: true });
85
+
86
+ const ws = createWorkspace(projectDir);
87
+ const scope = scopeMilestone(ws, "M001");
88
+
89
+ // Record the path the "writer" would use
90
+ const writerPath = scope.contextFile();
91
+ assert.ok(writerPath.startsWith(projectDir), "writer path is under projectDir");
92
+
93
+ // Simulate cwd drift
94
+ process.chdir(otherDir);
95
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted away from projectDir");
96
+
97
+ // The "validator" recomputes via the same scope
98
+ const validatorPath = scope.contextFile();
99
+
100
+ assert.equal(
101
+ validatorPath,
102
+ writerPath,
103
+ "contextFile() must return the same absolute path regardless of cwd drift",
104
+ );
105
+ });
106
+
107
+ test("scopeMilestone paths are stable across cwd changes (roadmap, state, db)", () => {
108
+ const ws = createWorkspace(projectDir);
109
+ const scope = scopeMilestone(ws, "M001");
110
+
111
+ const before = {
112
+ roadmap: scope.roadmapFile(),
113
+ state: scope.stateFile(),
114
+ db: scope.dbPath(),
115
+ milestoneDir: scope.milestoneDir(),
116
+ };
117
+
118
+ process.chdir(otherDir);
119
+
120
+ assert.equal(scope.roadmapFile(), before.roadmap, "roadmapFile() stable after cwd drift");
121
+ assert.equal(scope.stateFile(), before.state, "stateFile() stable after cwd drift");
122
+ assert.equal(scope.dbPath(), before.db, "dbPath() stable after cwd drift");
123
+ assert.equal(scope.milestoneDir(), before.milestoneDir, "milestoneDir() stable after cwd drift");
124
+ });
125
+ });
126
+
127
+ // ─── Test 2: Abort path leaves no stale state ────────────────────────────────
128
+
129
+ describe("workspace-collapse integration: Test 2 — abort teardown clears stale state", () => {
130
+ let repoDir: string;
131
+ const savedCwd = process.cwd();
132
+
133
+ beforeEach(() => {
134
+ repoDir = makeGitRepo();
135
+ _resetAutoWorktreeOriginalBaseForTests();
136
+ });
137
+
138
+ afterEach(() => {
139
+ process.chdir(savedCwd);
140
+ _resetAutoWorktreeOriginalBaseForTests();
141
+ rmSync(repoDir, { recursive: true, force: true });
142
+ });
143
+
144
+ test("STATE.md, auto.lock, and M001-META.json are removed by teardownAutoWorktree", () => {
145
+ const gsdDir = join(repoDir, ".gsd");
146
+ const milestonesDir = join(gsdDir, "milestones", "M001");
147
+ mkdirSync(milestonesDir, { recursive: true });
148
+
149
+ const stateMd = join(gsdDir, "STATE.md");
150
+ const autoLock = join(gsdDir, "auto.lock");
151
+ const metaJson = join(milestonesDir, "M001-META.json");
152
+
153
+ writeFileSync(stateMd, "# State\nactive\n");
154
+ writeFileSync(autoLock, JSON.stringify({ pid: process.pid, unitType: "plan-milestone", unitId: "M001" }));
155
+ writeFileSync(metaJson, JSON.stringify({ milestoneId: "M001" }));
156
+
157
+ assert.ok(existsSync(stateMd), "STATE.md exists before teardown");
158
+ assert.ok(existsSync(autoLock), "auto.lock exists before teardown");
159
+ assert.ok(existsSync(metaJson), "M001-META.json exists before teardown");
160
+
161
+ // teardownAutoWorktree clears state files before the git step; git removal
162
+ // may fail in a minimal test repo — that is acceptable.
163
+ try {
164
+ teardownAutoWorktree(repoDir, "M001");
165
+ } catch {
166
+ // git worktree removal may fail when no worktree was created — non-fatal for this assertion
167
+ }
168
+
169
+ assert.ok(!existsSync(stateMd), "STATE.md removed by teardownAutoWorktree (regression: A5)");
170
+ assert.ok(!existsSync(autoLock), "auto.lock removed by teardownAutoWorktree (regression: A5)");
171
+ assert.ok(!existsSync(metaJson), "M001-META.json removed by teardownAutoWorktree (regression: A5)");
172
+ });
173
+ });
174
+
175
+ // ─── Test 3: Cwd drift between persist and load of write-gate state ──────────
176
+
177
+ describe("workspace-collapse integration: Test 3 — write-gate snapshot survives cwd drift", () => {
178
+ let projectDir: string;
179
+ let otherDir: string;
180
+ const savedCwd = process.cwd();
181
+
182
+ beforeEach(() => {
183
+ projectDir = makeProjectDir();
184
+ otherDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-wg-other-")));
185
+ // Start with a clean write-gate state for projectDir
186
+ clearDiscussionFlowState(projectDir);
187
+ });
188
+
189
+ afterEach(() => {
190
+ process.chdir(savedCwd);
191
+ clearDiscussionFlowState(projectDir);
192
+ try { clearDiscussionFlowState(otherDir); } catch { /* best-effort */ }
193
+ rmSync(projectDir, { recursive: true, force: true });
194
+ rmSync(otherDir, { recursive: true, force: true });
195
+ });
196
+
197
+ test("loadWriteGateSnapshot returns persisted state after cwd drift", () => {
198
+ // Persist a snapshot: mark M001 depth-verified for projectDir
199
+ markDepthVerified("M001", projectDir);
200
+
201
+ // Drift cwd away from projectDir
202
+ process.chdir(otherDir);
203
+ assert.notEqual(process.cwd(), projectDir, "cwd has drifted");
204
+
205
+ // Load the snapshot using the explicit basePath — must not be affected by cwd
206
+ const snapshot = loadWriteGateSnapshot(projectDir);
207
+
208
+ assert.ok(
209
+ snapshot.verifiedDepthMilestones.includes("M001"),
210
+ "snapshot loaded from projectDir includes M001 despite cwd drift",
211
+ );
212
+ });
213
+
214
+ test("loadWriteGateSnapshot from different basePath does not bleed state", () => {
215
+ markDepthVerified("M001", projectDir);
216
+
217
+ process.chdir(otherDir);
218
+
219
+ // otherDir has no persisted state — should return empty snapshot
220
+ const snapshot = loadWriteGateSnapshot(otherDir);
221
+
222
+ assert.ok(
223
+ !snapshot.verifiedDepthMilestones.includes("M001"),
224
+ "otherDir snapshot must not bleed M001 state from projectDir",
225
+ );
226
+ });
227
+ });
228
+
229
+ // ─── Test 4: Sibling worktrees share DB connection ───────────────────────────
230
+
231
+ describe("workspace-collapse integration: Test 4 — sibling worktrees share DB connection", () => {
232
+ let projectDir: string;
233
+
234
+ beforeEach(() => {
235
+ projectDir = makeProjectDir();
236
+ });
237
+
238
+ afterEach(() => {
239
+ closeAllDatabases();
240
+ rmSync(projectDir, { recursive: true, force: true });
241
+ });
242
+
243
+ test("ws1 and ws2 (sibling worktrees) have same identityKey", () => {
244
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
245
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
246
+ mkdirSync(wt1, { recursive: true });
247
+ mkdirSync(wt2, { recursive: true });
248
+
249
+ const ws1 = createWorkspace(wt1);
250
+ const ws2 = createWorkspace(wt2);
251
+
252
+ assert.equal(
253
+ ws1.identityKey,
254
+ ws2.identityKey,
255
+ "sibling worktrees M001 and M002 must share the same identityKey",
256
+ );
257
+ assert.equal(
258
+ ws1.identityKey,
259
+ realpathSync(projectDir),
260
+ "identityKey is the realpath of the project root",
261
+ );
262
+ });
263
+
264
+ test("openDatabaseByWorkspace for sibling worktrees resolves to the same DB path", () => {
265
+ const wt1 = join(projectDir, ".gsd", "worktrees", "M001");
266
+ const wt2 = join(projectDir, ".gsd", "worktrees", "M002");
267
+ mkdirSync(wt1, { recursive: true });
268
+ mkdirSync(wt2, { recursive: true });
269
+
270
+ const ws1 = createWorkspace(wt1);
271
+ const ws2 = createWorkspace(wt2);
272
+
273
+ const ok1 = openDatabaseByWorkspace(ws1);
274
+ assert.ok(ok1, "openDatabaseByWorkspace(ws1) must succeed");
275
+ const cacheAfterWs1 = _getDbCache();
276
+ const entry1 = cacheAfterWs1.get(ws1.identityKey);
277
+ assert.ok(entry1, "cache entry for ws1.identityKey must exist");
278
+ const dbPath1 = entry1.dbPath;
279
+
280
+ const ok2 = openDatabaseByWorkspace(ws2);
281
+ assert.ok(ok2, "openDatabaseByWorkspace(ws2) must succeed");
282
+ const cacheAfterWs2 = _getDbCache();
283
+ const entry2 = cacheAfterWs2.get(ws2.identityKey);
284
+ assert.ok(entry2, "cache entry for ws2.identityKey must exist");
285
+ const dbPath2 = entry2.dbPath;
286
+
287
+ assert.equal(
288
+ dbPath1,
289
+ dbPath2,
290
+ "sibling worktrees must resolve to the same DB path (shared WAL)",
291
+ );
292
+ assert.equal(
293
+ cacheAfterWs2.size,
294
+ 1,
295
+ "only one cache entry for project + two sibling worktrees",
296
+ );
297
+ });
298
+ });
299
+
300
+ // ─── Test 5: gsdRootCache normalization survives trailing-slash inputs ────────
301
+
302
+ describe("workspace-collapse integration: Test 5 — gsdRootCache normalization deduplicates trailing-slash inputs", () => {
303
+ let projectDir: string;
304
+ let fakeHome: string;
305
+ let savedHome: string | undefined;
306
+ let savedUserProfile: string | undefined;
307
+ let savedGsdHome: string | undefined;
308
+
309
+ beforeEach(() => {
310
+ projectDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-")));
311
+ mkdirSync(join(projectDir, ".gsd"), { recursive: true });
312
+
313
+ fakeHome = realpathSync(mkdtempSync(join(tmpdir(), "gsd-cache-int-home-")));
314
+
315
+ savedHome = process.env.HOME;
316
+ savedUserProfile = process.env.USERPROFILE;
317
+ savedGsdHome = process.env.GSD_HOME;
318
+
319
+ // Prevent ~/.gsd interference
320
+ process.env.HOME = fakeHome;
321
+ process.env.USERPROFILE = fakeHome;
322
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
323
+
324
+ clearPathCache();
325
+ });
326
+
327
+ afterEach(() => {
328
+ if (savedHome === undefined) delete process.env.HOME;
329
+ else process.env.HOME = savedHome;
330
+ if (savedUserProfile === undefined) delete process.env.USERPROFILE;
331
+ else process.env.USERPROFILE = savedUserProfile;
332
+ if (savedGsdHome === undefined) delete process.env.GSD_HOME;
333
+ else process.env.GSD_HOME = savedGsdHome;
334
+
335
+ clearPathCache();
336
+ rmSync(projectDir, { recursive: true, force: true });
337
+ rmSync(fakeHome, { recursive: true, force: true });
338
+ });
339
+
340
+ test("gsdRoot('/path/to/project') and gsdRoot('/path/to/project/') return identical paths", () => {
341
+ const withoutSlash = gsdRoot(projectDir);
342
+ const withSlash = gsdRoot(projectDir + "/");
343
+
344
+ assert.equal(
345
+ withoutSlash,
346
+ withSlash,
347
+ "gsdRoot must return identical paths for inputs with and without trailing slash",
348
+ );
349
+ assert.equal(
350
+ withoutSlash,
351
+ join(projectDir, ".gsd"),
352
+ "both calls must resolve to projectDir/.gsd",
353
+ );
354
+ });
355
+
356
+ test("both calls after clearPathCache() return identical paths (no duplicate cache entries)", () => {
357
+ // Start clean
358
+ clearPathCache();
359
+
360
+ const r1 = gsdRoot(projectDir);
361
+ const r2 = gsdRoot(projectDir + "/");
362
+
363
+ assert.equal(r1, r2, "r1 and r2 must be the same string after normalization");
364
+ // The cache normalizes both inputs to the same key — no duplicate entries.
365
+ // We can't inspect the cache size directly, but the behavioral proof is
366
+ // that a second call after clearPathCache re-probes and still matches.
367
+ clearPathCache();
368
+ const r3 = gsdRoot(projectDir + "/");
369
+ assert.equal(r3, r1, "re-probe after clearPathCache must produce the same result");
370
+ });
371
+ });
@@ -856,10 +856,10 @@ test('── markdown-renderer: renderAllFromDb produces all files ──', asyn
856
856
  });
857
857
 
858
858
  // ═══════════════════════════════════════════════════════════════════════════
859
- // Graceful Degradation (Disk Fallback)
859
+ // DB-authoritative regeneration
860
860
  // ═══════════════════════════════════════════════════════════════════════════
861
861
 
862
- test('── markdown-renderer: graceful fallback reads from disk when artifact not in DB ──', async () => {
862
+ test('── markdown-renderer: missing artifact regenerates from DB without importing disk projection ──', async () => {
863
863
  const tmpDir = makeTmpDir();
864
864
  const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
865
865
  openDatabase(dbPath);
@@ -874,7 +874,7 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
874
874
  // Write roadmap to disk but NOT in artifacts DB
875
875
  const roadmapContent = makeRoadmapContent([
876
876
  { id: 'S01', title: 'Core', done: false },
877
- ]);
877
+ ]) + '\n\nDISK_ONLY_SENTINEL';
878
878
  const roadmapPath = path.join(tmpDir, '.gsd', 'milestones', 'M001', 'M001-ROADMAP.md');
879
879
  fs.writeFileSync(roadmapPath, roadmapContent);
880
880
  clearAllCaches();
@@ -883,14 +883,20 @@ test('── markdown-renderer: graceful fallback reads from disk when artifact
883
883
  const before = getArtifact('milestones/M001/M001-ROADMAP.md');
884
884
  assert.deepStrictEqual(before, null, 'artifact not in DB before render');
885
885
 
886
- // Render — should read from disk, store in DB
886
+ // Render — should regenerate from DB rows, not import/patch disk content.
887
887
  const ok = await renderRoadmapCheckboxes(tmpDir, 'M001');
888
- assert.ok(ok, 'render succeeds with disk fallback');
888
+ assert.ok(ok, 'render succeeds by regenerating from DB');
889
889
 
890
- // Verify artifact now in DB (stored after reading from disk)
890
+ // Verify artifact now exists in DB but does not contain disk-only content.
891
891
  const after = getArtifact('milestones/M001/M001-ROADMAP.md');
892
- assert.ok(after !== null, 'artifact stored in DB after disk fallback render');
893
- assert.ok(after!.full_content.includes('[x] **S01:'), 'DB artifact reflects rendered state');
892
+ assert.ok(after !== null, 'artifact regenerated in DB');
893
+ assert.ok(!after!.full_content.includes('DISK_ONLY_SENTINEL'), 'disk projection content was not imported');
894
+ assert.ok(after!.full_content.includes('S01'), 'DB artifact reflects DB slice state');
895
+
896
+ assert.ok(fs.existsSync(roadmapPath), 'roadmap projection regenerated on disk');
897
+ const diskAfter = fs.readFileSync(roadmapPath, 'utf-8');
898
+ assert.ok(!diskAfter.includes('DISK_ONLY_SENTINEL'), 'disk projection was rewritten from DB');
899
+ assert.ok(diskAfter.includes('S01'), 'disk projection reflects DB slice state');
894
900
  } finally {
895
901
  closeDatabase();
896
902
  cleanupDir(tmpDir);
@@ -9,6 +9,7 @@ import {
9
9
  getRequirementById,
10
10
  getActiveRequirements,
11
11
  insertArtifact,
12
+ SCHEMA_VERSION,
12
13
  _getAdapter,
13
14
  } from '../gsd-db.ts';
14
15
  import {
@@ -363,7 +364,7 @@ test('md-importer: schema v1→v2 migration', () => {
363
364
  openDatabase(':memory:');
364
365
  const adapter = _getAdapter();
365
366
  const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
366
- assert.deepStrictEqual(version?.v, 22, 'new DB should be at schema version 22');
367
+ assert.deepStrictEqual(version?.v, SCHEMA_VERSION, `new DB should be at schema version ${SCHEMA_VERSION}`);
367
368
 
368
369
  // Artifacts table should exist
369
370
  const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();
@@ -2,6 +2,7 @@ import {
2
2
  openDatabase,
3
3
  closeDatabase,
4
4
  isDbAvailable,
5
+ SCHEMA_VERSION,
5
6
  _getAdapter,
6
7
  } from '../gsd-db.ts';
7
8
  import {
@@ -328,9 +329,9 @@ test('memory-store: schema includes memories table', () => {
328
329
  const viewCount = adapter.prepare('SELECT count(*) as cnt FROM active_memories').get();
329
330
  assert.deepStrictEqual(viewCount?.['cnt'], 0, 'active_memories view should exist');
330
331
 
331
- // Verify schema version is 22 (v22 quality_gates DDL fix included)
332
+ // Verify schema version is current (includes quality_gates DDL fix and later migrations)
332
333
  const version = adapter.prepare('SELECT MAX(version) as v FROM schema_version').get();
333
- assert.deepStrictEqual(version?.["v"], 22, 'schema version should be 22');
334
+ assert.deepStrictEqual(version?.["v"], SCHEMA_VERSION, `schema version should be ${SCHEMA_VERSION}`);
334
335
 
335
336
  closeDatabase();
336
337
  });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * GSD2 Metrics — regression test for parallel-mode atomic merge
3
+ *
4
+ * Verifies that concurrent metrics.json writers do not silently discard
5
+ * each other's entries (last-writer-wins). Two child processes each write
6
+ * a distinct milestone unit; after both complete, the merged file must
7
+ * contain both units.
8
+ */
9
+
10
+ import { describe, test, beforeEach, afterEach } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import {
13
+ mkdtempSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ rmSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { tmpdir } from "node:os";
21
+ import { spawnSync } from "node:child_process";
22
+
23
+ // ─── Worker script source ────────────────────────────────────────────────────
24
+ //
25
+ // Each child process runs this script with two env vars:
26
+ // GSD_TEST_METRICS_PATH — absolute path to metrics.json
27
+ // GSD_TEST_MILESTONE_ID — milestone ID to record (e.g. "M001" or "M002")
28
+ //
29
+ // The script uses the same lock-acquire → read → merge → atomic-write
30
+ // pattern implemented in metrics.ts saveLedger(), but using only built-in
31
+ // Node.js modules so it runs without the full extension dependency tree.
32
+ //
33
+ const WORKER_SCRIPT = `
34
+ const { openSync, closeSync, unlinkSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } = require('node:fs');
35
+ const { dirname } = require('node:path');
36
+ const { randomBytes } = require('node:crypto');
37
+
38
+ const metricsPath = process.env.GSD_TEST_METRICS_PATH;
39
+ const milestoneId = process.env.GSD_TEST_MILESTONE_ID;
40
+ const lockPath = metricsPath + '.lock';
41
+
42
+ // ── Lock helpers ──────────────────────────────────────────────────────────
43
+ function acquireLock(lockPath, timeoutMs) {
44
+ const deadline = Date.now() + timeoutMs;
45
+ while (Date.now() < deadline) {
46
+ try {
47
+ const fd = openSync(lockPath, 'wx');
48
+ closeSync(fd);
49
+ return true;
50
+ } catch {
51
+ const waitUntil = Date.now() + Math.min(50, deadline - Date.now());
52
+ while (Date.now() < waitUntil) { /* spin */ }
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+
58
+ function releaseLock(lockPath) {
59
+ try { unlinkSync(lockPath); } catch {}
60
+ }
61
+
62
+ // ── Atomic write helper ───────────────────────────────────────────────────
63
+ function saveJsonAtomic(filePath, data) {
64
+ mkdirSync(dirname(filePath), { recursive: true });
65
+ const tmp = filePath + '.tmp.' + randomBytes(4).toString('hex');
66
+ writeFileSync(tmp, JSON.stringify(data, null, 2) + '\\n', 'utf-8');
67
+ renameSync(tmp, filePath);
68
+ }
69
+
70
+ // ── Dedup helper (same logic as metrics.ts deduplicateUnits) ─────────────
71
+ function deduplicateUnits(units) {
72
+ const map = new Map();
73
+ for (const u of units) {
74
+ const key = u.type + '\\0' + u.id + '\\0' + u.startedAt;
75
+ const existing = map.get(key);
76
+ if (!existing || u.finishedAt > existing.finishedAt) {
77
+ map.set(key, u);
78
+ }
79
+ }
80
+ return Array.from(map.values());
81
+ }
82
+
83
+ // ── Worker unit ───────────────────────────────────────────────────────────
84
+ const workerUnit = {
85
+ type: 'execute-task',
86
+ id: milestoneId + '/S01/T01',
87
+ model: 'test-model',
88
+ startedAt: 1000,
89
+ finishedAt: Date.now(),
90
+ tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, total: 150 },
91
+ cost: 0.01,
92
+ toolCalls: 1,
93
+ assistantMessages: 1,
94
+ userMessages: 1,
95
+ };
96
+
97
+ const workerLedger = {
98
+ version: 1,
99
+ projectStartedAt: 1000,
100
+ units: [workerUnit],
101
+ };
102
+
103
+ // ── Merge write ───────────────────────────────────────────────────────────
104
+ const acquired = acquireLock(lockPath, 5000);
105
+ try {
106
+ let onDiskUnits = [];
107
+ if (existsSync(metricsPath)) {
108
+ try {
109
+ const parsed = JSON.parse(readFileSync(metricsPath, 'utf-8'));
110
+ if (parsed && Array.isArray(parsed.units)) onDiskUnits = parsed.units;
111
+ } catch {}
112
+ }
113
+ const merged = deduplicateUnits([...onDiskUnits, ...workerLedger.units]);
114
+ saveJsonAtomic(metricsPath, { ...workerLedger, units: merged });
115
+ } finally {
116
+ if (acquired) releaseLock(lockPath);
117
+ }
118
+ `;
119
+
120
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
121
+
122
+ function spawnWorker(metricsPath: string, milestoneId: string): void {
123
+ const result = spawnSync(process.execPath, ["-e", WORKER_SCRIPT], {
124
+ env: {
125
+ ...process.env,
126
+ GSD_TEST_METRICS_PATH: metricsPath,
127
+ GSD_TEST_MILESTONE_ID: milestoneId,
128
+ },
129
+ encoding: "utf-8",
130
+ timeout: 10_000,
131
+ });
132
+ if (result.error) throw result.error;
133
+ if (result.status !== 0) {
134
+ throw new Error(
135
+ `Worker for ${milestoneId} exited with status ${result.status}:\n${result.stderr}`,
136
+ );
137
+ }
138
+ }
139
+
140
+ // ─── Tests ───────────────────────────────────────────────────────────────────
141
+
142
+ describe("metrics atomic merge — parallel workers", () => {
143
+ let tmpDir: string;
144
+ let gsdDir: string;
145
+ let metricsPath: string;
146
+
147
+ beforeEach(() => {
148
+ tmpDir = mkdtempSync(join(tmpdir(), "gsd-metrics-atomic-"));
149
+ gsdDir = join(tmpDir, ".gsd");
150
+ mkdirSync(gsdDir, { recursive: true });
151
+ metricsPath = join(gsdDir, "metrics.json");
152
+ });
153
+
154
+ afterEach(() => {
155
+ rmSync(tmpDir, { recursive: true, force: true });
156
+ });
157
+
158
+ test("sequential writes from two workers both land in metrics.json", () => {
159
+ // Sequential baseline: M001 then M002. Both must survive.
160
+ spawnWorker(metricsPath, "M001");
161
+ spawnWorker(metricsPath, "M002");
162
+
163
+ const raw = readFileSync(metricsPath, "utf-8");
164
+ const ledger = JSON.parse(raw);
165
+
166
+ assert.ok(Array.isArray(ledger.units), "units must be an array");
167
+
168
+ const ids = ledger.units.map((u: { id: string }) => u.id) as string[];
169
+ assert.ok(ids.some(id => id.startsWith("M001")), "M001 unit must be present");
170
+ assert.ok(ids.some(id => id.startsWith("M002")), "M002 unit must be present");
171
+ });
172
+
173
+ test("concurrent writes from two workers both land in metrics.json (no last-writer-wins)", () => {
174
+ // Write an existing M001 entry to disk first, then run M002 worker.
175
+ // This simulates the race: M001 finishes and saves, then M002 reads-merges-writes.
176
+ const initialLedger = {
177
+ version: 1,
178
+ projectStartedAt: 1000,
179
+ units: [
180
+ {
181
+ type: "execute-task",
182
+ id: "M001/S01/T01",
183
+ model: "test-model",
184
+ startedAt: 1000,
185
+ finishedAt: 2000,
186
+ tokens: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, total: 150 },
187
+ cost: 0.01,
188
+ toolCalls: 1,
189
+ assistantMessages: 1,
190
+ userMessages: 1,
191
+ },
192
+ ],
193
+ };
194
+ writeFileSync(metricsPath, JSON.stringify(initialLedger, null, 2) + "\n", "utf-8");
195
+
196
+ // M002 worker runs — without merge semantics it would overwrite M001's data.
197
+ spawnWorker(metricsPath, "M002");
198
+
199
+ const raw = readFileSync(metricsPath, "utf-8");
200
+ const ledger = JSON.parse(raw);
201
+
202
+ assert.ok(Array.isArray(ledger.units), "units must be an array");
203
+ assert.equal(ledger.units.length, 2, "must contain exactly 2 units (M001 + M002)");
204
+
205
+ const ids = ledger.units.map((u: { id: string }) => u.id) as string[];
206
+ assert.ok(ids.some(id => id.startsWith("M001")), "M001 unit must be preserved after M002 write");
207
+ assert.ok(ids.some(id => id.startsWith("M002")), "M002 unit must be present");
208
+ });
209
+
210
+ test("idempotent write does not duplicate units", () => {
211
+ // Writing the same milestone unit twice must not create duplicates.
212
+ spawnWorker(metricsPath, "M001");
213
+ spawnWorker(metricsPath, "M001");
214
+
215
+ const raw = readFileSync(metricsPath, "utf-8");
216
+ const ledger = JSON.parse(raw);
217
+
218
+ assert.ok(Array.isArray(ledger.units), "units must be an array");
219
+ const m001Units = ledger.units.filter((u: { id: string }) => u.id.startsWith("M001"));
220
+ assert.equal(m001Units.length, 1, "duplicate units must be collapsed to one");
221
+ });
222
+ });