gsd-pi 2.81.0 → 2.82.0

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 (290) hide show
  1. package/README.md +36 -24
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +111 -8
  4. package/dist/resources/extensions/gsd/auto/phases.js +190 -97
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +66 -3
  6. package/dist/resources/extensions/gsd/auto/session.js +9 -0
  7. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +43 -0
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +182 -178
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -11
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  11. package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
  12. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -0
  13. package/dist/resources/extensions/gsd/auto-start.js +20 -23
  14. package/dist/resources/extensions/gsd/auto-unit-closeout.js +33 -5
  15. package/dist/resources/extensions/gsd/auto-verification.js +12 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +8 -0
  17. package/dist/resources/extensions/gsd/auto.js +265 -76
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +13 -6
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -2
  20. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +4 -8
  21. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +4 -10
  22. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
  23. package/dist/resources/extensions/gsd/git-service.js +2 -1
  24. package/dist/resources/extensions/gsd/gsd-db.js +7 -23
  25. package/dist/resources/extensions/gsd/health-widget-core.js +1 -1
  26. package/dist/resources/extensions/gsd/health-widget.js +4 -10
  27. package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
  28. package/dist/resources/extensions/gsd/native-git-bridge.js +14 -14
  29. package/dist/resources/extensions/gsd/notification-overlay.js +35 -40
  30. package/dist/resources/extensions/gsd/parallel-merge.js +53 -30
  31. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +25 -33
  32. package/dist/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  33. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  34. package/dist/resources/extensions/gsd/prompts/discuss.md +20 -2
  35. package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
  36. package/dist/resources/extensions/gsd/session-lock.js +40 -0
  37. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
  40. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
  41. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
  42. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
  43. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
  44. package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
  45. package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
  46. package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
  47. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
  48. package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
  49. package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
  50. package/dist/resources/extensions/gsd/tui/render-kit.js +74 -0
  51. package/dist/resources/extensions/gsd/watch/header-renderer.js +92 -69
  52. package/dist/resources/extensions/gsd/watch/splash-palette.js +10 -0
  53. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  54. package/dist/resources/extensions/gsd/worktree-lifecycle.js +722 -316
  55. package/dist/resources/extensions/gsd/worktree-telemetry.js +3 -1
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.html +1 -1
  77. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  84. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  86. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  87. package/dist/welcome-screen.d.ts +0 -7
  88. package/dist/welcome-screen.js +60 -69
  89. package/package.json +1 -1
  90. package/packages/daemon/package.json +2 -2
  91. package/packages/mcp-server/package.json +2 -2
  92. package/packages/native/package.json +1 -1
  93. package/packages/pi-agent-core/package.json +1 -1
  94. package/packages/pi-ai/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts +2 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js +47 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +76 -9
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts +2 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js +40 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts +0 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +30 -29
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js +10 -3
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +13 -13
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -3
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +58 -3
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +2 -2
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +12 -6
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -41
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +0 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +86 -82
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts +35 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js +152 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts +16 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts.map +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js +73 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js.map +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +12 -8
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +105 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +27 -26
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +9 -6
  152. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  153. package/packages/pi-coding-agent/package.json +1 -1
  154. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/assistant-message-design.test.ts +56 -0
  155. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +113 -9
  156. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/user-message-design.test.ts +48 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.test.ts +10 -3
  158. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +43 -42
  159. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +14 -14
  160. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +64 -3
  161. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +13 -7
  162. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +15 -42
  163. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -104
  164. package/packages/pi-coding-agent/src/modes/interactive/components/transcript-design.ts +196 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/tui-style-kit.ts +94 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +14 -9
  167. package/packages/pi-coding-agent/src/modes/interactive/theme/theme-highlight.test.ts +23 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +106 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +27 -26
  170. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +9 -6
  171. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  172. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +14 -1
  173. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
  174. package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
  175. package/packages/pi-tui/dist/overlay-layout.js +9 -6
  176. package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
  177. package/packages/pi-tui/package.json +1 -1
  178. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +20 -1
  179. package/packages/pi-tui/src/overlay-layout.ts +10 -7
  180. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  181. package/packages/rpc-client/package.json +1 -1
  182. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  183. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  184. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  185. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  186. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  187. package/pkg/dist/modes/interactive/theme/theme.js +105 -1
  188. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  189. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  190. package/pkg/dist/modes/interactive/theme/themes.js +27 -26
  191. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  192. package/pkg/package.json +1 -1
  193. package/src/resources/extensions/gsd/auto/loop-deps.ts +9 -5
  194. package/src/resources/extensions/gsd/auto/loop.ts +113 -9
  195. package/src/resources/extensions/gsd/auto/phases.ts +144 -19
  196. package/src/resources/extensions/gsd/auto/run-unit.ts +69 -4
  197. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  198. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +82 -0
  199. package/src/resources/extensions/gsd/auto-dashboard.ts +230 -183
  200. package/src/resources/extensions/gsd/auto-dispatch.ts +15 -1
  201. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -1
  202. package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
  203. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  204. package/src/resources/extensions/gsd/auto-start.ts +22 -22
  205. package/src/resources/extensions/gsd/auto-unit-closeout.ts +51 -0
  206. package/src/resources/extensions/gsd/auto-verification.ts +12 -6
  207. package/src/resources/extensions/gsd/auto-worktree.ts +8 -0
  208. package/src/resources/extensions/gsd/auto.ts +295 -75
  209. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -6
  210. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -2
  211. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +5 -8
  212. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +4 -10
  213. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
  214. package/src/resources/extensions/gsd/git-service.ts +2 -0
  215. package/src/resources/extensions/gsd/gsd-db.ts +7 -23
  216. package/src/resources/extensions/gsd/health-widget-core.ts +1 -1
  217. package/src/resources/extensions/gsd/health-widget.ts +6 -10
  218. package/src/resources/extensions/gsd/journal.ts +2 -0
  219. package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
  220. package/src/resources/extensions/gsd/native-git-bridge.ts +14 -13
  221. package/src/resources/extensions/gsd/notification-overlay.ts +50 -46
  222. package/src/resources/extensions/gsd/parallel-merge.ts +61 -34
  223. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +33 -35
  224. package/src/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  225. package/src/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  226. package/src/resources/extensions/gsd/prompts/discuss.md +20 -2
  227. package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
  228. package/src/resources/extensions/gsd/session-lock.ts +41 -0
  229. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
  230. package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
  231. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
  232. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
  233. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
  234. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
  235. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
  236. package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
  237. package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
  238. package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
  239. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
  240. package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
  241. package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
  242. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
  243. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +654 -176
  245. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +291 -4
  246. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +16 -1
  247. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +18 -0
  248. package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +68 -0
  249. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +28 -1
  250. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +20 -2
  251. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +44 -0
  252. package/src/resources/extensions/gsd/tests/header-renderer.test.ts +40 -0
  253. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +10 -0
  254. package/src/resources/extensions/gsd/tests/health-widget.test.ts +14 -4
  255. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +26 -0
  256. package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
  257. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +116 -24
  258. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -1
  259. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +46 -11
  261. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +78 -41
  262. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +44 -0
  263. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +12 -217
  264. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +38 -6
  265. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +2 -2
  266. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
  267. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +24 -1
  268. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +7 -3
  269. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
  270. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
  271. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +65 -58
  272. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
  273. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -0
  274. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +121 -1
  275. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +66 -0
  276. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +83 -0
  277. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +6 -0
  278. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +158 -58
  279. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +572 -118
  280. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +59 -2
  281. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +18 -0
  282. package/src/resources/extensions/gsd/tui/render-kit.ts +109 -0
  283. package/src/resources/extensions/gsd/watch/header-renderer.ts +121 -79
  284. package/src/resources/extensions/gsd/watch/splash-palette.ts +11 -0
  285. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  286. package/src/resources/extensions/gsd/worktree-lifecycle.ts +1151 -524
  287. package/src/resources/extensions/gsd/worktree-telemetry.ts +7 -2
  288. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +0 -1544
  289. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_buildManifest.js +0 -0
  290. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_ssgManifest.js +0 -0
