gsd-pi 2.44.0 → 2.45.0-dev.6b9da3e

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 (463) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/activity-log.js +7 -0
  3. package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
  4. package/dist/resources/extensions/gsd/auto/phases.js +37 -36
  5. package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
  6. package/dist/resources/extensions/gsd/auto-start.js +31 -2
  7. package/dist/resources/extensions/gsd/auto-timers.js +57 -3
  8. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
  9. package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
  10. package/dist/resources/extensions/gsd/auto.js +30 -3
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
  12. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
  13. package/dist/resources/extensions/gsd/commands/catalog.js +7 -1
  14. package/dist/resources/extensions/gsd/commands/handlers/core.js +2 -0
  15. package/dist/resources/extensions/gsd/commands/handlers/ops.js +10 -0
  16. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  17. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  18. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  19. package/dist/resources/extensions/gsd/doctor.js +8 -0
  20. package/dist/resources/extensions/gsd/git-service.js +8 -3
  21. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  22. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  23. package/dist/resources/extensions/gsd/preferences.js +9 -1
  24. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  25. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  27. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  28. package/dist/resources/extensions/gsd/prompts/rethink.md +78 -0
  29. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  30. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  31. package/dist/resources/extensions/gsd/repo-identity.js +45 -7
  32. package/dist/resources/extensions/gsd/rethink.js +115 -0
  33. package/dist/resources/extensions/gsd/state.js +41 -3
  34. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  35. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  36. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  37. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  38. package/dist/resources/extensions/gsd/worktree-manager.js +32 -2
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  40. package/dist/resources/extensions/mcp-client/index.js +14 -0
  41. package/dist/web/standalone/.next/BUILD_ID +1 -1
  42. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  43. package/dist/web/standalone/.next/build-manifest.json +4 -4
  44. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  45. package/dist/web/standalone/.next/react-loadable-manifest.json +2 -2
  46. package/dist/web/standalone/.next/required-server-files.json +3 -3
  47. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  48. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  50. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  58. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.rsc +5 -5
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +5 -5
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +4 -4
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  67. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  74. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  89. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  95. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +5 -5
  112. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  118. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  124. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  125. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  126. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  127. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  128. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  129. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  130. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  132. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  134. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  135. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  136. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  138. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  140. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  141. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  142. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  143. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  144. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  145. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  146. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  147. package/dist/web/standalone/.next/server/app/index.html +1 -1
  148. package/dist/web/standalone/.next/server/app/index.rsc +6 -6
  149. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  150. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +6 -6
  151. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  152. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +4 -4
  153. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  154. package/dist/web/standalone/.next/server/app/page.js +2 -2
  155. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  156. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  157. package/dist/web/standalone/.next/server/chunks/229.js +1 -1
  158. package/dist/web/standalone/.next/server/chunks/471.js +3 -3
  159. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  160. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  161. package/dist/web/standalone/.next/server/middleware.js +2 -2
  162. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  163. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  164. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  165. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  166. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  167. package/dist/web/standalone/.next/static/chunks/4024.11ca5c01938e5948.js +9 -0
  168. package/dist/web/standalone/.next/static/chunks/{3721.bf31263de6d5fa46.js → 485.243af25f0cdf50d6.js} +2 -2
  169. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
  170. package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
  171. package/dist/web/standalone/.next/static/chunks/app/page-6654a8cca61a3d1c.js +1 -0
  172. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
  173. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
  174. package/dist/web/standalone/.next/static/chunks/webpack-0a4cd455ec4197d2.js +1 -0
  175. package/dist/web/standalone/.next/static/css/dd4ae3f58ac9b600.css +1 -0
  176. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  177. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  178. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  179. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  180. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  181. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  182. package/dist/web/standalone/server.js +1 -1
  183. package/package.json +1 -1
  184. package/packages/native/dist/stream-process/index.js +2 -2
  185. package/packages/native/src/__tests__/stream-process.test.mjs +34 -0
  186. package/packages/native/src/stream-process/index.ts +2 -2
  187. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  188. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  189. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  190. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  191. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  192. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  194. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  195. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  196. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  197. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  198. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  199. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  200. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  201. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  202. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  203. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  204. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  205. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  206. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  207. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  208. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  209. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  210. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  211. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  212. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  213. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  214. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  215. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  216. package/packages/pi-coding-agent/dist/main.js +17 -0
  217. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  218. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  219. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  220. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  221. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  222. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  223. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  224. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  225. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  226. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  227. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  228. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  229. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  230. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  231. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  232. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  233. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  234. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  235. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  236. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  237. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  238. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  239. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  240. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  241. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  242. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  243. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  244. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  245. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  246. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  247. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  248. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  249. package/packages/pi-coding-agent/package.json +1 -1
  250. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  251. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  252. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  253. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  254. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  255. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  256. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  257. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  258. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  259. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  260. package/packages/pi-coding-agent/src/main.ts +19 -0
  261. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  262. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  263. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  264. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  265. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  266. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  267. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  268. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
  269. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  270. package/pkg/package.json +1 -1
  271. package/src/resources/extensions/gsd/activity-log.ts +1 -0
  272. package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
  273. package/src/resources/extensions/gsd/auto/phases.ts +46 -48
  274. package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
  275. package/src/resources/extensions/gsd/auto-start.ts +39 -2
  276. package/src/resources/extensions/gsd/auto-timers.ts +64 -3
  277. package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
  278. package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
  279. package/src/resources/extensions/gsd/auto.ts +37 -3
  280. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
  281. package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
  282. package/src/resources/extensions/gsd/commands/catalog.ts +7 -1
  283. package/src/resources/extensions/gsd/commands/handlers/core.ts +2 -0
  284. package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
  285. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  286. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  287. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  288. package/src/resources/extensions/gsd/doctor.ts +7 -1
  289. package/src/resources/extensions/gsd/git-service.ts +6 -2
  290. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  291. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  292. package/src/resources/extensions/gsd/preferences.ts +11 -1
  293. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  294. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  295. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  296. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  297. package/src/resources/extensions/gsd/prompts/rethink.md +78 -0
  298. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  299. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  300. package/src/resources/extensions/gsd/repo-identity.ts +46 -7
  301. package/src/resources/extensions/gsd/rethink.ts +154 -0
  302. package/src/resources/extensions/gsd/state.ts +41 -1
  303. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  304. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  305. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  306. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  307. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  308. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  309. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  310. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  311. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  312. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  313. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  314. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  315. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  316. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  317. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  318. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  319. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  320. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  321. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  322. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  323. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  324. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  325. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  326. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  327. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  328. package/src/resources/extensions/gsd/tests/db-writer.test.ts +465 -416
  329. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  330. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  331. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +121 -0
  332. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +210 -181
  333. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  334. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  335. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  336. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  337. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  338. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  339. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  340. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  341. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  342. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  343. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  344. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  345. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  346. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  347. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  348. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  349. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  350. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  351. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  352. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  353. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  354. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  355. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  356. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  357. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  358. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  359. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  360. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  361. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  362. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  363. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  364. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  365. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  366. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  367. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  368. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  369. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  370. package/src/resources/extensions/gsd/tests/inherited-repo-home-dir.test.ts +121 -0
  371. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  372. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  373. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  374. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  375. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  376. package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
  377. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  378. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  379. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  380. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  381. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  382. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  383. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  384. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  385. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  386. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  387. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  388. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  389. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  390. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  391. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  392. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  393. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  394. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  395. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  396. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  397. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  398. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  399. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  400. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  401. package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
  402. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  403. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  404. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  405. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  406. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  407. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  408. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  409. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  410. package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +176 -0
  411. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  412. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  413. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  414. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  415. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  416. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  417. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  418. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  419. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  420. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  421. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  422. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  423. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  424. package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +108 -0
  425. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  426. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  427. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  428. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  429. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +10 -11
  430. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  431. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  432. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  433. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  434. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  435. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  436. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  437. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  438. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  439. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  440. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  441. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  442. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  443. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  444. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  445. package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +65 -0
  446. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  447. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  448. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  449. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  450. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  451. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  452. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  453. package/src/resources/extensions/gsd/worktree-manager.ts +43 -2
  454. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  455. package/src/resources/extensions/mcp-client/index.ts +20 -0
  456. package/dist/web/standalone/.next/static/chunks/4024.0de81b543b28b9fe.js +0 -9
  457. package/dist/web/standalone/.next/static/chunks/app/page-7e9530a7122506c5.js +0 -1
  458. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
  459. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
  460. package/dist/web/standalone/.next/static/chunks/webpack-9014b5adb127a98a.js +0 -1
  461. package/dist/web/standalone/.next/static/css/8a727f372cf53002.css +0 -1
  462. /package/dist/web/standalone/.next/static/{mgkxN0mGP6gSUmGPEzbk_ → rzO54ZboyINyEt7cVM_uS}/_buildManifest.js +0 -0
  463. /package/dist/web/standalone/.next/static/{mgkxN0mGP6gSUmGPEzbk_ → rzO54ZboyINyEt7cVM_uS}/_ssgManifest.js +0 -0
@@ -4,8 +4,14 @@
4
4
  * Covers: squash-merge topology (one commit on main), rich commit message with