@@ -1,1544 +0,0 @@
1
- // Project/App: GSD-2
2
- // File Purpose: WorktreeLifecycle merge/exit unit and regression tests.
3
- //
4
- // These tests previously exercised WorktreeResolver, which retired in slice 7
5
- // step E of ADR-016. The merge bodies now live on WorktreeLifecycle. The
6
- // `makeResolver()` shim below preserves the old void/throw caller shape so the
7
- // existing test bodies and `assert.throws(...)` assertions migrate verbatim
8
- // without a rewrite of every call site.
9
- import test from "node:test";
10
- import assert from "node:assert/strict";
11
- import { mkdtempSync, rmSync, mkdirSync, realpathSync } from "node:fs";
12
- import { tmpdir } from "node:os";
13
- import { join } from "node:path";
14
- import {
15
- WorktreeLifecycle,
16
- type WorktreeLifecycleDeps,
17
- type NotifyCtx,
18
- } from "../worktree-lifecycle.js";
19
- import { WorktreeStateProjection } from "../worktree-state-projection.js";
20
- import { resolveWorktreeProjectRoot } from "../worktree-root.js";
21
- import { AutoSession } from "../auto/session.js";
22
-
23
- /**
24
- * Test-local type that extends WorktreeLifecycleDeps with the three fields
25
- * that lived on the retired LegacyTestDeps but never made it into
26
- * Lifecycle's narrower dep set. Tests can still record/override them; the
27
- * Lifecycle constructor ignores them via structural typing.
28
- */
29
- type LegacyTestDeps = WorktreeLifecycleDeps & {
30
- shouldUseWorktreeIsolation?: () => boolean;
31
- syncWorktreeStateBack?: (
32
- mainBasePath: string,
33
- worktreePath: string,
34
- milestoneId: string,
35
- ) => { synced: string[] };
36
- captureIntegrationBranch?: (basePath: string, mid: string | undefined) => void;
37
- };
38
-
39
- /**
40
- * Shim factory preserving the legacy WorktreeResolver public shape for tests.
41
- * Wraps a fresh WorktreeLifecycle and converts the typed-result API back to
42
- * the old `void` / throw shape so test bodies migrate verbatim.
43
- */
44
- function makeResolver(s: AutoSession, deps: LegacyTestDeps) {
45
- const lifecycle = new WorktreeLifecycle(s, deps);
46
- return {
47
- get workPath(): string {
48
- return s.basePath;
49
- },
50
- get projectRoot(): string {
51
- return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
52
- },
53
- get lockPath(): string {
54
- return resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
55
- },
56
- enterMilestone: (mid: string, ctx: NotifyCtx) =>
57
- lifecycle.enterMilestone(mid, ctx),
58
- exitMilestone: (
59
- mid: string,
60
- ctx: NotifyCtx,
61
- opts?: { preserveBranch?: boolean },
62
- ): void => {
63
- const r = lifecycle.exitMilestone(
64
- mid,
65
- { merge: false, preserveBranch: opts?.preserveBranch },
66
- ctx,
67
- );
68
- if (!r.ok && r.cause instanceof Error) throw r.cause;
69
- },
70
- mergeAndExit: (mid: string, ctx: NotifyCtx): void => {
71
- const r = lifecycle.exitMilestone(mid, { merge: true }, ctx);
72
- if (!r.ok && r.cause instanceof Error) throw r.cause;
73
- },
74
- mergeAndEnterNext: (
75
- currentMilestoneId: string,
76
- nextMilestoneId: string,
77
- ctx: NotifyCtx,
78
- ): void => {
79
- lifecycle.mergeAndEnterNext(currentMilestoneId, nextMilestoneId, ctx);
80
- },
81
- };
82
- }
83
- import {
84
- closeDatabase,
85
- insertMilestone,
86
- openDatabase,
87
- } from "../gsd-db.js";
88
- import { registerAutoWorker } from "../db/auto-workers.js";
89
- import {
90
- claimMilestoneLease,
91
- getMilestoneLease,
92
- releaseMilestoneLease,
93
- } from "../db/milestone-leases.js";
94
-
95
- // ─── Helpers ─────────────────────────────────────────────────────────────────
96
-
97
- /** Track calls to mock deps for assertion. */
98
- interface CallLog {
99
- fn: string;
100
- args: unknown[];
101
- }
102
-
103
- function makeSession(
104
- overrides?: Partial<AutoSession>,
105
- ): AutoSession {
106
- const s = new AutoSession();
107
- s.basePath = overrides?.basePath ?? "/project";
108
- s.originalBasePath = overrides?.originalBasePath ?? "/project";
109
- Object.assign(s, overrides);
110
- return s;
111
- }
112
-
113
- function makeDeps(
114
- overrides?: Partial<LegacyTestDeps>,
115
- ): LegacyTestDeps & { calls: CallLog[] } {
116
- const calls: CallLog[] = [];
117
-
118
- const deps: LegacyTestDeps & { calls: CallLog[] } = {
119
- calls,
120
- isInAutoWorktree: (basePath: string) => {
121
- calls.push({ fn: "isInAutoWorktree", args: [basePath] });
122
- return false;
123
- },
124
- shouldUseWorktreeIsolation: () => {
125
- calls.push({ fn: "shouldUseWorktreeIsolation", args: [] });
126
- return true;
127
- },
128
- getIsolationMode: () => {
129
- calls.push({ fn: "getIsolationMode", args: [] });
130
- return "worktree";
131
- },
132
- mergeMilestoneToMain: (
133
- basePath: string,
134
- milestoneId: string,
135
- roadmapContent: string,
136
- ) => {
137
- calls.push({
138
- fn: "mergeMilestoneToMain",
139
- args: [basePath, milestoneId, roadmapContent],
140
- });
141
- return { pushed: false, codeFilesChanged: true };
142
- },
143
- syncWorktreeStateBack: (
144
- mainBasePath: string,
145
- worktreePath: string,
146
- milestoneId: string,
147
- ) => {
148
- calls.push({
149
- fn: "syncWorktreeStateBack",
150
- args: [mainBasePath, worktreePath, milestoneId],
151
- });
152
- return { synced: [] };
153
- },
154
- teardownAutoWorktree: (
155
- basePath: string,
156
- milestoneId: string,
157
- opts?: { preserveBranch?: boolean },
158
- ) => {
159
- calls.push({
160
- fn: "teardownAutoWorktree",
161
- args: [basePath, milestoneId, opts],
162
- });
163
- },
164
- createAutoWorktree: (basePath: string, milestoneId: string) => {
165
- calls.push({ fn: "createAutoWorktree", args: [basePath, milestoneId] });
166
- return `/project/.gsd/worktrees/${milestoneId}`;
167
- },
168
- enterAutoWorktree: (basePath: string, milestoneId: string) => {
169
- calls.push({ fn: "enterAutoWorktree", args: [basePath, milestoneId] });
170
- return `/project/.gsd/worktrees/${milestoneId}`;
171
- },
172
- getAutoWorktreePath: (basePath: string, milestoneId: string) => {
173
- calls.push({ fn: "getAutoWorktreePath", args: [basePath, milestoneId] });
174
- return null;
175
- },
176
- autoCommitCurrentBranch: (
177
- basePath: string,
178
- reason: string,
179
- milestoneId: string,
180
- ) => {
181
- calls.push({
182
- fn: "autoCommitCurrentBranch",
183
- args: [basePath, reason, milestoneId],
184
- });
185
- },
186
- getCurrentBranch: (basePath: string) => {
187
- calls.push({ fn: "getCurrentBranch", args: [basePath] });
188
- return "main";
189
- },
190
- checkoutBranch: (basePath: string, branch: string) => {
191
- calls.push({ fn: "checkoutBranch", args: [basePath, branch] });
192
- },
193
- autoWorktreeBranch: (milestoneId: string) => {
194
- calls.push({ fn: "autoWorktreeBranch", args: [milestoneId] });
195
- return `milestone/${milestoneId}`;
196
- },
197
- resolveMilestoneFile: (
198
- basePath: string,
199
- milestoneId: string,
200
- fileType: string,
201
- ) => {
202
- calls.push({
203
- fn: "resolveMilestoneFile",
204
- args: [basePath, milestoneId, fileType],
205
- });
206
- return `/project/.gsd/milestones/${milestoneId}/${milestoneId}-ROADMAP.md`;
207
- },
208
- readFileSync: (path: string, _encoding: string) => {
209
- calls.push({ fn: "readFileSync", args: [path] });
210
- return "# Roadmap\n- [x] S01: Slice one\n";
211
- },
212
- GitServiceImpl: class MockGitServiceImpl {
213
- basePath: string;
214
- gitConfig: unknown;
215
- constructor(basePath: string, gitConfig: unknown) {
216
- calls.push({ fn: "GitServiceImpl", args: [basePath, gitConfig] });
217
- this.basePath = basePath;
218
- this.gitConfig = gitConfig;
219
- }
220
- } as unknown as LegacyTestDeps["GitServiceImpl"],
221
- loadEffectiveGSDPreferences: () => {
222
- calls.push({ fn: "loadEffectiveGSDPreferences", args: [] });
223
- return { preferences: { git: {} } };
224
- },
225
- invalidateAllCaches: () => {
226
- calls.push({ fn: "invalidateAllCaches", args: [] });
227
- },
228
- captureIntegrationBranch: (
229
- basePath: string,
230
- mid: string | undefined,
231
- ) => {
232
- calls.push({
233
- fn: "captureIntegrationBranch",
234
- args: [basePath, mid],
235
- });
236
- },
237
- enterBranchModeForMilestone: (basePath: string, milestoneId: string) => {
238
- calls.push({ fn: "enterBranchModeForMilestone", args: [basePath, milestoneId] });
239
- },
240
- worktreeProjection: new WorktreeStateProjection(),
241
- ...overrides,
242
- };
243
-
244
- // Re-apply overrides that add the call tracking
245
- if (overrides) {
246
- for (const [key, val] of Object.entries(overrides)) {
247
- if (key !== "calls") {
248
- (deps as unknown as Record<string, unknown>)[key] = val;
249
- }
250
- }
251
- }
252
-
253
- return deps;
254
- }
255
-
256
- function makeNotifyCtx(): NotifyCtx & {
257
- messages: Array<{ msg: string; level?: string }>;
258
- } {
259
- const messages: Array<{ msg: string; level?: string }> = [];
260
- return {
261
- messages,
262
- notify: (msg: string, level?: "info" | "warning" | "error" | "success") => {
263
- messages.push({ msg, level });
264
- },
265
- };
266
- }
267
-
268
- function findCalls(calls: CallLog[], fn: string): CallLog[] {
269
- return calls.filter((c) => c.fn === fn);
270
- }
271
-
272
- function makeDbBase(): string {
273
- const base = mkdtempSync(join(tmpdir(), "gsd-worktree-resolver-"));
274
- mkdirSync(join(base, ".gsd"), { recursive: true });
275
- return base;
276
- }
277
-
278
- function cleanupDbBase(base: string): void {
279
- try { closeDatabase(); } catch { /* noop */ }
280
- try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
281
- }
282
-
283
- // ─── Getter Tests ────────────────────────────────────────────────────────────
284
-
285
- test("workPath returns s.basePath", () => {
286
- const s = makeSession({ basePath: "/project/.gsd/worktrees/M001" });
287
- const resolver = makeResolver(s,makeDeps());
288
- assert.equal(resolver.workPath, "/project/.gsd/worktrees/M001");
289
- });
290
-
291
- test("projectRoot returns originalBasePath when set", () => {
292
- const s = makeSession({
293
- basePath: "/project/.gsd/worktrees/M001",
294
- originalBasePath: "/project",
295
- });
296
- const resolver = makeResolver(s,makeDeps());
297
- assert.equal(resolver.projectRoot, "/project");
298
- });
299
-
300
- test("projectRoot falls back to basePath when originalBasePath is empty", () => {
301
- const s = makeSession({ basePath: "/project", originalBasePath: "" });
302
- const resolver = makeResolver(s,makeDeps());
303
- assert.equal(resolver.projectRoot, "/project");
304
- });
305
-
306
- test("lockPath returns originalBasePath when set (same as lockBase)", () => {
307
- const s = makeSession({
308
- basePath: "/project/.gsd/worktrees/M001",
309
- originalBasePath: "/project",
310
- });
311
- const resolver = makeResolver(s,makeDeps());
312
- assert.equal(resolver.lockPath, "/project");
313
- });
314
-
315
- test("lockPath falls back to basePath when originalBasePath is empty", () => {
316
- const s = makeSession({ basePath: "/project", originalBasePath: "" });
317
- const resolver = makeResolver(s,makeDeps());
318
- assert.equal(resolver.lockPath, "/project");
319
- });
320
-
321
- // ─── enterMilestone Tests ────────────────────────────────────────────────────
322
-
323
- test("enterMilestone creates new worktree when none exists", () => {
324
- const s = makeSession();
325
- const deps = makeDeps({
326
- getAutoWorktreePath: () => null,
327
- });
328
- const ctx = makeNotifyCtx();
329
- const resolver = makeResolver(s,deps);
330
-
331
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
332
-
333
- assert.equal(s.basePath, "/project/.gsd/worktrees/M001");
334
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 1);
335
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
336
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
337
- assert.ok(
338
- ctx.messages.some(
339
- (m) => m.level === "info" && m.msg.includes("Entered worktree"),
340
- ),
341
- );
342
- });
343
-
344
- test("enterMilestone enters existing worktree instead of creating", () => {
345
- const s = makeSession();
346
- const deps = makeDeps({
347
- getAutoWorktreePath: () => "/project/.gsd/worktrees/M001",
348
- });
349
- const ctx = makeNotifyCtx();
350
- const resolver = makeResolver(s,deps);
351
-
352
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
353
-
354
- assert.equal(s.basePath, "/project/.gsd/worktrees/M001");
355
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 1);
356
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
357
- });
358
-
359
- test("enterMilestone is no-op when isolation mode is none", () => {
360
- const s = makeSession();
361
- const deps = makeDeps({
362
- getIsolationMode: () => "none",
363
- });
364
- const ctx = makeNotifyCtx();
365
- const resolver = makeResolver(s,deps);
366
-
367
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
368
-
369
- assert.equal(s.basePath, "/project"); // unchanged
370
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
371
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
372
- assert.equal(findCalls(deps.calls, "enterBranchModeForMilestone").length, 0);
373
- });
374
-
375
- test("enterMilestone passes project root to isolation mode guard", () => {
376
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
377
- let checkedBasePath: string | undefined;
378
- const deps = makeDeps({
379
- getIsolationMode: (basePath?: string) => {
380
- checkedBasePath = basePath;
381
- return "none";
382
- },
383
- });
384
- const ctx = makeNotifyCtx();
385
- const resolver = makeResolver(s,deps);
386
-
387
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
388
-
389
- assert.equal(checkedBasePath, "/project");
390
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
391
- });
392
-
393
- test("enterMilestone does NOT update basePath on creation failure", () => {
394
- const s = makeSession();
395
- const deps = makeDeps({
396
- getAutoWorktreePath: () => null,
397
- createAutoWorktree: () => {
398
- throw new Error("disk full");
399
- },
400
- });
401
- const ctx = makeNotifyCtx();
402
- const resolver = makeResolver(s,deps);
403
-
404
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
405
-
406
- assert.equal(s.basePath, "/project"); // unchanged — error recovery
407
- assert.ok(
408
- ctx.messages.some(
409
- (m) => m.level === "warning" && m.msg.includes("disk full"),
410
- ),
411
- );
412
- });
413
-
414
- test("enterMilestone uses originalBasePath as base for worktree ops", () => {
415
- const s = makeSession({
416
- basePath: "/project/.gsd/worktrees/M001",
417
- originalBasePath: "/project",
418
- });
419
- let createdFrom = "";
420
- const deps = makeDeps({
421
- getAutoWorktreePath: () => null,
422
- createAutoWorktree: (basePath: string, _mid: string) => {
423
- createdFrom = basePath;
424
- return "/project/.gsd/worktrees/M002";
425
- },
426
- });
427
- const ctx = makeNotifyCtx();
428
- const resolver = makeResolver(s,deps);
429
-
430
- new WorktreeLifecycle(s, deps).enterMilestone("M002", ctx);
431
-
432
- assert.equal(createdFrom, "/project"); // uses originalBasePath, not current basePath
433
- });
434
-
435
- test("enterMilestone does not create double-nested worktree when originalBasePath is empty and basePath is a worktree path", () => {
436
- // Regression test for #3729: when s.originalBasePath is "" (falsy) and
437
- // s.basePath is already a worktree path, the expression
438
- // `this.s.originalBasePath || this.s.basePath` evaluates to the worktree
439
- // path. Passing that to createAutoWorktree produces a doubly-nested path
440
- // like /project/.gsd/worktrees/M001/.gsd/worktrees/M002.
441
- const wtPath = "/project/.gsd/worktrees/M001";
442
- const s = makeSession({
443
- basePath: wtPath,
444
- originalBasePath: "/project", // will be overwritten below to simulate the bug
445
- });
446
- // Simulate the real bug: originalBasePath is "" (falsy) as it is when AutoSession
447
- // is constructed fresh or reset() is called without auto-start re-setting it.
448
- s.originalBasePath = "";
449
-
450
- let createdFromPath = "";
451
- const deps = makeDeps({
452
- getAutoWorktreePath: () => null,
453
- createAutoWorktree: (basePath: string, _mid: string) => {
454
- createdFromPath = basePath;
455
- return `/project/.gsd/worktrees/M002`;
456
- },
457
- });
458
- const ctx = makeNotifyCtx();
459
- const resolver = makeResolver(s,deps);
460
-
461
- new WorktreeLifecycle(s, deps).enterMilestone("M002", ctx);
462
-
463
- // The path passed to createAutoWorktree must be the project root, NOT the
464
- // worktree path. If it equals wtPath the worktree would be created at
465
- // /project/.gsd/worktrees/M001/.gsd/worktrees/M002 (double-nesting).
466
- assert.ok(
467
- !createdFromPath.includes("/.gsd/worktrees/"),
468
- `createAutoWorktree must be called with project root, got: "${createdFromPath}"`,
469
- );
470
- });
471
-
472
- test("enterMilestone reacquires a released same-milestone lease before worktree entry", (t) => {
473
- const base = makeDbBase();
474
- t.after(() => cleanupDbBase(base));
475
- openDatabase(join(base, ".gsd", "gsd.db"));
476
- insertMilestone({ id: "M001", title: "Test milestone", status: "active" });
477
-
478
- const workerId = registerAutoWorker({ projectRootRealpath: base });
479
- const originalClaim = claimMilestoneLease(workerId, "M001");
480
- assert.equal(originalClaim.ok, true);
481
- if (!originalClaim.ok) throw new Error("expected test lease claim");
482
- assert.equal(releaseMilestoneLease(workerId, "M001", originalClaim.token), true);
483
-
484
- const s = makeSession({
485
- basePath: base,
486
- originalBasePath: base,
487
- workerId,
488
- currentMilestoneId: "M001",
489
- milestoneLeaseToken: originalClaim.token,
490
- });
491
- const deps = makeDeps({
492
- createAutoWorktree: (basePath: string, milestoneId: string) => join(basePath, ".gsd", "worktrees", milestoneId),
493
- });
494
- const ctx = makeNotifyCtx();
495
- const resolver = makeResolver(s,deps);
496
-
497
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
498
-
499
- const row = getMilestoneLease("M001");
500
- assert.ok(row);
501
- assert.equal(row.worker_id, workerId);
502
- assert.equal(row.status, "held");
503
- assert.equal(row.fencing_token, originalClaim.token + 1);
504
- assert.equal(s.milestoneLeaseToken, originalClaim.token + 1);
505
- assert.equal(s.basePath, join(base, ".gsd", "worktrees", "M001"));
506
- assert.equal(ctx.messages.some((m) => m.level === "error"), false);
507
- });
508
-
509
- // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
510
-
511
- test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {
512
- const s = makeSession();
513
- const deps = makeDeps({
514
- getIsolationMode: () => "branch",
515
- });
516
- const ctx = makeNotifyCtx();
517
- const resolver = makeResolver(s,deps);
518
-
519
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
520
-
521
- // Branch mode: no worktree created, basePath unchanged
522
- assert.equal(s.basePath, "/project");
523
- assert.equal(findCalls(deps.calls, "enterBranchModeForMilestone").length, 1);
524
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
525
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
526
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
527
- assert.ok(ctx.messages.some((m) => m.level === "info" && m.msg.includes("milestone/M001")));
528
- });
529
-
530
- test("enterMilestone in branch mode uses originalBasePath as base", () => {
531
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
532
- let calledWith = "";
533
- const deps = makeDeps({
534
- getIsolationMode: () => "branch",
535
- enterBranchModeForMilestone: (basePath: string, _mid: string) => {
536
- calledWith = basePath;
537
- },
538
- });
539
- const ctx = makeNotifyCtx();
540
- const resolver = makeResolver(s,deps);
541
-
542
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
543
-
544
- assert.equal(calledWith, "/project");
545
- });
546
-
547
- test("enterMilestone in branch mode degrades isolation on failure", () => {
548
- const s = makeSession();
549
- const deps = makeDeps({
550
- getIsolationMode: () => "branch",
551
- enterBranchModeForMilestone: () => {
552
- throw new Error("checkout failed");
553
- },
554
- });
555
- const ctx = makeNotifyCtx();
556
- const resolver = makeResolver(s,deps);
557
-
558
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
559
-
560
- assert.equal(s.basePath, "/project"); // unchanged
561
- assert.ok(s.isolationDegraded);
562
- assert.ok(ctx.messages.some((m) => m.level === "warning" && m.msg.includes("checkout failed")));
563
- });
564
-
565
- test("enterMilestone branch mode is skipped when isolationDegraded", () => {
566
- const s = makeSession();
567
- s.isolationDegraded = true;
568
- const deps = makeDeps({
569
- getIsolationMode: () => "branch",
570
- });
571
- const ctx = makeNotifyCtx();
572
- const resolver = makeResolver(s,deps);
573
-
574
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
575
-
576
- assert.equal(findCalls(deps.calls, "enterBranchModeForMilestone").length, 0);
577
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
578
- });
579
-
580
- // ─── exitMilestone Tests ─────────────────────────────────────────────────────
581
-
582
- test("exitMilestone commits, tears down, and resets basePath", () => {
583
- const s = makeSession({
584
- basePath: "/project/.gsd/worktrees/M001",
585
- originalBasePath: "/project",
586
- });
587
- const deps = makeDeps({
588
- isInAutoWorktree: () => true,
589
- });
590
- const ctx = makeNotifyCtx();
591
- const resolver = makeResolver(s,deps);
592
-
593
- resolver.exitMilestone("M001", ctx);
594
-
595
- assert.equal(s.basePath, "/project"); // reset to originalBasePath
596
- assert.equal(findCalls(deps.calls, "autoCommitCurrentBranch").length, 1);
597
- assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 1);
598
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1); // rebuilt
599
- assert.equal(findCalls(deps.calls, "invalidateAllCaches").length, 1);
600
- });
601
-
602
- test("exitMilestone moves cwd to project root before teardown", (t) => {
603
- const originalCwd = process.cwd();
604
- const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-exit-cwd-")));
605
- const wtPath = join(base, ".gsd", "worktrees", "M001");
606
- mkdirSync(wtPath, { recursive: true });
607
- t.after(() => {
608
- process.chdir(originalCwd);
609
- rmSync(base, { recursive: true, force: true });
610
- });
611
-
612
- process.chdir(wtPath);
613
- const s = makeSession({
614
- basePath: wtPath,
615
- originalBasePath: base,
616
- });
617
- const deps = makeDeps({
618
- isInAutoWorktree: () => true,
619
- });
620
- deps.teardownAutoWorktree = (
621
- teardownBasePath: string,
622
- milestoneId: string,
623
- opts?: { preserveBranch?: boolean },
624
- ) => {
625
- deps.calls.push({ fn: "teardownAutoWorktree", args: [teardownBasePath, milestoneId, opts] });
626
- assert.equal(process.cwd(), base);
627
- rmSync(wtPath, { recursive: true, force: true });
628
- };
629
- const ctx = makeNotifyCtx();
630
- const resolver = makeResolver(s,deps);
631
-
632
- resolver.exitMilestone("M001", ctx);
633
-
634
- assert.equal(process.cwd(), base);
635
- assert.equal(s.basePath, base);
636
- });
637
-
638
- test("exitMilestone is no-op when not in worktree", () => {
639
- const s = makeSession();
640
- const deps = makeDeps({
641
- isInAutoWorktree: () => false,
642
- });
643
- const ctx = makeNotifyCtx();
644
- const resolver = makeResolver(s,deps);
645
-
646
- resolver.exitMilestone("M001", ctx);
647
-
648
- assert.equal(s.basePath, "/project"); // unchanged
649
- assert.equal(findCalls(deps.calls, "autoCommitCurrentBranch").length, 0);
650
- assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
651
- });
652
-
653
- test("exitMilestone passes preserveBranch option", () => {
654
- const s = makeSession({
655
- basePath: "/project/.gsd/worktrees/M001",
656
- originalBasePath: "/project",
657
- });
658
- let preserveOpts: unknown = null;
659
- const deps = makeDeps({
660
- isInAutoWorktree: () => true,
661
- teardownAutoWorktree: (
662
- _basePath: string,
663
- _mid: string,
664
- opts?: { preserveBranch?: boolean },
665
- ) => {
666
- preserveOpts = opts;
667
- },
668
- });
669
- const ctx = makeNotifyCtx();
670
- const resolver = makeResolver(s,deps);
671
-
672
- resolver.exitMilestone("M001", ctx, { preserveBranch: true });
673
-
674
- assert.deepEqual(preserveOpts, { preserveBranch: true });
675
- });
676
-
677
- test("exitMilestone still resets basePath even if auto-commit fails", () => {
678
- const s = makeSession({
679
- basePath: "/project/.gsd/worktrees/M001",
680
- originalBasePath: "/project",
681
- });
682
- const deps = makeDeps({
683
- isInAutoWorktree: () => true,
684
- autoCommitCurrentBranch: () => {
685
- throw new Error("commit error");
686
- },
687
- });
688
- const ctx = makeNotifyCtx();
689
- const resolver = makeResolver(s,deps);
690
-
691
- resolver.exitMilestone("M001", ctx);
692
-
693
- // Should still complete: reset basePath, rebuild git service
694
- assert.equal(s.basePath, "/project");
695
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
696
- });
697
-
698
- // ─── mergeAndExit Tests (worktree mode) ──────────────────────────────────────
699
-
700
- test("mergeAndExit in worktree mode reads roadmap and merges", () => {
701
- const s = makeSession({
702
- basePath: "/project/.gsd/worktrees/M001",
703
- originalBasePath: "/project",
704
- });
705
- const deps = makeDeps({
706
- isInAutoWorktree: () => true,
707
- getIsolationMode: () => "worktree",
708
- });
709
- const ctx = makeNotifyCtx();
710
- const resolver = makeResolver(s,deps);
711
-
712
- resolver.mergeAndExit("M001", ctx);
713
-
714
- // ADR-016 / slice 7 step D: the worktree → root state flow moved from the
715
- // injected deps.syncWorktreeStateBack to WorktreeStateProjection
716
- // .finalizeProjectionForMerge inside WorktreeLifecycle. The remaining
717
- // assertions still cover the merge behaviour end-to-end.
718
- assert.equal(findCalls(deps.calls, "resolveMilestoneFile").length, 1);
719
- assert.equal(findCalls(deps.calls, "readFileSync").length, 1);
720
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
721
- assert.equal(s.basePath, "/project"); // restored
722
- assert.ok(ctx.messages.some((m) => m.msg.includes("merged to main")));
723
- });
724
-
725
- test("mergeAndExit in worktree mode shows pushed status", () => {
726
- const s = makeSession({
727
- basePath: "/project/.gsd/worktrees/M001",
728
- originalBasePath: "/project",
729
- });
730
- const deps = makeDeps({
731
- isInAutoWorktree: () => true,
732
- getIsolationMode: () => "worktree",
733
- mergeMilestoneToMain: () => ({ pushed: true, codeFilesChanged: true }),
734
- });
735
- const ctx = makeNotifyCtx();
736
- const resolver = makeResolver(s,deps);
737
-
738
- resolver.mergeAndExit("M001", ctx);
739
-
740
- assert.ok(ctx.messages.some((m) => m.msg.includes("Pushed to remote")));
741
- });
742
-
743
- test("mergeAndExit falls back to teardown with preserveBranch when roadmap is missing (#1573)", () => {
744
- const s = makeSession({
745
- basePath: "/project/.gsd/worktrees/M001",
746
- originalBasePath: "/project",
747
- });
748
- const deps = makeDeps({
749
- isInAutoWorktree: () => true,
750
- getIsolationMode: () => "worktree",
751
- resolveMilestoneFile: () => null,
752
- });
753
- const ctx = makeNotifyCtx();
754
- const resolver = makeResolver(s,deps);
755
-
756
- resolver.mergeAndExit("M001", ctx);
757
-
758
- const teardownCalls = findCalls(deps.calls, "teardownAutoWorktree");
759
- assert.equal(teardownCalls.length, 1);
760
- // Branch must be preserved so commits are not orphaned (#1573)
761
- assert.deepEqual(teardownCalls[0].args[2], { preserveBranch: true });
762
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
763
- assert.equal(s.basePath, "/project"); // restored
764
- assert.ok(ctx.messages.some((m) => m.msg.includes("branch preserved")));
765
- });
766
-
767
- test("mergeAndExit resolves roadmap from worktree when missing at project root (#1573)", () => {
768
- const s = makeSession({
769
- basePath: "/project/.gsd/worktrees/M001",
770
- originalBasePath: "/project",
771
- });
772
- // resolveMilestoneFile returns null for project root, returns path for worktree
773
- const deps = makeDeps({
774
- isInAutoWorktree: () => true,
775
- getIsolationMode: () => "worktree",
776
- resolveMilestoneFile: (basePath: string) => {
777
- if (basePath === "/project") return null; // missing at project root
778
- if (basePath === "/project/.gsd/worktrees/M001") {
779
- return "/project/.gsd/worktrees/M001/.gsd/milestones/M001/M001-ROADMAP.md";
780
- }
781
- return null;
782
- },
783
- });
784
- const ctx = makeNotifyCtx();
785
- const resolver = makeResolver(s,deps);
786
-
787
- resolver.mergeAndExit("M001", ctx);
788
-
789
- // Should have called mergeMilestoneToMain, not bare teardown
790
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
791
- // #2945 Bug 3: secondary teardown is now called after merge for cleanup
792
- assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 1);
793
- assert.equal(s.basePath, "/project"); // restored
794
- assert.ok(ctx.messages.some((m) => m.msg.includes("merged to main")));
795
- });
796
-
797
- test("mergeAndExit in worktree mode restores to project root on merge failure", () => {
798
- const s = makeSession({
799
- basePath: "/project/.gsd/worktrees/M001",
800
- originalBasePath: "/project",
801
- });
802
- const deps = makeDeps({
803
- isInAutoWorktree: () => true,
804
- getIsolationMode: () => "worktree",
805
- mergeMilestoneToMain: () => {
806
- throw new Error("conflict in main");
807
- },
808
- });
809
- const ctx = makeNotifyCtx();
810
- const resolver = makeResolver(s,deps);
811
-
812
- // Error propagates (#4380) — callers handle recovery. restoreToProjectRoot()
813
- // still runs before re-throw so state is consistent for the caller.
814
- assert.throws(() => resolver.mergeAndExit("M001", ctx), /conflict in main/);
815
-
816
- assert.equal(s.basePath, "/project"); // error recovery — restored before re-throw
817
- assert.ok(
818
- ctx.messages.some(
819
- (m) => m.level === "warning" && m.msg.includes("conflict in main"),
820
- ),
821
- );
822
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1); // rebuilt after recovery
823
- });
824
-
825
- test("mergeAndExit failure message tells user worktree and branch are preserved (#1668)", () => {
826
- // Regression test: before the fix, the failure message was a bare
827
- // "Milestone merge failed: <reason>" with no recovery guidance. Users were
828
- // left confused about whether their code had been deleted. The new message
829
- // explicitly states that the worktree and branch are preserved and what to do.
830
- const s = makeSession({
831
- basePath: "/project/.gsd/worktrees/M001",
832
- originalBasePath: "/project",
833
- });
834
- const deps = makeDeps({
835
- isInAutoWorktree: () => true,
836
- getIsolationMode: () => "worktree",
837
- mergeMilestoneToMain: () => {
838
- throw new Error("pathspec 'main' did not match any file(s) known to git");
839
- },
840
- });
841
- const ctx = makeNotifyCtx();
842
- const resolver = makeResolver(s,deps);
843
-
844
- // Error propagates (#4380) — notification is still emitted before re-throw
845
- assert.throws(() => resolver.mergeAndExit("M001", ctx), /pathspec 'main' did not match/);
846
-
847
- const warning = ctx.messages.find((m) => m.level === "warning");
848
- assert.ok(warning, "a warning message is emitted");
849
- // Must contain the original error
850
- assert.ok(warning!.msg.includes("pathspec 'main' did not match"), "warning includes the original error");
851
- // Must tell the user their work is safe
852
- assert.ok(
853
- warning!.msg.includes("preserved"),
854
- "warning tells user the worktree and branch are preserved",
855
- );
856
- // Must suggest a recovery action
857
- assert.ok(
858
- warning!.msg.includes("retry") || warning!.msg.includes("manually"),
859
- "warning suggests a recovery action",
860
- );
861
- });
862
-
863
- test("mergeAndExit failure message references /gsd dispatch complete-milestone, not /complete-milestone (#1891)", () => {
864
- // Regression test: the failure notification previously told users to
865
- // "retry /complete-milestone" — a command that does not exist. The correct
866
- // recovery command is "/gsd dispatch complete-milestone".
867
- const s = makeSession({
868
- basePath: "/project/.gsd/worktrees/M001",
869
- originalBasePath: "/project",
870
- });
871
- const deps = makeDeps({
872
- isInAutoWorktree: () => true,
873
- getIsolationMode: () => "worktree",
874
- mergeMilestoneToMain: () => {
875
- throw new Error("dirty working tree");
876
- },
877
- });
878
- const ctx = makeNotifyCtx();
879
- const resolver = makeResolver(s,deps);
880
-
881
- // Error propagates (#4380) — notification is still emitted before re-throw
882
- assert.throws(() => resolver.mergeAndExit("M001", ctx), /dirty working tree/);
883
-
884
- const warning = ctx.messages.find((m) => m.level === "warning");
885
- assert.ok(warning, "a warning message is emitted");
886
- // Must reference the correct dispatch command
887
- assert.ok(
888
- warning!.msg.includes("/gsd dispatch complete-milestone"),
889
- "warning references /gsd dispatch complete-milestone, not bare /complete-milestone",
890
- );
891
- // Must NOT contain the bare (incorrect) command without the dispatch prefix
892
- assert.ok(
893
- !warning!.msg.match(/retry\s+\/complete-milestone(?!\S)/),
894
- "warning must not reference the non-existent /complete-milestone command",
895
- );
896
- });
897
-
898
- // ─── mergeAndExit Tests (branch mode) ────────────────────────────────────────
899
-
900
- test("mergeAndExit in branch mode merges when on milestone branch", () => {
901
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
902
- const deps = makeDeps({
903
- isInAutoWorktree: () => false,
904
- getIsolationMode: () => "branch",
905
- getCurrentBranch: () => "milestone/M001",
906
- autoWorktreeBranch: () => "milestone/M001",
907
- });
908
- const ctx = makeNotifyCtx();
909
- const resolver = makeResolver(s,deps);
910
-
911
- resolver.mergeAndExit("M001", ctx);
912
-
913
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
914
- assert.ok(ctx.messages.some((m) => m.msg.includes("branch mode")));
915
- });
916
-
917
- test("mergeAndExit in branch mode checks out the milestone branch and merges (#5538-followup)", () => {
918
- // Regression: previously this case silently returned without merging,
919
- // stranding the milestone's commits on the branch (the test12345 repro).
920
- // The fix forces a checkout first; merge proceeds when checkout succeeds.
921
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
922
- let currentBranch = "main";
923
- const checkoutInvocations: Array<{ basePath: string; branch: string }> = [];
924
- const deps = makeDeps({
925
- isInAutoWorktree: () => false,
926
- getIsolationMode: () => "branch",
927
- getCurrentBranch: () => currentBranch,
928
- autoWorktreeBranch: () => "milestone/M001",
929
- checkoutBranch: (basePath: string, branch: string) => {
930
- checkoutInvocations.push({ basePath, branch });
931
- currentBranch = branch;
932
- },
933
- });
934
- const ctx = makeNotifyCtx();
935
- const resolver = makeResolver(s,deps);
936
-
937
- resolver.mergeAndExit("M001", ctx);
938
-
939
- assert.equal(checkoutInvocations.length, 1, "must attempt checkout when on wrong branch");
940
- assert.deepEqual(checkoutInvocations[0], { basePath: "/project", branch: "milestone/M001" });
941
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1);
942
- });
943
-
944
- test("mergeAndExit in branch mode throws when checkout fails", () => {
945
- // Regression for the silent-skip bug: if the working tree is on the wrong
946
- // branch and checkout fails, we must throw so the caller pauses auto-mode
947
- // — never silently advance with the milestone unmerged.
948
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
949
- const deps = makeDeps({
950
- isInAutoWorktree: () => false,
951
- getIsolationMode: () => "branch",
952
- getCurrentBranch: () => "main",
953
- autoWorktreeBranch: () => "milestone/M001",
954
- checkoutBranch: () => {
955
- throw new Error("dirty working tree blocks checkout");
956
- },
957
- });
958
- const ctx = makeNotifyCtx();
959
- const resolver = makeResolver(s,deps);
960
-
961
- assert.throws(
962
- () => resolver.mergeAndExit("M001", ctx),
963
- /dirty working tree blocks checkout/,
964
- );
965
- assert.equal(
966
- findCalls(deps.calls, "mergeMilestoneToMain").length,
967
- 0,
968
- "merge must not run when checkout failed",
969
- );
970
- const errorNotify = ctx.messages.find((m) => m.level === "error");
971
- assert.ok(errorNotify, "an error notification must be emitted");
972
- assert.match(errorNotify!.msg, /milestone\/M001 failed/);
973
- assert.match(errorNotify!.msg, /Resolve manually/);
974
- assert.equal(
975
- ctx.messages.some((m) => m.level === "warning" && m.msg.includes("Milestone merge failed")),
976
- false,
977
- "checkout failures with explicit recovery guidance must not emit a duplicate warning",
978
- );
979
- });
980
-
981
- test("mergeAndExit in branch mode throws when checkout reports success but HEAD is still wrong", () => {
982
- // Defense in depth: even if checkoutBranch returns without throwing, we
983
- // re-verify and throw if HEAD didn't actually move. Prevents merging on
984
- // top of the wrong branch on platforms where the checkout is a no-op.
985
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
986
- const deps = makeDeps({
987
- isInAutoWorktree: () => false,
988
- getIsolationMode: () => "branch",
989
- getCurrentBranch: () => "main", // never changes — simulates no-op checkout
990
- autoWorktreeBranch: () => "milestone/M001",
991
- checkoutBranch: () => {
992
- // Pretend success — but getCurrentBranch will still return "main".
993
- },
994
- });
995
- const ctx = makeNotifyCtx();
996
- const resolver = makeResolver(s,deps);
997
-
998
- assert.throws(
999
- () => resolver.mergeAndExit("M001", ctx),
1000
- /reported success but current branch is main/,
1001
- );
1002
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1003
- });
1004
-
1005
- test("mergeAndExit in branch mode handles merge failure gracefully", () => {
1006
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
1007
- const deps = makeDeps({
1008
- isInAutoWorktree: () => false,
1009
- getIsolationMode: () => "branch",
1010
- getCurrentBranch: () => "milestone/M001",
1011
- autoWorktreeBranch: () => "milestone/M001",
1012
- mergeMilestoneToMain: () => {
1013
- throw new Error("branch merge conflict");
1014
- },
1015
- });
1016
- const ctx = makeNotifyCtx();
1017
- const resolver = makeResolver(s,deps);
1018
-
1019
- // Error propagates (#4380) — notification is still emitted before re-throw
1020
- assert.throws(() => resolver.mergeAndExit("M001", ctx), /branch merge conflict/);
1021
-
1022
- assert.ok(
1023
- ctx.messages.some(
1024
- (m) => m.level === "warning" && m.msg.includes("branch merge conflict"),
1025
- ),
1026
- );
1027
- });
1028
-
1029
- test("mergeAndExit in branch mode skips when no roadmap", () => {
1030
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
1031
- const deps = makeDeps({
1032
- isInAutoWorktree: () => false,
1033
- getIsolationMode: () => "branch",
1034
- getCurrentBranch: () => "milestone/M001",
1035
- autoWorktreeBranch: () => "milestone/M001",
1036
- resolveMilestoneFile: () => null,
1037
- });
1038
- const ctx = makeNotifyCtx();
1039
- const resolver = makeResolver(s,deps);
1040
-
1041
- resolver.mergeAndExit("M001", ctx);
1042
-
1043
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1044
- });
1045
-
1046
- test("mergeAndExit in branch mode rebuilds GitService after merge", () => {
1047
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
1048
- const deps = makeDeps({
1049
- isInAutoWorktree: () => false,
1050
- getIsolationMode: () => "branch",
1051
- getCurrentBranch: () => "milestone/M001",
1052
- autoWorktreeBranch: () => "milestone/M001",
1053
- });
1054
- const ctx = makeNotifyCtx();
1055
- const resolver = makeResolver(s,deps);
1056
-
1057
- resolver.mergeAndExit("M001", ctx);
1058
-
1059
- assert.equal(findCalls(deps.calls, "GitServiceImpl").length, 1);
1060
- });
1061
-
1062
- // ─── mergeAndExit Tests (none mode) ──────────────────────────────────────────
1063
-
1064
- test("mergeAndExit in none mode is a no-op", () => {
1065
- const s = makeSession();
1066
- const deps = makeDeps({
1067
- getIsolationMode: () => "none",
1068
- });
1069
- const ctx = makeNotifyCtx();
1070
- const resolver = makeResolver(s,deps);
1071
-
1072
- resolver.mergeAndExit("M001", ctx);
1073
-
1074
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1075
- assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
1076
- assert.equal(ctx.messages.length, 0);
1077
- });
1078
-
1079
- // ─── #1906 — metadata-only merge warning ────────────────────────────────────
1080
-
1081
- test("mergeAndExit warns when merge contains no code changes (#1906)", () => {
1082
- const s = makeSession({
1083
- basePath: "/project/.gsd/worktrees/M001",
1084
- originalBasePath: "/project",
1085
- });
1086
- const deps = makeDeps({
1087
- isInAutoWorktree: () => true,
1088
- getIsolationMode: () => "worktree",
1089
- mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
1090
- });
1091
- const ctx = makeNotifyCtx();
1092
- const resolver = makeResolver(s,deps);
1093
-
1094
- resolver.mergeAndExit("M001", ctx);
1095
-
1096
- assert.ok(
1097
- ctx.messages.some((m) => m.msg.includes("NO code changes") && m.level === "warning"),
1098
- "must emit warning when only .gsd/ metadata was merged",
1099
- );
1100
- assert.ok(
1101
- !ctx.messages.some((m) => m.msg.includes("merged to main") && m.level === "info"),
1102
- "must NOT emit success-style info notification for metadata-only merge",
1103
- );
1104
- });
1105
-
1106
- test("mergeAndExit emits info when merge contains code changes (#1906)", () => {
1107
- const s = makeSession({
1108
- basePath: "/project/.gsd/worktrees/M001",
1109
- originalBasePath: "/project",
1110
- });
1111
- const deps = makeDeps({
1112
- isInAutoWorktree: () => true,
1113
- getIsolationMode: () => "worktree",
1114
- mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: true }),
1115
- });
1116
- const ctx = makeNotifyCtx();
1117
- const resolver = makeResolver(s,deps);
1118
-
1119
- resolver.mergeAndExit("M001", ctx);
1120
-
1121
- assert.ok(
1122
- ctx.messages.some((m) => m.msg.includes("merged to main") && m.level === "info"),
1123
- "must emit info notification when code files were merged",
1124
- );
1125
- assert.ok(
1126
- !ctx.messages.some((m) => m.msg.includes("NO code changes")),
1127
- "must NOT emit metadata-only warning when code files were merged",
1128
- );
1129
- });
1130
-
1131
- test("mergeAndExit branch mode warns when merge contains no code changes (#1906)", () => {
1132
- const s = makeSession({
1133
- basePath: "/project",
1134
- originalBasePath: "/project",
1135
- });
1136
- const deps = makeDeps({
1137
- isInAutoWorktree: () => false,
1138
- getIsolationMode: () => "branch",
1139
- getCurrentBranch: () => "milestone/M001",
1140
- autoWorktreeBranch: () => "milestone/M001",
1141
- mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
1142
- });
1143
- const ctx = makeNotifyCtx();
1144
- const resolver = makeResolver(s,deps);
1145
-
1146
- resolver.mergeAndExit("M001", ctx);
1147
-
1148
- assert.ok(
1149
- ctx.messages.some((m) => m.msg.includes("NO code changes") && m.level === "warning"),
1150
- "branch mode must emit warning when only .gsd/ metadata was merged",
1151
- );
1152
- });
1153
-
1154
- // ─── mergeAndEnterNext Tests ─────────────────────────────────────────────────
1155
-
1156
- test("mergeAndEnterNext calls mergeAndExit then enterMilestone", () => {
1157
- const s = makeSession({
1158
- basePath: "/project/.gsd/worktrees/M001",
1159
- originalBasePath: "/project",
1160
- });
1161
- const callOrder: string[] = [];
1162
- const deps = makeDeps({
1163
- isInAutoWorktree: () => true,
1164
- getIsolationMode: () => "worktree",
1165
- shouldUseWorktreeIsolation: () => true,
1166
- mergeMilestoneToMain: (
1167
- basePath: string,
1168
- milestoneId: string,
1169
- _roadmap: string,
1170
- ) => {
1171
- callOrder.push(`merge:${milestoneId}`);
1172
- return { pushed: false, codeFilesChanged: true };
1173
- },
1174
- getAutoWorktreePath: () => null,
1175
- createAutoWorktree: (basePath: string, milestoneId: string) => {
1176
- callOrder.push(`create:${milestoneId}`);
1177
- return `/project/.gsd/worktrees/${milestoneId}`;
1178
- },
1179
- });
1180
- const ctx = makeNotifyCtx();
1181
- const resolver = makeResolver(s,deps);
1182
-
1183
- resolver.mergeAndEnterNext("M001", "M002", ctx);
1184
-
1185
- assert.deepEqual(callOrder, ["merge:M001", "create:M002"]);
1186
- assert.equal(s.basePath, "/project/.gsd/worktrees/M002");
1187
- });
1188
-
1189
- test("mergeAndEnterNext enters next milestone even if merge fails", () => {
1190
- const s = makeSession({
1191
- basePath: "/project/.gsd/worktrees/M001",
1192
- originalBasePath: "/project",
1193
- });
1194
- const deps = makeDeps({
1195
- isInAutoWorktree: (basePath: string) => basePath.includes("worktrees"),
1196
- getIsolationMode: () => "worktree",
1197
- shouldUseWorktreeIsolation: () => true,
1198
- mergeMilestoneToMain: () => {
1199
- throw new Error("merge failed");
1200
- },
1201
- getAutoWorktreePath: () => null,
1202
- createAutoWorktree: (_basePath: string, milestoneId: string) => {
1203
- return `/project/.gsd/worktrees/${milestoneId}`;
1204
- },
1205
- });
1206
- const ctx = makeNotifyCtx();
1207
- const resolver = makeResolver(s,deps);
1208
-
1209
- resolver.mergeAndEnterNext("M001", "M002", ctx);
1210
-
1211
- // Merge failed but enter should still happen
1212
- assert.equal(s.basePath, "/project/.gsd/worktrees/M002");
1213
- assert.ok(
1214
- ctx.messages.some(
1215
- (m) => m.level === "warning" && m.msg.includes("merge failed"),
1216
- ),
1217
- );
1218
- assert.ok(
1219
- ctx.messages.some(
1220
- (m) => m.level === "info" && m.msg.includes("Entered worktree"),
1221
- ),
1222
- );
1223
- });
1224
-
1225
- test("mergeAndEnterNext halts when mergeAndExit preserves branch without merging", () => {
1226
- const s = makeSession({
1227
- basePath: "/project/.gsd/worktrees/M001",
1228
- originalBasePath: "/project",
1229
- });
1230
- const deps = makeDeps({
1231
- isInAutoWorktree: () => true,
1232
- getIsolationMode: () => "worktree",
1233
- shouldUseWorktreeIsolation: () => true,
1234
- resolveMilestoneFile: () => null,
1235
- });
1236
- const ctx = makeNotifyCtx();
1237
- const resolver = makeResolver(s, deps);
1238
-
1239
- assert.throws(
1240
- () => resolver.mergeAndEnterNext("M001", "M002", ctx),
1241
- /Cannot enter milestone M002 because M001 was not merged/,
1242
- );
1243
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1244
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
1245
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
1246
- assert.equal(s.basePath, "/project");
1247
- assert.ok(ctx.messages.some((m) => m.msg.includes("branch preserved")));
1248
- });
1249
-
1250
- test("mergeAndEnterNext halts after branch-mode user-notified checkout failure", () => {
1251
- const s = makeSession({ basePath: "/project", originalBasePath: "/project" });
1252
- const deps = makeDeps({
1253
- isInAutoWorktree: () => false,
1254
- getIsolationMode: () => "branch",
1255
- getCurrentBranch: () => "main",
1256
- autoWorktreeBranch: () => "milestone/M001",
1257
- checkoutBranch: () => {
1258
- throw new Error("dirty working tree blocks checkout");
1259
- },
1260
- });
1261
- const ctx = makeNotifyCtx();
1262
- const resolver = makeResolver(s,deps);
1263
-
1264
- assert.throws(
1265
- () => resolver.mergeAndEnterNext("M001", "M002", ctx),
1266
- /dirty working tree blocks checkout/,
1267
- );
1268
- assert.equal(
1269
- findCalls(deps.calls, "enterBranchModeForMilestone").length,
1270
- 0,
1271
- "must not enter the next milestone after a user-notified branch-mode failure",
1272
- );
1273
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1274
- assert.ok(ctx.messages.some((m) => m.level === "error" && m.msg.includes("Resolve manually")));
1275
- });
1276
-
1277
- // ─── GitService Rebuild Atomicity ────────────────────────────────────────────
1278
-
1279
- test("GitService is rebuilt with the NEW basePath after enterMilestone", () => {
1280
- const s = makeSession();
1281
- let gitServiceBasePath = "";
1282
- const deps = makeDeps({
1283
- getAutoWorktreePath: () => null,
1284
- GitServiceImpl: class {
1285
- constructor(basePath: string, _config: unknown) {
1286
- gitServiceBasePath = basePath;
1287
- }
1288
- } as unknown as LegacyTestDeps["GitServiceImpl"],
1289
- });
1290
- const ctx = makeNotifyCtx();
1291
- const resolver = makeResolver(s,deps);
1292
-
1293
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
1294
-
1295
- assert.equal(gitServiceBasePath, "/project/.gsd/worktrees/M001"); // new path, not old
1296
- });
1297
-
1298
- test("GitService is rebuilt with originalBasePath after exitMilestone", () => {
1299
- const s = makeSession({
1300
- basePath: "/project/.gsd/worktrees/M001",
1301
- originalBasePath: "/project",
1302
- });
1303
- let gitServiceBasePath = "";
1304
- const deps = makeDeps({
1305
- isInAutoWorktree: () => true,
1306
- GitServiceImpl: class {
1307
- constructor(basePath: string, _config: unknown) {
1308
- gitServiceBasePath = basePath;
1309
- }
1310
- } as unknown as LegacyTestDeps["GitServiceImpl"],
1311
- });
1312
- const ctx = makeNotifyCtx();
1313
- const resolver = makeResolver(s,deps);
1314
-
1315
- resolver.exitMilestone("M001", ctx);
1316
-
1317
- assert.equal(gitServiceBasePath, "/project"); // project root, not worktree
1318
- });
1319
-
1320
- // ─── Isolation Degradation Tests (#2483) ──────────────────────────────────
1321
-
1322
- test("enterMilestone sets isolationDegraded when worktree creation throws (#2483)", () => {
1323
- const s = makeSession();
1324
- const deps = makeDeps({
1325
- getAutoWorktreePath: () => null,
1326
- createAutoWorktree: () => {
1327
- throw new Error("empty repo");
1328
- },
1329
- });
1330
- const ctx = makeNotifyCtx();
1331
- const resolver = makeResolver(s,deps);
1332
-
1333
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
1334
-
1335
- assert.equal(s.isolationDegraded, true);
1336
- assert.equal(s.basePath, "/project"); // unchanged — error recovery
1337
- });
1338
-
1339
- test("enterMilestone is no-op when isolationDegraded is true (#2483)", () => {
1340
- const s = makeSession();
1341
- s.isolationDegraded = true;
1342
- const deps = makeDeps();
1343
- const ctx = makeNotifyCtx();
1344
- const resolver = makeResolver(s,deps);
1345
-
1346
- new WorktreeLifecycle(s, deps).enterMilestone("M001", ctx);
1347
-
1348
- assert.equal(s.basePath, "/project"); // unchanged
1349
- assert.equal(findCalls(deps.calls, "createAutoWorktree").length, 0);
1350
- assert.equal(findCalls(deps.calls, "enterAutoWorktree").length, 0);
1351
- assert.equal(findCalls(deps.calls, "shouldUseWorktreeIsolation").length, 0);
1352
- });
1353
-
1354
- test("mergeAndExit is no-op when isolationDegraded is true (#2483)", () => {
1355
- const s = makeSession({
1356
- basePath: "/project",
1357
- originalBasePath: "/project",
1358
- });
1359
- s.isolationDegraded = true;
1360
- const deps = makeDeps({
1361
- getIsolationMode: () => "worktree",
1362
- });
1363
- const ctx = makeNotifyCtx();
1364
- const resolver = makeResolver(s,deps);
1365
-
1366
- resolver.mergeAndExit("M001", ctx);
1367
-
1368
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0);
1369
- assert.equal(findCalls(deps.calls, "teardownAutoWorktree").length, 0);
1370
- assert.equal(findCalls(deps.calls, "getIsolationMode").length, 0);
1371
- assert.ok(
1372
- ctx.messages.some(
1373
- (m) => m.level === "info" && m.msg.includes("isolation was degraded"),
1374
- ),
1375
- );
1376
- });
1377
-
1378
- test("isolationDegraded is reset by session.reset() (#2483)", () => {
1379
- const s = new AutoSession();
1380
- s.isolationDegraded = true;
1381
-
1382
- s.reset();
1383
-
1384
- assert.equal(s.isolationDegraded, false);
1385
- });
1386
-
1387
- // ─── #2625 — Default isolation mode change must not orphan worktree commits ──
1388
-
1389
- test("mergeAndExit still merges when mode is 'none' but session is in a worktree (#2625)", () => {
1390
- // Scenario: user upgraded from a version where default was "worktree" to one
1391
- // where default is "none". They have an active worktree with committed work.
1392
- // mergeAndExit must detect the active worktree and merge regardless of config.
1393
- const s = makeSession({
1394
- basePath: "/project/.gsd/worktrees/M001",
1395
- originalBasePath: "/project",
1396
- });
1397
- const deps = makeDeps({
1398
- isInAutoWorktree: () => true,
1399
- getIsolationMode: () => "none", // config says "none" — but we ARE in a worktree
1400
- });
1401
- const ctx = makeNotifyCtx();
1402
- const resolver = makeResolver(s,deps);
1403
-
1404
- resolver.mergeAndExit("M001", ctx);
1405
-
1406
- // Must still merge — not skip silently
1407
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 1,
1408
- "must call mergeMilestoneToMain even when isolation mode is 'none' but we are in a worktree");
1409
- assert.equal(s.basePath, "/project", "basePath must be restored to project root");
1410
- assert.ok(ctx.messages.some((m) => m.msg.includes("merged to main")),
1411
- "must notify about the merge");
1412
- });
1413
-
1414
- test("mergeAndExit in none mode remains a no-op when NOT in a worktree (#2625)", () => {
1415
- // When mode is "none" and we are genuinely not in a worktree, it should still be a no-op.
1416
- const s = makeSession({
1417
- basePath: "/project",
1418
- originalBasePath: "/project",
1419
- });
1420
- const deps = makeDeps({
1421
- isInAutoWorktree: () => false,
1422
- getIsolationMode: () => "none",
1423
- });
1424
- const ctx = makeNotifyCtx();
1425
- const resolver = makeResolver(s,deps);
1426
-
1427
- resolver.mergeAndExit("M001", ctx);
1428
-
1429
- assert.equal(findCalls(deps.calls, "mergeMilestoneToMain").length, 0,
1430
- "must NOT merge when not in a worktree and mode is none");
1431
- });
1432
-
1433
- // ─── #4380 — Non-MergeConflictError must not be swallowed ────────────────────
1434
-
1435
- test("mergeAndExit propagates non-MergeConflictError to caller (#4380)", () => {
1436
- // Regression test: previously the catch block in _mergeWorktreeMode only
1437
- // re-threw MergeConflictError. Permission errors, filesystem errors, and other
1438
- // non-conflict failures were swallowed silently, making broken states impossible
1439
- // to diagnose and preventing callers (phases.ts) from applying their own
1440
- // error-recovery logic.
1441
- const permissionError = new Error("EACCES: permission denied, open '/project/.git/SQUASH_MSG'");
1442
- const s = makeSession({
1443
- basePath: "/project/.gsd/worktrees/M001",
1444
- originalBasePath: "/project",
1445
- });
1446
- const deps = makeDeps({
1447
- isInAutoWorktree: () => true,
1448
- getIsolationMode: () => "worktree",
1449
- mergeMilestoneToMain: () => {
1450
- throw permissionError;
1451
- },
1452
- });
1453
- const ctx = makeNotifyCtx();
1454
- const resolver = makeResolver(s,deps);
1455
-
1456
- // The error must propagate — callers need it to apply their own recovery logic
1457
- assert.throws(
1458
- () => resolver.mergeAndExit("M001", ctx),
1459
- (err: unknown) => err === permissionError,
1460
- "non-MergeConflictError must propagate to the caller, not be swallowed",
1461
- );
1462
- });
1463
-
1464
- // ─── Regression: mergeAndExit anchors cwd at project root before merge work ─
1465
- // (de73fb43d headless `gsd auto` exits-on-task regression)
1466
- //
1467
- // Background: the auto loop runs tasks inside the milestone worktree
1468
- // (process.cwd() === worktreePath). When the milestone completes, the
1469
- // worktree dir is torn down. If cwd was still inside it at that moment,
1470
- // every subsequent process.cwd() throws ENOENT — and after de73fb43d
1471
- // auto/run-unit.ts:50 turns that ENOENT into a session-failed cancel,
1472
- // which in headless mode bubbles up to a "Auto-mode stopped" notify
1473
- // and process.exit(0). mergeAndExit must therefore guarantee cwd is
1474
- // anchored at the project root regardless of which merge path runs.
1475
-
1476
- test("mergeAndExit chdirs to project root before merge work (regression: headless gsd auto exit)", () => {
1477
- // Set up real dirs so process.chdir actually succeeds. realpathSync
1478
- // canonicalizes the macOS /var → /private/var symlink so equality holds.
1479
- const projectRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-resolver-cwd-")));
1480
- const worktreePath = join(projectRoot, ".gsd/worktrees/M001");
1481
- mkdirSync(worktreePath, { recursive: true });
1482
- const previousCwd = process.cwd();
1483
-
1484
- try {
1485
- process.chdir(worktreePath);
1486
- assert.equal(process.cwd(), worktreePath, "precondition: cwd is in worktree");
1487
-
1488
- const s = makeSession({
1489
- basePath: worktreePath,
1490
- originalBasePath: projectRoot,
1491
- });
1492
- const deps = makeDeps({
1493
- isInAutoWorktree: () => true,
1494
- getIsolationMode: () => "worktree",
1495
- });
1496
- const ctx = makeNotifyCtx();
1497
- const resolver = makeResolver(s,deps);
1498
-
1499
- resolver.mergeAndExit("M001", ctx);
1500
-
1501
- assert.equal(
1502
- process.cwd(),
1503
- projectRoot,
1504
- "mergeAndExit must leave cwd at the project root, not the (about-to-be-removed) worktree",
1505
- );
1506
- } finally {
1507
- try { process.chdir(previousCwd); } catch { /* best-effort */ }
1508
- rmSync(projectRoot, { recursive: true, force: true });
1509
- }
1510
- });
1511
-
1512
- test("mergeAndExit anchors cwd even on isolation-degraded skip path", () => {
1513
- // The skip paths (isolation-degraded, mode-none, missing-original-base)
1514
- // bypass the per-mode merge helpers entirely. They must still leave cwd
1515
- // at the project root so a subsequent worktree teardown elsewhere does
1516
- // not strand cwd in a deleted dir.
1517
- const projectRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-resolver-cwd-degraded-")));
1518
- const worktreePath = join(projectRoot, ".gsd/worktrees/M001");
1519
- mkdirSync(worktreePath, { recursive: true });
1520
- const previousCwd = process.cwd();
1521
-
1522
- try {
1523
- process.chdir(worktreePath);
1524
- const s = makeSession({
1525
- basePath: worktreePath,
1526
- originalBasePath: projectRoot,
1527
- });
1528
- s.isolationDegraded = true;
1529
- const deps = makeDeps({ getIsolationMode: () => "worktree" });
1530
- const ctx = makeNotifyCtx();
1531
- const resolver = makeResolver(s,deps);
1532
-
1533
- resolver.mergeAndExit("M001", ctx);
1534
-
1535
- assert.equal(
1536
- process.cwd(),
1537
- projectRoot,
1538
- "isolation-degraded skip must still anchor cwd at project root",
1539
- );
1540
- } finally {
1541
- try { process.chdir(previousCwd); } catch { /* best-effort */ }
1542
- rmSync(projectRoot, { recursive: true, force: true });
1543
- }
1544
- });