5
5
  * slice titles, worktree cleanup, nothing-to-commit edge case, auto-push with
6
6
  * bare remote. All tests use real git operations in temp repos.
7
+ *
8
+ * Note: execSync is used intentionally in these tests for git operations with
9
+ * controlled, hardcoded inputs (no user input). This is safe and necessary for
10
+ * testing real git behavior.
7
11
  */
8
12
 
13
+ import { describe, test, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
9
15
  import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, readFileSync } from "node:fs";
10
16
  import { join } from "node:path";
11
17
  import { tmpdir } from "node:os";
@@ -19,11 +25,8 @@ import {
19
25
  import { getSliceBranchName } from "../worktree.ts";
20
26
  import { nativeMergeSquash } from "../native-git-bridge.ts";
21
27
 
22
- import { createTestContext } from "./test-helpers.ts";
23
-
24
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
25
-
26
28
  function run(cmd: string, cwd: string): string {
29
+ // Safe: all inputs are hardcoded test strings, not user input
27
30
  return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
28
31
  }
29
32
 
@@ -56,7 +59,6 @@ function addSliceToMilestone(
56
59
  sliceTitle: string,
57
60
  commits: Array<{ file: string; content: string; message: string }>,
58
61
  ): void {
59
- // Detect worktree name for branch naming
60
62
  const normalizedPath = wtPath.replaceAll("\\", "/");
61
63
  const marker = "/.gsd/worktrees/";
62
64
  const idx = normalizedPath.indexOf(marker);
@@ -72,11 +74,10 @@ function addSliceToMilestone(
72
74
  }
73
75
  run(`git checkout milestone/${milestoneId}`, wtPath);
74
76
  run(`git merge --no-ff ${sliceBranch} -m "feat(${milestoneId}/${sliceId}): ${sliceTitle}"`, wtPath);
75
- // Clean up the slice branch
76
77
  run(`git branch -d ${sliceBranch}`, wtPath);
77
78
  }
78
79
 
79
- async function main(): Promise<void> {
80
+ describe("auto-worktree-milestone-merge", () => {
80
81
  const savedCwd = process.cwd();
81
82
  const tempDirs: string[] = [];
82
83
 
@@ -86,694 +87,570 @@ async function main(): Promise<void> {
86
87
  return d;
87
88
  }
88
89
 
89
- try {
90
- // ─── Test 1: Basic squash merge — one commit on main ───────────────
91
- console.log("\n=== basic squash merge — one commit on main ===");
92
- {
93
- const repo = freshRepo();
94
- const wtPath = createAutoWorktree(repo, "M010");
95
-
96
- // Add two slices with multiple commits each
97
- addSliceToMilestone(repo, wtPath, "M010", "S01", "Auth module", [
98
- { file: "auth.ts", content: "export const auth = true;\n", message: "add auth" },
99
- { file: "auth-utils.ts", content: "export const hash = () => {};\n", message: "add auth utils" },
100
- ]);
101
- addSliceToMilestone(repo, wtPath, "M010", "S02", "User dashboard", [
102
- { file: "dashboard.ts", content: "export const dash = true;\n", message: "add dashboard" },
103
- { file: "widgets.ts", content: "export const widgets = [];\n", message: "add widgets" },
104
- ]);
105
-
106
- const roadmap = makeRoadmap("M010", "User management", [
107
- { id: "S01", title: "Auth module" },
108
- { id: "S02", title: "User dashboard" },
109
- ]);
110
-
111
- const mainLogBefore = run("git log --oneline main", repo);
112
- const mainCommitCountBefore = mainLogBefore.split("\n").length;
113
-
114
- const result = mergeMilestoneToMain(repo, "M010", roadmap);
115
-
116
- // Exactly one new commit on main
117
- const mainLog = run("git log --oneline main", repo);
118
- const mainCommitCountAfter = mainLog.split("\n").length;
119
- assertEq(mainCommitCountAfter, mainCommitCountBefore + 1, "exactly one new commit on main");
120
-
121
- // Milestone branch deleted
122
- const branches = run("git branch", repo);
123
- assertTrue(!branches.includes("milestone/M010"), "milestone branch deleted");
124
-
125
- // Worktree directory removed
126
- const worktreeDir = join(repo, ".gsd", "worktrees", "M010");
127
- assertTrue(!existsSync(worktreeDir), "worktree directory removed");
128
-
129
- // Module state cleared
130
- assertEq(getAutoWorktreeOriginalBase(), null, "originalBase cleared after merge");
131
-
132
- // Files from both slices present on main
133
- assertTrue(existsSync(join(repo, "auth.ts")), "auth.ts on main");
134
- assertTrue(existsSync(join(repo, "dashboard.ts")), "dashboard.ts on main");
135
- assertTrue(existsSync(join(repo, "widgets.ts")), "widgets.ts on main");
136
-
137
- // Result shape
138
- assertTrue(result.commitMessage.length > 0, "commitMessage returned");
139
- assertTrue(typeof result.pushed === "boolean", "pushed is boolean");
90
+ afterEach(() => {
91
+ process.chdir(savedCwd);
92
+ for (const d of tempDirs) {
93
+ if (existsSync(d)) rmSync(d, { recursive: true, force: true });
140
94
  }
141
-
142
- // ─── Test 2: Rich commit message format ────────────────────────────
143
- console.log("\n=== rich commit message format ===");
144
- {
145
- const repo = freshRepo();
146
- const wtPath = createAutoWorktree(repo, "M020");
147
-
148
- addSliceToMilestone(repo, wtPath, "M020", "S01", "Core API", [
149
- { file: "api.ts", content: "export const api = true;\n", message: "add api" },
150
- ]);
151
- addSliceToMilestone(repo, wtPath, "M020", "S02", "Error handling", [
152
- { file: "errors.ts", content: "export class AppError {}\n", message: "add errors" },
153
- ]);
154
- addSliceToMilestone(repo, wtPath, "M020", "S03", "Logging infra", [
155
- { file: "logger.ts", content: "export const log = () => {};\n", message: "add logger" },
156
- ]);
157
-
158
- const roadmap = makeRoadmap("M020", "Backend foundation", [
159
- { id: "S01", title: "Core API" },
160
- { id: "S02", title: "Error handling" },
161
- { id: "S03", title: "Logging infra" },
162
- ]);
163
-
164
- const result = mergeMilestoneToMain(repo, "M020", roadmap);
165
-
166
- // Subject line: conventional commit format
167
- assertMatch(result.commitMessage, /^feat\(M020\):/, "subject has conventional commit prefix");
168
- assertTrue(result.commitMessage.includes("Backend foundation"), "subject includes milestone title");
169
-
170
- // Body: slice listing
171
- assertTrue(result.commitMessage.includes("- S01: Core API"), "body lists S01");
172
- assertTrue(result.commitMessage.includes("- S02: Error handling"), "body lists S02");
173
- assertTrue(result.commitMessage.includes("- S03: Logging infra"), "body lists S03");
174
-
175
- // Branch metadata
176
- assertTrue(result.commitMessage.includes("Branch: milestone/M020"), "body has branch metadata");
177
-
178
- // Verify the actual git commit message matches
179
- const gitMsg = run("git log -1 --format=%B main", repo).trim();
180
- assertMatch(gitMsg, /^feat\(M020\):/, "git commit message starts with feat(M020):");
181
- assertTrue(gitMsg.includes("- S01: Core API"), "git commit body has S01");
95
+ tempDirs.length = 0;
96
+ });
97
+
98
+ test("basic squash merge — one commit on main", () => {
99
+ const repo = freshRepo();
100
+ const wtPath = createAutoWorktree(repo, "M010");
101
+
102
+ addSliceToMilestone(repo, wtPath, "M010", "S01", "Auth module", [
103
+ { file: "auth.ts", content: "export const auth = true;\n", message: "add auth" },
104
+ { file: "auth-utils.ts", content: "export const hash = () => {};\n", message: "add auth utils" },
105
+ ]);
106
+ addSliceToMilestone(repo, wtPath, "M010", "S02", "User dashboard", [
107
+ { file: "dashboard.ts", content: "export const dash = true;\n", message: "add dashboard" },
108
+ { file: "widgets.ts", content: "export const widgets = [];\n", message: "add widgets" },
109
+ ]);
110
+
111
+ const roadmap = makeRoadmap("M010", "User management", [
112
+ { id: "S01", title: "Auth module" },
113
+ { id: "S02", title: "User dashboard" },
114
+ ]);
115
+
116
+ const mainLogBefore = run("git log --oneline main", repo);
117
+ const mainCommitCountBefore = mainLogBefore.split("\n").length;
118
+
119
+ const result = mergeMilestoneToMain(repo, "M010", roadmap);
120
+
121
+ const mainLog = run("git log --oneline main", repo);
122
+ const mainCommitCountAfter = mainLog.split("\n").length;
123
+ assert.strictEqual(mainCommitCountAfter, mainCommitCountBefore + 1, "exactly one new commit on main");
124
+
125
+ const branches = run("git branch", repo);
126
+ assert.ok(!branches.includes("milestone/M010"), "milestone branch deleted");
127
+
128
+ const worktreeDir = join(repo, ".gsd", "worktrees", "M010");
129
+ assert.ok(!existsSync(worktreeDir), "worktree directory removed");
130
+
131
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "originalBase cleared after merge");
132
+
133
+ assert.ok(existsSync(join(repo, "auth.ts")), "auth.ts on main");
134
+ assert.ok(existsSync(join(repo, "dashboard.ts")), "dashboard.ts on main");
135
+ assert.ok(existsSync(join(repo, "widgets.ts")), "widgets.ts on main");
136
+
137
+ assert.ok(result.commitMessage.length > 0, "commitMessage returned");
138
+ assert.strictEqual(typeof result.pushed, "boolean", "pushed is boolean");
139
+ });
140
+
141
+ test("rich commit message format", () => {
142
+ const repo = freshRepo();
143
+ const wtPath = createAutoWorktree(repo, "M020");
144
+
145
+ addSliceToMilestone(repo, wtPath, "M020", "S01", "Core API", [
146
+ { file: "api.ts", content: "export const api = true;\n", message: "add api" },
147
+ ]);
148
+ addSliceToMilestone(repo, wtPath, "M020", "S02", "Error handling", [
149
+ { file: "errors.ts", content: "export class AppError {}\n", message: "add errors" },
150
+ ]);
151
+ addSliceToMilestone(repo, wtPath, "M020", "S03", "Logging infra", [
152
+ { file: "logger.ts", content: "export const log = () => {};\n", message: "add logger" },
153
+ ]);
154
+
155
+ const roadmap = makeRoadmap("M020", "Backend foundation", [
156
+ { id: "S01", title: "Core API" },
157
+ { id: "S02", title: "Error handling" },
158
+ { id: "S03", title: "Logging infra" },
159
+ ]);
160
+
161
+ const result = mergeMilestoneToMain(repo, "M020", roadmap);
162
+
163
+ assert.match(result.commitMessage, /^feat\(M020\):/, "subject has conventional commit prefix");
164
+ assert.ok(result.commitMessage.includes("Backend foundation"), "subject includes milestone title");
165
+ assert.ok(result.commitMessage.includes("- S01: Core API"), "body lists S01");
166
+ assert.ok(result.commitMessage.includes("- S02: Error handling"), "body lists S02");
167
+ assert.ok(result.commitMessage.includes("- S03: Logging infra"), "body lists S03");
168
+ assert.ok(result.commitMessage.includes("Branch: milestone/M020"), "body has branch metadata");
169
+
170
+ const gitMsg = run("git log -1 --format=%B main", repo).trim();
171
+ assert.match(gitMsg, /^feat\(M020\):/, "git commit message starts with feat(M020):");
172
+ assert.ok(gitMsg.includes("- S01: Core API"), "git commit body has S01");
173
+ });
174
+
175
+ test("nothing to commit — safe when no code changes (#1738, #1792)", () => {
176
+ const repo = freshRepo();
177
+ const wtPath = createAutoWorktree(repo, "M030");
178
+ const roadmap = makeRoadmap("M030", "Empty milestone", []);
179
+
180
+ let threw = false;
181
+ let errorMsg = "";
182
+ try {
183
+ mergeMilestoneToMain(repo, "M030", roadmap);
184
+ } catch (err: unknown) {
185
+ threw = true;
186
+ errorMsg = err instanceof Error ? err.message : String(err);
182
187
  }
188
+ assert.ok(!threw, `safe empty milestone should not throw (got: ${errorMsg})`);
183
189
 
184
- // ─── Test 3: Nothing to commit — preserves branch (#1738) ──────────
185
- console.log("\n=== nothing to commit safe when no code changes (#1738, #1792) ===");
186
- {
187
- const repo = freshRepo();
188
- const wtPath = createAutoWorktree(repo, "M030");
189
-
190
- // Don't add any slices/changes — milestone branch is identical to main
191
- const roadmap = makeRoadmap("M030", "Empty milestone", []);
192
-
193
- // Should NOT throw — milestone branch is identical to main, nothing to lose.
194
- // The anchor check (#1792) verifies no code files differ and passes through.
195
- let threw = false;
196
- let errorMsg = "";
197
- try {
198
- mergeMilestoneToMain(repo, "M030", roadmap);
199
- } catch (err: unknown) {
200
- threw = true;
201
- errorMsg = err instanceof Error ? err.message : String(err);
202
- }
203
- assertTrue(!threw, `safe empty milestone should not throw (got: ${errorMsg})`);
204
-
205
- // Main log unchanged (only init commit)
206
- const mainLog = run("git log --oneline main", repo);
207
- assertEq(mainLog.split("\n").length, 1, "main still has only init commit");
208
- }
190
+ const mainLog = run("git log --oneline main", repo);
191
+ assert.strictEqual(mainLog.split("\n").length, 1, "main still has only init commit");
192
+ });
209
193
 
210
- // ─── Test 4: Auto-push verify push mechanics work ──────────────
211
- // Note: loadEffectiveGSDPreferences uses a module-level const for project
212
- // prefs path (process.cwd() at import time), so temp repo prefs aren't
213
- // discoverable. We verify the push mechanics work by testing that
214
- // mergeMilestoneToMain successfully completes with a remote configured,
215
- // then manually push to verify the remote is set up correctly.
216
- console.log("\n=== auto-push with bare remote ===");
217
- {
218
- const repo = freshRepo();
219
-
220
- // Set up bare remote
221
- const bareDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-bare-")));
222
- tempDirs.push(bareDir);
223
- run("git init --bare", bareDir);
224
- run(`git remote add origin ${bareDir}`, repo);
225
- run("git push -u origin main", repo);
226
-
227
- const wtPath = createAutoWorktree(repo, "M040");
228
-
229
- addSliceToMilestone(repo, wtPath, "M040", "S01", "Push test", [
230
- { file: "pushed.ts", content: "export const pushed = true;\n", message: "add pushed file" },
231
- ]);
232
-
233
- const roadmap = makeRoadmap("M040", "Push verification", [
234
- { id: "S01", title: "Push test" },
235
- ]);
236
-
237
- const result = mergeMilestoneToMain(repo, "M040", roadmap);
238
-
239
- // Verify merge succeeded (commit on main)
240
- const mainLog = run("git log --oneline main", repo);
241
- assertTrue(mainLog.includes("feat(M040)"), "milestone commit on main");
242
-
243
- // Manually push to verify remote works
244
- run("git push origin main", repo);
245
- const remoteLog = run("git log --oneline main", bareDir);
246
- assertTrue(remoteLog.includes("feat(M040)"), "milestone commit reachable on remote after manual push");
247
-
248
- // Temp-repo prefs may or may not be discoverable depending on process cwd and
249
- // current preference-loading behavior. The important contract is that remote
250
- // push mechanics work and the returned value reflects what happened.
251
- assertTrue(typeof result.pushed === "boolean", "pushed flag remains boolean");
252
- }
194
+ test("auto-push with bare remote", () => {
195
+ const repo = freshRepo();
253
196
 
254
- // ─── Test 5: Auto-resolve .gsd/ state file conflicts (#530) ───────
255
- console.log("\n=== auto-resolve .gsd/ state file conflicts ===");
256
- {
257
- const repo = freshRepo();
258
- const wtPath = createAutoWorktree(repo, "M050");
259
-
260
- // Add a slice with real work
261
- addSliceToMilestone(repo, wtPath, "M050", "S01", "Conflict test", [
262
- { file: "feature.ts", content: "export const feature = true;\n", message: "add feature" },
263
- ]);
264
-
265
- // Modify .gsd/STATE.md on the milestone branch (simulates auto-mode state updates)
266
- writeFileSync(join(wtPath, ".gsd", "STATE.md"), "# State\n\n## Updated on milestone branch\n");
267
- run("git add .", wtPath);
268
- run('git commit -m "chore: update state on milestone branch"', wtPath);
269
-
270
- // Now modify .gsd/STATE.md on main too (simulates divergence)
271
- run("git checkout main", repo);
272
- writeFileSync(join(repo, ".gsd", "STATE.md"), "# State\n\n## Updated on main\n");
273
- run("git add .", repo);
274
- run('git commit -m "chore: update state on main"', repo);
275
-
276
- // Go back to worktree for the merge
277
- process.chdir(wtPath);
278
-
279
- const roadmap = makeRoadmap("M050", "Conflict resolution", [
280
- { id: "S01", title: "Conflict test" },
281
- ]);
282
-
283
- // Merge should succeed despite .gsd/STATE.md conflict — auto-resolved
284
- let threw = false;
285
- try {
286
- const result = mergeMilestoneToMain(repo, "M050", roadmap);
287
- assertTrue(result.commitMessage.includes("feat(M050)"), "merge commit created despite .gsd conflict");
288
- } catch (err) {
289
- threw = true;
290
- }
291
- assertTrue(!threw, "auto-resolves .gsd/ state file conflicts without throwing");
292
-
293
- // Feature file should be on main
294
- assertTrue(existsSync(join(repo, "feature.ts")), "feature.ts merged to main");
295
- }
197
+ const bareDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-bare-")));
198
+ tempDirs.push(bareDir);
199
+ run("git init --bare", bareDir);
200
+ run(`git remote add origin ${bareDir}`, repo);
201
+ run("git push -u origin main", repo);
296
202
 
297
- // ─── Test 6: Skip checkout when main already current (#757) ───────
298
- console.log("\n=== skip checkout when main already current (#757) ===");
299
- {
300
- const repo = freshRepo();
301
- const wtPath = createAutoWorktree(repo, "M060");
302
-
303
- addSliceToMilestone(repo, wtPath, "M060", "S01", "Skip checkout test", [
304
- { file: "skip-checkout.ts", content: "export const skip = true;\n", message: "add skip-checkout" },
305
- ]);
306
-
307
- const roadmap = makeRoadmap("M060", "Skip checkout verification", [
308
- { id: "S01", title: "Skip checkout test" },
309
- ]);
310
-
311
- // Verify main is already checked out at repo root (worktree default)
312
- const branchAtRoot = run("git rev-parse --abbrev-ref HEAD", repo);
313
- assertEq(branchAtRoot, "main", "main is already checked out at project root");
314
-
315
- // mergeMilestoneToMain should succeed without attempting to checkout main
316
- // (which would fail with "already used by worktree" error)
317
- let threw = false;
318
- try {
319
- const result = mergeMilestoneToMain(repo, "M060", roadmap);
320
- assertTrue(result.commitMessage.includes("feat(M060)"), "merge commit created");
321
- } catch (err) {
322
- threw = true;
323
- console.error("Unexpected error:", err);
324
- }
325
- assertTrue(!threw, "does not fail when main is already checked out at project root");
326
-
327
- // Verify the merge actually happened
328
- assertTrue(existsSync(join(repo, "skip-checkout.ts")), "skip-checkout.ts merged to main");
329
- }
203
+ const wtPath = createAutoWorktree(repo, "M040");
330
204
 
331
- // ─── Test 7: Repo using `master` as default branch (#1668) ────────
332
- console.log("\n=== master-branch repo no META.json, no prefs (#1668) ===");
333
- {
334
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-master-test-")));
335
- tempDirs.push(dir);
336
- run("git init -b master", dir);
337
- run("git config user.email test@test.com", dir);
338
- run("git config user.name Test", dir);
339
- writeFileSync(join(dir, "README.md"), "# master-branch repo\n");
340
- mkdirSync(join(dir, ".gsd"), { recursive: true });
341
- writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
342
- run("git add .", dir);
343
- run("git commit -m init", dir);
344
- const defaultBranch = run("git rev-parse --abbrev-ref HEAD", dir);
345
- assertEq(defaultBranch, "master", "repo is on master branch");
346
-
347
- const wtPath = createAutoWorktree(dir, "M070");
348
- addSliceToMilestone(dir, wtPath, "M070", "S01", "Master branch test", [
349
- { file: "master-feature.ts", content: "export const masterFeature = true;\n", message: "add master feature" },
350
- ]);
351
-
352
- const metaFile = join(dir, ".gsd", "milestones", "M070", "M070-META.json");
353
- assertTrue(!existsSync(metaFile), "no META.json — integration branch not captured");
354
-
355
- const roadmap = makeRoadmap("M070", "Master branch milestone", [
356
- { id: "S01", title: "Master branch test" },
357
- ]);
358
-
359
- let threw = false;
360
- let errMsg = "";
361
- try {
362
- const result = mergeMilestoneToMain(dir, "M070", roadmap);
363
- assertTrue(result.commitMessage.includes("feat(M070)"), "merge commit created on master");
364
- } catch (err) {
365
- threw = true;
366
- errMsg = err instanceof Error ? err.message : String(err);
367
- }
368
- assertTrue(!threw, `should not throw on master-branch repo (got: ${errMsg})`);
369
-
370
- const finalBranch = run("git rev-parse --abbrev-ref HEAD", dir);
371
- assertEq(finalBranch, "master", "repo is still on master after merge");
372
- assertTrue(existsSync(join(dir, "master-feature.ts")), "feature merged to master");
373
- const branches = run("git branch", dir);
374
- assertTrue(!branches.includes("milestone/M070"), "milestone branch deleted after merge");
375
- }
205
+ addSliceToMilestone(repo, wtPath, "M040", "S01", "Push test", [
206
+ { file: "pushed.ts", content: "export const pushed = true;\n", message: "add pushed file" },
207
+ ]);
376
208
 
377
- // ─── Test 8: #1738 Bug 1 — dirty working tree detected by nativeMergeSquash ──
378
- console.log("\n=== #1738 bug 1: nativeMergeSquash detects dirty working tree ===");
379
- {
380
- const { nativeMergeSquash } = await import("../native-git-bridge.ts");
381
- const repo = freshRepo();
382
-
383
- run("git checkout -b milestone/M070", repo);
384
- writeFileSync(join(repo, "feature.ts"), "export const feature = true;\n");
385
- run("git add .", repo);
386
- run('git commit -m "add feature"', repo);
387
- run("git checkout main", repo);
388
-
389
- writeFileSync(join(repo, "feature.ts"), "// local dirty version\n");
390
-
391
- const result = nativeMergeSquash(repo, "milestone/M070");
392
- assertEq(result.success, false, "merge reports failure on dirty working tree");
393
- assertTrue(
394
- result.conflicts.includes("__dirty_working_tree__"),
395
- "conflicts include __dirty_working_tree__ sentinel",
396
- );
397
-
398
- run("git checkout -- . 2>/dev/null || true", repo);
399
- run("rm -f feature.ts", repo);
400
- }
209
+ const roadmap = makeRoadmap("M040", "Push verification", [
210
+ { id: "S01", title: "Push test" },
211
+ ]);
401
212
 
402
- // ─── Test 9: #1738 Bug 2 — branch preserved on empty squash commit ──
403
- console.log("\n=== #1738 bug 2: branch preserved when squash commit empty ===");
404
- {
405
- const repo = freshRepo();
406
- const wtPath = createAutoWorktree(repo, "M080");
407
-
408
- // Make no changes — squash will produce nothing to commit
409
- const roadmap = makeRoadmap("M080", "Empty milestone", []);
410
-
411
- // With the #1792 anchor check, empty milestones with no code changes
412
- // are safe to proceed — no data to lose.
413
- let threw = false;
414
- let errMsg = "";
415
- try {
416
- mergeMilestoneToMain(repo, "M080", roadmap);
417
- } catch (err: unknown) {
418
- threw = true;
419
- errMsg = err instanceof Error ? err.message : String(err);
420
- }
421
- assertTrue(!threw, `empty milestone with no code changes should not throw (got: ${errMsg})`);
422
- }
213
+ const result = mergeMilestoneToMain(repo, "M040", roadmap);
423
214
 
424
- // ─── Test 10: #1738 Bug 3 — clearProjectRootStateFiles cleans synced dirs ──
425
- console.log("\n=== #1738 bug 3: synced .gsd/ dirs cleaned before merge ===");
426
- {
427
- const repo = freshRepo();
428
- const wtPath = createAutoWorktree(repo, "M090");
429
-
430
- addSliceToMilestone(repo, wtPath, "M090", "S01", "Sync test", [
431
- { file: "sync-test.ts", content: "export const sync = true;\n", message: "add sync-test" },
432
- ]);
433
-
434
- // Simulate syncStateToProjectRoot: create untracked .gsd/ milestone files
435
- const msDir = join(repo, ".gsd", "milestones", "M090", "slices", "S01");
436
- mkdirSync(msDir, { recursive: true });
437
- writeFileSync(join(msDir, "S01-PLAN.md"), "# synced plan\n");
438
- writeFileSync(
439
- join(repo, ".gsd", "milestones", "M090", "M090-ROADMAP.md"),
440
- "# synced roadmap\n",
441
- );
442
-
443
- const runtimeDir = join(repo, ".gsd", "runtime", "units");
444
- mkdirSync(runtimeDir, { recursive: true });
445
- writeFileSync(join(runtimeDir, "unit-001.json"), '{"stale": true}');
446
-
447
- const roadmap = makeRoadmap("M090", "Sync cleanup test", [
448
- { id: "S01", title: "Sync test" },
449
- ]);
450
-
451
- let threw = false;
452
- try {
453
- const result = mergeMilestoneToMain(repo, "M090", roadmap);
454
- assertTrue(
455
- result.commitMessage.includes("feat(M090)"),
456
- "#1738 merge succeeds after cleaning synced dirs",
457
- );
458
- } catch (err: unknown) {
459
- threw = true;
460
- console.error("#1738 bug 3 regression:", err);
461
- }
462
- assertTrue(!threw, "#1738 merge does not fail on synced .gsd/ files");
463
- assertTrue(existsSync(join(repo, "sync-test.ts")), "sync-test.ts on main after merge");
464
- }
215
+ const mainLog = run("git log --oneline main", repo);
216
+ assert.ok(mainLog.includes("feat(M040)"), "milestone commit on main");
465
217
 
466
- // ─── Test 11: #1738 Bug 1+2 → #2151: dirty tree auto-stashed, merge succeeds ──
467
- // Before #2151, a conflicting dirty file in the project root would cause
468
- // the squash merge to reject. Now auto-stash moves it out of the way,
469
- // the merge succeeds, and the user's local file goes to the stash.
470
- console.log("\n=== #2151: dirty tree auto-stashed, merge succeeds ===");
471
- {
472
- const repo = freshRepo();
473
- const wtPath = createAutoWorktree(repo, "M100");
218
+ run("git push origin main", repo);
219
+ const remoteLog = run("git log --oneline main", bareDir);
220
+ assert.ok(remoteLog.includes("feat(M040)"), "milestone commit reachable on remote after manual push");
474
221
 
475
- addSliceToMilestone(repo, wtPath, "M100", "S01", "E2E test", [
476
- { file: "e2e.ts", content: "export const e2e = true;\n", message: "add e2e" },
477
- ]);
222
+ assert.strictEqual(typeof result.pushed, "boolean", "pushed flag remains boolean");
223
+ });
478
224
 
479
- // Create a conflicting local file previously blocked the merge.
480
- writeFileSync(join(repo, "e2e.ts"), "// conflicting local file\n");
225
+ test("auto-resolve .gsd/ state file conflicts", () => {
226
+ const repo = freshRepo();
227
+ const wtPath = createAutoWorktree(repo, "M050");
481
228
 
482
- const roadmap = makeRoadmap("M100", "E2E dirty tree", [
483
- { id: "S01", title: "E2E test" },
484
- ]);
229
+ addSliceToMilestone(repo, wtPath, "M050", "S01", "Conflict test", [
230
+ { file: "feature.ts", content: "export const feature = true;\n", message: "add feature" },
231
+ ]);
485
232
 
486
- // With auto-stash (#2151), the merge should succeed.
487
- const result = mergeMilestoneToMain(repo, "M100", roadmap);
488
- assertTrue(result.commitMessage.includes("feat(M100)"), "#2151: merge succeeds after auto-stash");
233
+ writeFileSync(join(wtPath, ".gsd", "STATE.md"), "# State\n\n## Updated on milestone branch\n");
234
+ run("git add .", wtPath);
235
+ run('git commit -m "chore: update state on milestone branch"', wtPath);
489
236
 
490
- // The milestone code should be on main.
491
- assertTrue(existsSync(join(repo, "e2e.ts")), "#2151: e2e.ts merged to main");
492
- const content = readFileSync(join(repo, "e2e.ts"), "utf-8");
493
- assertEq(content.replace(/\r\n/g, "\n"), "export const e2e = true;\n", "#2151: merged content is from milestone branch");
494
- }
237
+ run("git checkout main", repo);
238
+ writeFileSync(join(repo, ".gsd", "STATE.md"), "# State\n\n## Updated on main\n");
239
+ run("git add .", repo);
240
+ run('git commit -m "chore: update state on main"', repo);
241
+
242
+ process.chdir(wtPath);
495
243
 
496
- // ─── Test 12: Throw on unanchored code changes after empty commit (#1792) ─
497
- console.log("\n=== throw on unanchored code changes after empty commit (#1792) ===");
498
- {
499
- const repo = freshRepo();
500
- const wtPath = createAutoWorktree(repo, "M120");
501
-
502
- addSliceToMilestone(repo, wtPath, "M120", "S01", "Critical feature", [
503
- { file: "critical.ts", content: "export const critical = true;\n", message: "add critical feature" },
504
- ]);
505
-
506
- // Simulate: merge then revert — git considers branch "already merged"
507
- // but code is NOT on main (reverted).
508
- run(`git merge milestone/M120 --no-ff -m "merge M120"`, repo);
509
- run("git revert HEAD --no-edit -m 1", repo);
510
-
511
- const roadmap = makeRoadmap("M120", "Critical milestone", [
512
- { id: "S01", title: "Critical feature" },
513
- ]);
514
-
515
- let threw = false;
516
- let errMsg = "";
517
- try {
518
- mergeMilestoneToMain(repo, "M120", roadmap);
519
- } catch (err) {
520
- threw = true;
521
- errMsg = err instanceof Error ? err.message : String(err);
522
- }
523
- assertTrue(threw, "throws when milestone has unanchored code changes (#1792)");
524
- assertTrue(
525
- errMsg.includes("code file(s) not on"),
526
- "error message mentions unanchored code files (#1792)",
527
- );
528
-
529
- const branches = run("git branch", repo);
530
- assertTrue(
531
- branches.includes("milestone/M120"),
532
- "milestone branch preserved when code is unanchored (#1792)",
533
- );
244
+ const roadmap = makeRoadmap("M050", "Conflict resolution", [
245
+ { id: "S01", title: "Conflict test" },
246
+ ]);
247
+
248
+ let threw = false;
249
+ try {
250
+ const result = mergeMilestoneToMain(repo, "M050", roadmap);
251
+ assert.ok(result.commitMessage.includes("feat(M050)"), "merge commit created despite .gsd conflict");
252
+ } catch (err) {
253
+ threw = true;
254
+ }
255
+ assert.ok(!threw, "auto-resolves .gsd/ state file conflicts without throwing");
256
+ assert.ok(existsSync(join(repo, "feature.ts")), "feature.ts merged to main");
257
+ });
258
+
259
+ test("skip checkout when main already current (#757)", () => {
260
+ const repo = freshRepo();
261
+ const wtPath = createAutoWorktree(repo, "M060");
262
+
263
+ addSliceToMilestone(repo, wtPath, "M060", "S01", "Skip checkout test", [
264
+ { file: "skip-checkout.ts", content: "export const skip = true;\n", message: "add skip-checkout" },
265
+ ]);
266
+
267
+ const roadmap = makeRoadmap("M060", "Skip checkout verification", [
268
+ { id: "S01", title: "Skip checkout test" },
269
+ ]);
270
+
271
+ const branchAtRoot = run("git rev-parse --abbrev-ref HEAD", repo);
272
+ assert.strictEqual(branchAtRoot, "main", "main is already checked out at project root");
273
+
274
+ let threw = false;
275
+ try {
276
+ const result = mergeMilestoneToMain(repo, "M060", roadmap);
277
+ assert.ok(result.commitMessage.includes("feat(M060)"), "merge commit created");
278
+ } catch (err) {
279
+ threw = true;
280
+ }
281
+ assert.ok(!threw, "does not fail when main is already checked out at project root");
282
+ assert.ok(existsSync(join(repo, "skip-checkout.ts")), "skip-checkout.ts merged to main");
283
+ });
284
+
285
+ test("master-branch repo — no META.json, no prefs (#1668)", () => {
286
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-master-test-")));
287
+ tempDirs.push(dir);
288
+ run("git init -b master", dir);
289
+ run("git config user.email test@test.com", dir);
290
+ run("git config user.name Test", dir);
291
+ writeFileSync(join(dir, "README.md"), "# master-branch repo\n");
292
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
293
+ writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
294
+ run("git add .", dir);
295
+ run("git commit -m init", dir);
296
+ const defaultBranch = run("git rev-parse --abbrev-ref HEAD", dir);
297
+ assert.strictEqual(defaultBranch, "master", "repo is on master branch");
298
+
299
+ const wtPath = createAutoWorktree(dir, "M070");
300
+ addSliceToMilestone(dir, wtPath, "M070", "S01", "Master branch test", [
301
+ { file: "master-feature.ts", content: "export const masterFeature = true;\n", message: "add master feature" },
302
+ ]);
303
+
304
+ const metaFile = join(dir, ".gsd", "milestones", "M070", "M070-META.json");
305
+ assert.ok(!existsSync(metaFile), "no META.json — integration branch not captured");
306
+
307
+ const roadmap = makeRoadmap("M070", "Master branch milestone", [
308
+ { id: "S01", title: "Master branch test" },
309
+ ]);
310
+
311
+ let threw = false;
312
+ let errMsg = "";
313
+ try {
314
+ const result = mergeMilestoneToMain(dir, "M070", roadmap);
315
+ assert.ok(result.commitMessage.includes("feat(M070)"), "merge commit created on master");
316
+ } catch (err) {
317
+ threw = true;
318
+ errMsg = err instanceof Error ? err.message : String(err);
534
319
  }
320
+ assert.ok(!threw, `should not throw on master-branch repo (got: ${errMsg})`);
321
+
322
+ const finalBranch = run("git rev-parse --abbrev-ref HEAD", dir);
323
+ assert.strictEqual(finalBranch, "master", "repo is still on master after merge");
324
+ assert.ok(existsSync(join(dir, "master-feature.ts")), "feature merged to master");
325
+ const branches = run("git branch", dir);
326
+ assert.ok(!branches.includes("milestone/M070"), "milestone branch deleted after merge");
327
+ });
328
+
329
+ test("#1738 bug 1: nativeMergeSquash detects dirty working tree", async () => {
330
+ const { nativeMergeSquash } = await import("../native-git-bridge.ts");
331
+ const repo = freshRepo();
332
+
333
+ run("git checkout -b milestone/M070", repo);
334
+ writeFileSync(join(repo, "feature.ts"), "export const feature = true;\n");
335
+ run("git add .", repo);
336
+ run('git commit -m "add feature"', repo);
337
+ run("git checkout main", repo);
338
+
339
+ writeFileSync(join(repo, "feature.ts"), "// local dirty version\n");
340
+
341
+ const result = nativeMergeSquash(repo, "milestone/M070");
342
+ assert.strictEqual(result.success, false, "merge reports failure on dirty working tree");
343
+ assert.ok(
344
+ result.conflicts.includes("__dirty_working_tree__"),
345
+ "conflicts include __dirty_working_tree__ sentinel",
346
+ );
347
+
348
+ run("git checkout -- . 2>/dev/null || true", repo);
349
+ run("rm -f feature.ts", repo);
350
+ });
351
+
352
+ test("#1738 bug 2: branch preserved when squash commit empty", () => {
353
+ const repo = freshRepo();
354
+ const wtPath = createAutoWorktree(repo, "M080");
355
+ const roadmap = makeRoadmap("M080", "Empty milestone", []);
356
+
357
+ let threw = false;
358
+ let errMsg = "";
359
+ try {
360
+ mergeMilestoneToMain(repo, "M080", roadmap);
361
+ } catch (err: unknown) {
362
+ threw = true;
363
+ errMsg = err instanceof Error ? err.message : String(err);
364
+ }
365
+ assert.ok(!threw, `empty milestone with no code changes should not throw (got: ${errMsg})`);
366
+ });
367
+
368
+ test("#1738 bug 3: synced .gsd/ dirs cleaned before merge", () => {
369
+ const repo = freshRepo();
370
+ const wtPath = createAutoWorktree(repo, "M090");
371
+
372
+ addSliceToMilestone(repo, wtPath, "M090", "S01", "Sync test", [
373
+ { file: "sync-test.ts", content: "export const sync = true;\n", message: "add sync-test" },
374
+ ]);
375
+
376
+ const msDir = join(repo, ".gsd", "milestones", "M090", "slices", "S01");
377
+ mkdirSync(msDir, { recursive: true });
378
+ writeFileSync(join(msDir, "S01-PLAN.md"), "# synced plan\n");
379
+ writeFileSync(
380
+ join(repo, ".gsd", "milestones", "M090", "M090-ROADMAP.md"),
381
+ "# synced roadmap\n",
382
+ );
383
+
384
+ const runtimeDir = join(repo, ".gsd", "runtime", "units");
385
+ mkdirSync(runtimeDir, { recursive: true });
386
+ writeFileSync(join(runtimeDir, "unit-001.json"), '{"stale": true}');
387
+
388
+ const roadmap = makeRoadmap("M090", "Sync cleanup test", [
389
+ { id: "S01", title: "Sync test" },
390
+ ]);
391
+
392
+ let threw = false;
393
+ try {
394
+ const result = mergeMilestoneToMain(repo, "M090", roadmap);
395
+ assert.ok(result.commitMessage.includes("feat(M090)"), "#1738 merge succeeds after cleaning synced dirs");
396
+ } catch (err: unknown) {
397
+ threw = true;
398
+ }
399
+ assert.ok(!threw, "#1738 merge does not fail on synced .gsd/ files");
400
+ assert.ok(existsSync(join(repo, "sync-test.ts")), "sync-test.ts on main after merge");
401
+ });
402
+
403
+ test("#1738 e2e: dirty tree is stashed before merge (#2151)", () => {
404
+ const repo = freshRepo();
405
+ const wtPath = createAutoWorktree(repo, "M100");
406
+
407
+ addSliceToMilestone(repo, wtPath, "M100", "S01", "E2E test", [
408
+ { file: "e2e.ts", content: "export const e2e = true;\n", message: "add e2e" },
409
+ ]);
535
410
 
536
- // ─── Test 13: Safe teardown when nothing-to-commit and work already on main (#1792)
537
- console.log("\n=== safe teardown — nothing to commit, work already on main (#1792) ===");
538
- {
539
- const repo = freshRepo();
540
- const wtPath = createAutoWorktree(repo, "M130");
541
-
542
- addSliceToMilestone(repo, wtPath, "M130", "S01", "Already landed", [
543
- { file: "landed.ts", content: "export const landed = true;\n", message: "add landed feature" },
544
- ]);
545
-
546
- run("git merge --squash milestone/M130", repo);
547
- run('git commit -m "pre-land milestone work"', repo);
548
-
549
- const roadmap = makeRoadmap("M130", "Pre-landed milestone", [
550
- { id: "S01", title: "Already landed" },
551
- ]);
552
-
553
- let threw = false;
554
- let errMsg = "";
555
- try {
556
- mergeMilestoneToMain(repo, "M130", roadmap);
557
- } catch (err) {
558
- threw = true;
559
- errMsg = err instanceof Error ? err.message : String(err);
560
- }
561
- assertTrue(!threw, `safe nothing-to-commit should not throw (got: ${errMsg})`);
562
- assertTrue(existsSync(join(repo, "landed.ts")), "landed.ts present on main");
411
+ writeFileSync(join(repo, "e2e.ts"), "// conflicting local file\n");
412
+
413
+ const roadmap = makeRoadmap("M100", "E2E dirty tree", [
414
+ { id: "S01", title: "E2E test" },
415
+ ]);
416
+
417
+ // Since #2151, dirty files are stashed before the squash merge instead
418
+ // of causing an immediate rejection. The merge should succeed.
419
+ let threw = false;
420
+ try {
421
+ const result = mergeMilestoneToMain(repo, "M100", roadmap);
422
+ assert.ok(result.commitMessage.includes("feat(M100)"), "#2151: merge succeeds after stashing dirty files");
423
+ } catch {
424
+ threw = true;
563
425
  }
426
+ assert.ok(!threw, "#2151: dirty tree no longer rejects — stash handles it");
427
+ });
428
+
429
+ test("throw on unanchored code changes after empty commit (#1792)", () => {
430
+ const repo = freshRepo();
431
+ const wtPath = createAutoWorktree(repo, "M120");
432
+
433
+ addSliceToMilestone(repo, wtPath, "M120", "S01", "Critical feature", [
434
+ { file: "critical.ts", content: "export const critical = true;\n", message: "add critical feature" },
435
+ ]);
436
+
437
+ run(`git merge milestone/M120 --no-ff -m "merge M120"`, repo);
438
+ run("git revert HEAD --no-edit -m 1", repo);
439
+
440
+ const roadmap = makeRoadmap("M120", "Critical milestone", [
441
+ { id: "S01", title: "Critical feature" },
442
+ ]);
443
+
444
+ let threw = false;
445
+ let errMsg = "";
446
+ try {
447
+ mergeMilestoneToMain(repo, "M120", roadmap);
448
+ } catch (err) {
449
+ threw = true;
450
+ errMsg = err instanceof Error ? err.message : String(err);
451
+ }
452
+ assert.ok(threw, "throws when milestone has unanchored code changes (#1792)");
453
+ assert.ok(errMsg.includes("code file(s) not on"), "error message mentions unanchored code files (#1792)");
454
+
455
+ const branches = run("git branch", repo);
456
+ assert.ok(branches.includes("milestone/M120"), "milestone branch preserved when code is unanchored (#1792)");
457
+ });
458
+
459
+ test("safe teardown — nothing to commit, work already on main (#1792)", () => {
460
+ const repo = freshRepo();
461
+ const wtPath = createAutoWorktree(repo, "M130");
462
+
463
+ addSliceToMilestone(repo, wtPath, "M130", "S01", "Already landed", [
464
+ { file: "landed.ts", content: "export const landed = true;\n", message: "add landed feature" },
465
+ ]);
466
+
467
+ run("git merge --squash milestone/M130", repo);
468
+ run('git commit -m "pre-land milestone work"', repo);
469
+
470
+ const roadmap = makeRoadmap("M130", "Pre-landed milestone", [
471
+ { id: "S01", title: "Already landed" },
472
+ ]);
473
+
474
+ let threw = false;
475
+ let errMsg = "";
476
+ try {
477
+ mergeMilestoneToMain(repo, "M130", roadmap);
478
+ } catch (err) {
479
+ threw = true;
480
+ errMsg = err instanceof Error ? err.message : String(err);
481
+ }
482
+ assert.ok(!threw, `safe nothing-to-commit should not throw (got: ${errMsg})`);
483
+ assert.ok(existsSync(join(repo, "landed.ts")), "landed.ts present on main");
484
+ });
485
+
486
+ test("stale branch ref — fast-forward before squash merge (#1846)", () => {
487
+ const repo = freshRepo();
488
+ const wtPath = createAutoWorktree(repo, "M140");
489
+
490
+ addSliceToMilestone(repo, wtPath, "M140", "S01", "Initial work", [
491
+ { file: "initial.ts", content: "export const initial = true;\n", message: "add initial" },
492
+ ]);
493
+
494
+ const branchRefBefore = run("git rev-parse milestone/M140", wtPath);
495
+ run("git checkout --detach HEAD", wtPath);
496
+
497
+ writeFileSync(join(wtPath, "feature-a.ts"), "export const featureA = true;\n");
498
+ run("git add .", wtPath);
499
+ run('git commit -m "add feature-a"', wtPath);
564
500
 
565
- // ─── Test 14: Stale branch ref — worktree HEAD ahead of branch (#1846)
566
- console.log("\n=== stale branch ref — fast-forward before squash merge (#1846) ===");
567
- {
568
- const repo = freshRepo();
569
- const wtPath = createAutoWorktree(repo, "M140");
570
-
571
- // Add a first slice normally — this advances both the branch ref and HEAD
572
- addSliceToMilestone(repo, wtPath, "M140", "S01", "Initial work", [
573
- { file: "initial.ts", content: "export const initial = true;\n", message: "add initial" },
574
- ]);
575
-
576
- // Now simulate the bug: detach HEAD in the worktree, then make commits
577
- // that advance HEAD but leave the milestone/M140 branch ref behind.
578
- const branchRefBefore = run("git rev-parse milestone/M140", wtPath);
579
- run("git checkout --detach HEAD", wtPath);
580
-
581
- // Add multiple commits on the detached HEAD (simulates agent work)
582
- writeFileSync(join(wtPath, "feature-a.ts"), "export const featureA = true;\n");
583
- run("git add .", wtPath);
584
- run('git commit -m "add feature-a"', wtPath);
585
-
586
- writeFileSync(join(wtPath, "feature-b.ts"), "export const featureB = true;\n");
587
- run("git add .", wtPath);
588
- run('git commit -m "add feature-b"', wtPath);
589
-
590
- writeFileSync(join(wtPath, "feature-c.ts"), "export const featureC = true;\n");
591
- run("git add .", wtPath);
592
- run('git commit -m "add feature-c"', wtPath);
593
-
594
- // Verify: branch ref is stale, HEAD is ahead
595
- const branchRefAfter = run("git rev-parse milestone/M140", wtPath);
596
- const worktreeHead = run("git rev-parse HEAD", wtPath);
597
- assertEq(branchRefBefore, branchRefAfter, "branch ref unchanged (stale)");
598
- assertTrue(worktreeHead !== branchRefAfter, "worktree HEAD ahead of branch ref");
599
-
600
- const roadmap = makeRoadmap("M140", "Stale ref milestone", [
601
- { id: "S01", title: "Initial work" },
602
- ]);
603
-
604
- // The fix should fast-forward the branch ref to worktree HEAD before
605
- // squash-merging, so ALL commits are captured.
606
- let threw = false;
607
- let errMsg = "";
608
- try {
609
- const result = mergeMilestoneToMain(repo, "M140", roadmap);
610
- assertTrue(result.commitMessage.includes("feat(M140)"), "merge commit created");
611
- } catch (err) {
612
- threw = true;
613
- errMsg = err instanceof Error ? err.message : String(err);
614
- }
615
- assertTrue(!threw, `should not throw with stale branch ref (got: ${errMsg})`);
616
-
617
- // ALL files from detached HEAD commits must be on main — not just
618
- // the ones from the stale branch ref
619
- assertTrue(existsSync(join(repo, "initial.ts")), "initial.ts on main");
620
- assertTrue(existsSync(join(repo, "feature-a.ts")), "feature-a.ts on main (#1846)");
621
- assertTrue(existsSync(join(repo, "feature-b.ts")), "feature-b.ts on main (#1846)");
622
- assertTrue(existsSync(join(repo, "feature-c.ts")), "feature-c.ts on main (#1846)");
501
+ writeFileSync(join(wtPath, "feature-b.ts"), "export const featureB = true;\n");
502
+ run("git add .", wtPath);
503
+ run('git commit -m "add feature-b"', wtPath);
504
+
505
+ writeFileSync(join(wtPath, "feature-c.ts"), "export const featureC = true;\n");
506
+ run("git add .", wtPath);
507
+ run('git commit -m "add feature-c"', wtPath);
508
+
509
+ const branchRefAfter = run("git rev-parse milestone/M140", wtPath);
510
+ const worktreeHead = run("git rev-parse HEAD", wtPath);
511
+ assert.strictEqual(branchRefBefore, branchRefAfter, "branch ref unchanged (stale)");
512
+ assert.ok(worktreeHead !== branchRefAfter, "worktree HEAD ahead of branch ref");
513
+
514
+ const roadmap = makeRoadmap("M140", "Stale ref milestone", [
515
+ { id: "S01", title: "Initial work" },
516
+ ]);
517
+
518
+ let threw = false;
519
+ let errMsg = "";
520
+ try {
521
+ const result = mergeMilestoneToMain(repo, "M140", roadmap);
522
+ assert.ok(result.commitMessage.includes("feat(M140)"), "merge commit created");
523
+ } catch (err) {
524
+ threw = true;
525
+ errMsg = err instanceof Error ? err.message : String(err);
623
526
  }
527
+ assert.ok(!threw, `should not throw with stale branch ref (got: ${errMsg})`);
528
+
529
+ assert.ok(existsSync(join(repo, "initial.ts")), "initial.ts on main");
530
+ assert.ok(existsSync(join(repo, "feature-a.ts")), "feature-a.ts on main (#1846)");
531
+ assert.ok(existsSync(join(repo, "feature-b.ts")), "feature-b.ts on main (#1846)");
532
+ assert.ok(existsSync(join(repo, "feature-c.ts")), "feature-c.ts on main (#1846)");
533
+ });
624
534
 
625
- // ─── Test 15: Diverged worktree HEAD — throws instead of losing data (#1846)
626
- console.log("\n=== diverged worktree HEAD — throws on divergence (#1846) ===");
627
- {
628
- const repo = freshRepo();
629
- const wtPath = createAutoWorktree(repo, "M150");
630
-
631
- addSliceToMilestone(repo, wtPath, "M150", "S01", "Base work", [
632
- { file: "base.ts", content: "export const base = true;\n", message: "add base" },
633
- ]);
634
-
635
- run("git checkout --detach HEAD", wtPath);
636
- writeFileSync(join(wtPath, "detached-work.ts"), "export const detached = true;\n");
637
- run("git add .", wtPath);
638
- run('git commit -m "detached work"', wtPath);
639
-
640
- run("git checkout milestone/M150", repo);
641
- writeFileSync(join(repo, "diverged-work.ts"), "export const diverged = true;\n");
642
- run("git add .", repo);
643
- run('git commit -m "diverged work on branch"', repo);
644
- run("git checkout main", repo);
645
-
646
- process.chdir(wtPath);
647
-
648
- const roadmap = makeRoadmap("M150", "Diverged milestone", [
649
- { id: "S01", title: "Base work" },
650
- ]);
651
-
652
- let threw = false;
653
- let errMsg = "";
654
- try {
655
- mergeMilestoneToMain(repo, "M150", roadmap);
656
- } catch (err) {
657
- threw = true;
658
- errMsg = err instanceof Error ? err.message : String(err);
659
- }
660
- assertTrue(threw, "throws when worktree HEAD diverged from branch ref (#1846)");
661
- assertTrue(errMsg.includes("diverged"), "error message mentions divergence (#1846)");
662
-
663
- const branches = run("git branch", repo);
664
- assertTrue(branches.includes("milestone/M150"), "milestone branch preserved on divergence (#1846)");
535
+ test("diverged worktree HEAD — throws on divergence (#1846)", () => {
536
+ const repo = freshRepo();
537
+ const wtPath = createAutoWorktree(repo, "M150");
538
+
539
+ addSliceToMilestone(repo, wtPath, "M150", "S01", "Base work", [
540
+ { file: "base.ts", content: "export const base = true;\n", message: "add base" },
541
+ ]);
542
+
543
+ run("git checkout --detach HEAD", wtPath);
544
+ writeFileSync(join(wtPath, "detached-work.ts"), "export const detached = true;\n");
545
+ run("git add .", wtPath);
546
+ run('git commit -m "detached work"', wtPath);
547
+
548
+ run("git checkout milestone/M150", repo);
549
+ writeFileSync(join(repo, "diverged-work.ts"), "export const diverged = true;\n");
550
+ run("git add .", repo);
551
+ run('git commit -m "diverged work on branch"', repo);
552
+ run("git checkout main", repo);
553
+
554
+ process.chdir(wtPath);
555
+
556
+ const roadmap = makeRoadmap("M150", "Diverged milestone", [
557
+ { id: "S01", title: "Base work" },
558
+ ]);
559
+
560
+ let threw = false;
561
+ let errMsg = "";
562
+ try {
563
+ mergeMilestoneToMain(repo, "M150", roadmap);
564
+ } catch (err) {
565
+ threw = true;
566
+ errMsg = err instanceof Error ? err.message : String(err);
665
567
  }
568
+ assert.ok(threw, "throws when worktree HEAD diverged from branch ref (#1846)");
569
+ assert.ok(errMsg.includes("diverged"), "error message mentions divergence (#1846)");
666
570
 
667
- // ─── Test 16: #1853 Bug 1 — SQUASH_MSG cleaned up after squash-merge ──
668
- console.log("\n=== #1853 bug 1: SQUASH_MSG cleaned up after successful squash-merge ===");
669
- {
670
- const repo = freshRepo();
671
- const wtPath = createAutoWorktree(repo, "M160");
571
+ const branches = run("git branch", repo);
572
+ assert.ok(branches.includes("milestone/M150"), "milestone branch preserved on divergence (#1846)");
573
+ });
672
574
 
673
- addSliceToMilestone(repo, wtPath, "M160", "S01", "SQUASH_MSG cleanup test", [
674
- { file: "squash-cleanup.ts", content: "export const cleanup = true;\n", message: "add squash-cleanup" },
675
- ]);
575
+ test("#1853 bug 1: SQUASH_MSG cleaned up after successful squash-merge", () => {
576
+ const repo = freshRepo();
577
+ const wtPath = createAutoWorktree(repo, "M160");
676
578
 
677
- const roadmap = makeRoadmap("M160", "SQUASH_MSG cleanup", [
678
- { id: "S01", title: "SQUASH_MSG cleanup test" },
679
- ]);
579
+ addSliceToMilestone(repo, wtPath, "M160", "S01", "SQUASH_MSG cleanup test", [
580
+ { file: "squash-cleanup.ts", content: "export const cleanup = true;\n", message: "add squash-cleanup" },
581
+ ]);
680
582
 
681
- const squashMsgPath = join(repo, ".git", "SQUASH_MSG");
682
- writeFileSync(squashMsgPath, "leftover squash message\n");
683
- assertTrue(existsSync(squashMsgPath), "SQUASH_MSG planted before merge");
583
+ const roadmap = makeRoadmap("M160", "SQUASH_MSG cleanup", [
584
+ { id: "S01", title: "SQUASH_MSG cleanup test" },
585
+ ]);
684
586
 
685
- const result = mergeMilestoneToMain(repo, "M160", roadmap);
686
- assertTrue(result.commitMessage.includes("feat(M160)"), "merge commit created");
587
+ const squashMsgPath = join(repo, ".git", "SQUASH_MSG");
588
+ writeFileSync(squashMsgPath, "leftover squash message\n");
589
+ assert.ok(existsSync(squashMsgPath), "SQUASH_MSG planted before merge");
687
590
 
688
- assertTrue(
689
- !existsSync(squashMsgPath),
690
- "#1853: SQUASH_MSG must not persist after successful squash-merge",
691
- );
692
- }
591
+ const result = mergeMilestoneToMain(repo, "M160", roadmap);
592
+ assert.ok(result.commitMessage.includes("feat(M160)"), "merge commit created");
693
593
 
694
- // ─── Test 17: #1853 Bug 2 uncommitted worktree code survives teardown ──
695
- console.log("\n=== #1853 bug 2: uncommitted worktree changes committed before teardown ===");
696
- {
697
- const repo = freshRepo();
698
- const wtPath = createAutoWorktree(repo, "M170");
594
+ assert.ok(!existsSync(squashMsgPath), "#1853: SQUASH_MSG must not persist after successful squash-merge");
595
+ });
699
596
 
700
- addSliceToMilestone(repo, wtPath, "M170", "S01", "Teardown safety test", [
701
- { file: "safe-file.ts", content: "export const safe = true;\n", message: "add safe file" },
702
- ]);
597
+ test("#1853 bug 2: uncommitted worktree changes committed before teardown", () => {
598
+ const repo = freshRepo();
599
+ const wtPath = createAutoWorktree(repo, "M170");
703
600
 
704
- writeFileSync(join(wtPath, "uncommitted-agent-code.ts"), "export const lost = true;\n");
601
+ addSliceToMilestone(repo, wtPath, "M170", "S01", "Teardown safety test", [
602
+ { file: "safe-file.ts", content: "export const safe = true;\n", message: "add safe file" },
603
+ ]);
705
604
 
706
- const roadmap = makeRoadmap("M170", "Teardown safety", [
707
- { id: "S01", title: "Teardown safety test" },
708
- ]);
605
+ writeFileSync(join(wtPath, "uncommitted-agent-code.ts"), "export const lost = true;\n");
709
606
 
710
- const result = mergeMilestoneToMain(repo, "M170", roadmap);
711
- assertTrue(result.commitMessage.includes("feat(M170)"), "merge commit created");
607
+ const roadmap = makeRoadmap("M170", "Teardown safety", [
608
+ { id: "S01", title: "Teardown safety test" },
609
+ ]);
712
610
 
713
- assertTrue(
714
- existsSync(join(repo, "uncommitted-agent-code.ts")),
715
- "#1853: uncommitted worktree code must survive teardown",
716
- );
717
- }
611
+ const result = mergeMilestoneToMain(repo, "M170", roadmap);
612
+ assert.ok(result.commitMessage.includes("feat(M170)"), "merge commit created");
718
613
 
719
- // ─── Test 18: #1906 — codeFilesChanged false when only .gsd/ metadata merged ──
720
- console.log("\n=== #1906: codeFilesChanged=false when only .gsd/ metadata merged ===");
721
- {
722
- const repo = freshRepo();
723
- const wtPath = createAutoWorktree(repo, "M180");
724
-
725
- // Only add .gsd/ metadata files — no actual code
726
- mkdirSync(join(wtPath, ".gsd", "milestones", "M180"), { recursive: true });
727
- writeFileSync(
728
- join(wtPath, ".gsd", "milestones", "M180", "SUMMARY.md"),
729
- "# M180 Summary\n\nThis milestone was planned but not implemented.\n",
730
- );
731
- run("git add .", wtPath);
732
- run('git commit -m "chore: add milestone summary"', wtPath);
733
-
734
- const roadmap = makeRoadmap("M180", "Metadata-only milestone", []);
735
-
736
- const result = mergeMilestoneToMain(repo, "M180", roadmap);
737
- assertEq(
738
- result.codeFilesChanged,
739
- false,
740
- "#1906: codeFilesChanged must be false when only .gsd/ files were merged",
741
- );
742
- }
614
+ assert.ok(
615
+ existsSync(join(repo, "uncommitted-agent-code.ts")),
616
+ "#1853: uncommitted worktree code must survive teardown",
617
+ );
618
+ });
743
619
 
744
- // ─── Test 19: #1906 codeFilesChanged true when real code is merged ──
745
- console.log("\n=== #1906: codeFilesChanged=true when real code is merged ===");
746
- {
747
- const repo = freshRepo();
748
- const wtPath = createAutoWorktree(repo, "M190");
749
-
750
- addSliceToMilestone(repo, wtPath, "M190", "S01", "Real code", [
751
- { file: "real-code.ts", content: "export const real = true;\n", message: "add real code" },
752
- ]);
753
-
754
- const roadmap = makeRoadmap("M190", "Code milestone", [
755
- { id: "S01", title: "Real code" },
756
- ]);
757
-
758
- const result = mergeMilestoneToMain(repo, "M190", roadmap);
759
- assertEq(
760
- result.codeFilesChanged,
761
- true,
762
- "#1906: codeFilesChanged must be true when real code files were merged",
763
- );
764
- assertTrue(existsSync(join(repo, "real-code.ts")), "real-code.ts merged to main");
765
- }
620
+ test("#1906: codeFilesChanged=false when only .gsd/ metadata merged", () => {
621
+ const repo = freshRepo();
622
+ const wtPath = createAutoWorktree(repo, "M180");
766
623
 
767
- // Tests 20 and 21 for #2151 are in auto-stash-merge.test.ts (node:test format).
624
+ mkdirSync(join(wtPath, ".gsd", "milestones", "M180"), { recursive: true });
625
+ writeFileSync(
626
+ join(wtPath, ".gsd", "milestones", "M180", "SUMMARY.md"),
627
+ "# M180 Summary\n\nThis milestone was planned but not implemented.\n",
628
+ );
629
+ run("git add .", wtPath);
630
+ run('git commit -m "chore: add milestone summary"', wtPath);
768
631
 
769
- } finally {
770
- process.chdir(savedCwd);
771
- for (const d of tempDirs) {
772
- if (existsSync(d)) rmSync(d, { recursive: true, force: true });
773
- }
774
- }
632
+ const roadmap = makeRoadmap("M180", "Metadata-only milestone", []);
775
633
 
776
- report();
777
- }
634
+ const result = mergeMilestoneToMain(repo, "M180", roadmap);
635
+ assert.strictEqual(result.codeFilesChanged, false,
636
+ "#1906: codeFilesChanged must be false when only .gsd/ files were merged");
637
+ });
638
+
639
+ test("#1906: codeFilesChanged=true when real code is merged", () => {
640
+ const repo = freshRepo();
641
+ const wtPath = createAutoWorktree(repo, "M190");
642
+
643
+ addSliceToMilestone(repo, wtPath, "M190", "S01", "Real code", [
644
+ { file: "real-code.ts", content: "export const real = true;\n", message: "add real code" },
645
+ ]);
646
+
647
+ const roadmap = makeRoadmap("M190", "Code milestone", [
648
+ { id: "S01", title: "Real code" },
649
+ ]);
778
650
 
779
- main();
651
+ const result = mergeMilestoneToMain(repo, "M190", roadmap);
652
+ assert.strictEqual(result.codeFilesChanged, true,
653
+ "#1906: codeFilesChanged must be true when real code files were merged");
654
+ assert.ok(existsSync(join(repo, "real-code.ts")), "real-code.ts merged to main");
655
+ });
656
+ });