@opengsd/gsd-pi 1.4.0 → 1.5.0-dev.4fb2d460

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 (657) hide show
  1. package/README.md +11 -11
  2. package/dist/onboarding.js +11 -0
  3. package/dist/resources/.managed-resources-content-hash +1 -1
  4. package/dist/resources/GSD-WORKFLOW.md +20 -25
  5. package/dist/resources/extensions/ask-user-questions.js +1 -1
  6. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +118 -53
  7. package/dist/resources/extensions/cursor-cli/index.js +16 -0
  8. package/dist/resources/extensions/cursor-cli/models.js +63 -0
  9. package/dist/resources/extensions/cursor-cli/package.json +11 -0
  10. package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
  11. package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
  12. package/dist/resources/extensions/gsd/activity-log.js +4 -1
  13. package/dist/resources/extensions/gsd/artifact-verification.js +25 -9
  14. package/dist/resources/extensions/gsd/auto/closeout.js +137 -42
  15. package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
  16. package/dist/resources/extensions/gsd/auto/orchestrator.js +4 -3
  17. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +6 -3
  18. package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
  19. package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
  20. package/dist/resources/extensions/gsd/auto/unit-phase.js +5 -9
  21. package/dist/resources/extensions/gsd/auto-artifact-paths.js +12 -3
  22. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +11 -7
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +31 -7
  24. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
  25. package/dist/resources/extensions/gsd/auto-prompts.js +76 -2
  26. package/dist/resources/extensions/gsd/auto-recovery.js +50 -6
  27. package/dist/resources/extensions/gsd/auto-start.js +8 -6
  28. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +4 -0
  29. package/dist/resources/extensions/gsd/auto-worktree-branch-lifecycle.js +189 -0
  30. package/dist/resources/extensions/gsd/auto-worktree-cleanup.js +133 -0
  31. package/dist/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.js +35 -0
  32. package/dist/resources/extensions/gsd/auto-worktree-creation.js +130 -0
  33. package/dist/resources/extensions/gsd/auto-worktree-entry.js +118 -0
  34. package/dist/resources/extensions/gsd/auto-worktree-merge-already-merged.js +98 -0
  35. package/dist/resources/extensions/gsd/auto-worktree-merge-branch-head.js +52 -0
  36. package/dist/resources/extensions/gsd/auto-worktree-merge-cleanup.js +87 -0
  37. package/dist/resources/extensions/gsd/auto-worktree-merge-code-changes.js +46 -0
  38. package/dist/resources/extensions/gsd/auto-worktree-merge-db-ready.js +68 -0
  39. package/dist/resources/extensions/gsd/auto-worktree-merge-integration-branch.js +51 -0
  40. package/dist/resources/extensions/gsd/auto-worktree-merge-message.js +106 -0
  41. package/dist/resources/extensions/gsd/auto-worktree-merge-pre-teardown.js +64 -0
  42. package/dist/resources/extensions/gsd/auto-worktree-merge-stash.js +168 -0
  43. package/dist/resources/extensions/gsd/auto-worktree-merge.js +302 -0
  44. package/dist/resources/extensions/gsd/auto-worktree-milestone-shelter.js +119 -0
  45. package/dist/resources/extensions/gsd/auto-worktree-path-resolution.js +5 -0
  46. package/dist/resources/extensions/gsd/auto-worktree-path-utils.js +5 -0
  47. package/dist/resources/extensions/gsd/auto-worktree-resource-version.js +41 -0
  48. package/dist/resources/extensions/gsd/auto-worktree-runtime-cleanup.js +99 -0
  49. package/dist/resources/extensions/gsd/auto-worktree-session-registry.js +57 -0
  50. package/dist/resources/extensions/gsd/auto-worktree-sync.js +249 -0
  51. package/dist/resources/extensions/gsd/auto-worktree-teardown.js +120 -0
  52. package/dist/resources/extensions/gsd/auto-worktree.js +20 -2046
  53. package/dist/resources/extensions/gsd/auto.js +31 -5
  54. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -0
  55. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -1
  56. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +134 -7
  57. package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
  58. package/dist/resources/extensions/gsd/bootstrap/system-context.js +1 -1
  59. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +194 -28
  60. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +14 -1
  61. package/dist/resources/extensions/gsd/codebase-generator.js +164 -20
  62. package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
  63. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  64. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +153 -1
  65. package/dist/resources/extensions/gsd/commands-workflow-templates.js +3 -3
  66. package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
  67. package/dist/resources/extensions/gsd/db/queries.js +48 -9
  68. package/dist/resources/extensions/gsd/db/writers/cascades.js +20 -2
  69. package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
  70. package/dist/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  71. package/dist/resources/extensions/gsd/doctor-engine-checks.js +292 -18
  72. package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
  73. package/dist/resources/extensions/gsd/doctor.js +93 -3
  74. package/dist/resources/extensions/gsd/exec-sandbox.js +6 -2
  75. package/dist/resources/extensions/gsd/flat-phase-migration.js +106 -26
  76. package/dist/resources/extensions/gsd/forensics.js +1 -1
  77. package/dist/resources/extensions/gsd/git-conflict-resolve.js +2 -2
  78. package/dist/resources/extensions/gsd/git-service.js +59 -18
  79. package/dist/resources/extensions/gsd/gsd-db.js +75 -11
  80. package/dist/resources/extensions/gsd/guidance.js +1 -0
  81. package/dist/resources/extensions/gsd/guided-flow.js +62 -19
  82. package/dist/resources/extensions/gsd/key-manager.js +1 -0
  83. package/dist/resources/extensions/gsd/markdown-renderer.js +40 -11
  84. package/dist/resources/extensions/gsd/md-importer.js +89 -23
  85. package/dist/resources/extensions/gsd/migrate-external.js +24 -7
  86. package/dist/resources/extensions/gsd/migration-auto-check.js +30 -3
  87. package/dist/resources/extensions/gsd/milestone-merge-transaction.js +14 -3
  88. package/dist/resources/extensions/gsd/milestone-settlement.js +1 -1
  89. package/dist/resources/extensions/gsd/parallel-merge.js +1 -1
  90. package/dist/resources/extensions/gsd/parallel-orchestrator.js +3 -1
  91. package/dist/resources/extensions/gsd/paths.js +17 -3
  92. package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
  93. package/dist/resources/extensions/gsd/preferences-models.js +6 -4
  94. package/dist/resources/extensions/gsd/preferences-skills.js +10 -16
  95. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  96. package/dist/resources/extensions/gsd/preferences-validation.js +107 -2
  97. package/dist/resources/extensions/gsd/preferences.js +49 -2
  98. package/dist/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  99. package/dist/resources/extensions/gsd/prompts/system.md +6 -6
  100. package/dist/resources/extensions/gsd/quick.js +12 -3
  101. package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
  102. package/dist/resources/extensions/gsd/repo-identity.js +16 -0
  103. package/dist/resources/extensions/gsd/repository-registry.js +6 -1
  104. package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
  105. package/dist/resources/extensions/gsd/safe-fs.js +23 -2
  106. package/dist/resources/extensions/gsd/skill-discovery.js +36 -23
  107. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  108. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  109. package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +2 -1
  110. package/dist/resources/extensions/gsd/state/derive/from-db.js +88 -153
  111. package/dist/resources/extensions/gsd/tool-contract.js +14 -0
  112. package/dist/resources/extensions/gsd/tools/plan-task.js +76 -8
  113. package/dist/resources/extensions/gsd/tools/reopen-slice.js +5 -0
  114. package/dist/resources/extensions/gsd/tools/reopen-task.js +13 -5
  115. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
  116. package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
  117. package/dist/resources/extensions/gsd/unit-registry.js +37 -2
  118. package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
  119. package/dist/resources/extensions/gsd/unmerged-milestone-guard.js +1 -1
  120. package/dist/resources/extensions/gsd/validation-block-guard.js +2 -1
  121. package/dist/resources/extensions/gsd/workspace-git-preflight.js +2 -2
  122. package/dist/resources/extensions/gsd/workspace.js +26 -8
  123. package/dist/resources/extensions/gsd/worktree-command.js +1 -1
  124. package/dist/resources/extensions/gsd/worktree-lifecycle.js +81 -14
  125. package/dist/resources/extensions/gsd/worktree-manager.js +97 -4
  126. package/dist/resources/extensions/gsd/worktree-reentry.js +2 -1
  127. package/dist/resources/extensions/shared/interview-ui.js +62 -9
  128. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  129. package/dist/web/standalone/.next/BUILD_ID +1 -1
  130. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  131. package/dist/web/standalone/.next/build-manifest.json +3 -3
  132. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  133. package/dist/web/standalone/.next/required-server-files.json +3 -3
  134. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  135. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  136. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  137. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  141. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  142. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  143. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  144. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  145. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  146. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  147. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  148. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  149. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  150. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  151. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  152. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  153. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  154. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  155. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  156. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  157. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  158. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  159. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  160. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  161. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  162. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  163. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  164. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  165. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  166. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  167. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  168. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  169. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  170. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  171. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  172. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  173. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  174. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  175. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  176. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  177. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  178. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  179. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  180. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  181. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  182. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  183. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  184. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  185. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  186. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  187. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  188. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  189. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  190. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  191. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  192. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
  193. package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
  194. package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
  195. package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
  196. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  197. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  198. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  199. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  200. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  201. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  202. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  203. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  204. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  205. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  206. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  207. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  208. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  209. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  210. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  211. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  212. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  213. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  214. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  215. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  216. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  217. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  218. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  219. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  220. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  221. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  222. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  223. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  224. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  225. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  226. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  227. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  228. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
  229. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  230. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  231. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  232. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  233. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  234. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  235. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  236. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  237. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  238. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  239. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  240. package/dist/web/standalone/.next/server/app/index.html +1 -1
  241. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  242. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  243. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  244. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  245. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  246. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  247. package/dist/web/standalone/.next/server/app/page.js +2 -2
  248. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  249. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  250. package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
  251. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  252. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  253. package/dist/web/standalone/.next/server/middleware.js +3 -3
  254. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  255. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  256. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  257. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
  258. package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
  259. package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
  260. package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
  261. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
  262. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  263. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  264. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  265. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  266. package/dist/web/standalone/server.js +1 -1
  267. package/dist/wizard.d.ts +8 -0
  268. package/dist/wizard.js +8 -1
  269. package/integrations/hermes/CONTEXT.md +47 -0
  270. package/integrations/hermes/README.md +21 -0
  271. package/integrations/hermes/__init__.py +31 -0
  272. package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
  273. package/integrations/hermes/docs/setup.md +4 -0
  274. package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
  275. package/integrations/hermes/open_gsd_hermes/config.py +2 -0
  276. package/integrations/hermes/open_gsd_hermes/gsd_client.py +591 -39
  277. package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
  278. package/integrations/hermes/plugin.yaml +4 -0
  279. package/integrations/hermes/tests/test_config.py +6 -0
  280. package/integrations/hermes/tests/test_directory_loader.py +37 -0
  281. package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
  282. package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
  283. package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
  284. package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
  285. package/integrations/hermes/tests/test_milestone_client.py +447 -0
  286. package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
  287. package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
  288. package/package.json +8 -8
  289. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
  290. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
  291. package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
  292. package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
  293. package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
  294. package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
  295. package/packages/cloud-mcp-gateway/package.json +2 -2
  296. package/packages/contracts/package.json +1 -1
  297. package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
  298. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
  299. package/packages/daemon/dist/cloud-runtime.js +25 -1
  300. package/packages/daemon/dist/cloud-runtime.js.map +1 -1
  301. package/packages/daemon/dist/cloud-runtime.test.js +51 -0
  302. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
  303. package/packages/daemon/dist/message-batcher.d.ts +7 -0
  304. package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
  305. package/packages/daemon/dist/message-batcher.js +13 -0
  306. package/packages/daemon/dist/message-batcher.js.map +1 -1
  307. package/packages/daemon/dist/message-batcher.test.js +48 -0
  308. package/packages/daemon/dist/message-batcher.test.js.map +1 -1
  309. package/packages/daemon/dist/session-manager.js +1 -1
  310. package/packages/daemon/dist/session-manager.js.map +1 -1
  311. package/packages/daemon/package.json +5 -5
  312. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
  313. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  314. package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
  315. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  316. package/packages/gsd-agent-core/dist/export-html/template.js +53 -4
  317. package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
  318. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
  319. package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
  320. package/packages/gsd-agent-core/package.json +5 -5
  321. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  322. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +3 -2
  323. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  324. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -1
  325. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +1 -0
  326. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -1
  327. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +1 -1
  328. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  329. package/packages/gsd-agent-modes/package.json +7 -7
  330. package/packages/mcp-server/dist/server.d.ts +18 -5
  331. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  332. package/packages/mcp-server/dist/server.js +46 -5
  333. package/packages/mcp-server/dist/server.js.map +1 -1
  334. package/packages/mcp-server/dist/session-manager.js +1 -1
  335. package/packages/mcp-server/dist/session-manager.js.map +1 -1
  336. package/packages/mcp-server/package.json +4 -4
  337. package/packages/native/package.json +1 -1
  338. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  339. package/packages/pi-agent-core/dist/agent-loop.js +14 -9
  340. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  341. package/packages/pi-agent-core/package.json +1 -1
  342. package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  343. package/packages/pi-ai/dist/env-api-keys.js +3 -0
  344. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  345. package/packages/pi-ai/dist/index.d.ts +1 -0
  346. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  347. package/packages/pi-ai/dist/index.js +1 -0
  348. package/packages/pi-ai/dist/index.js.map +1 -1
  349. package/packages/pi-ai/dist/providers/transform-messages.d.ts.map +1 -1
  350. package/packages/pi-ai/dist/providers/transform-messages.js +7 -3
  351. package/packages/pi-ai/dist/providers/transform-messages.js.map +1 -1
  352. package/packages/pi-ai/dist/tool-result-content.d.ts +3 -0
  353. package/packages/pi-ai/dist/tool-result-content.d.ts.map +1 -0
  354. package/packages/pi-ai/dist/tool-result-content.js +62 -0
  355. package/packages/pi-ai/dist/tool-result-content.js.map +1 -0
  356. package/packages/pi-ai/dist/types.d.ts +10 -22
  357. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  358. package/packages/pi-ai/dist/types.js.map +1 -1
  359. package/packages/pi-ai/package.json +1 -1
  360. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  361. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  362. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  363. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +9 -0
  364. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  365. package/packages/pi-coding-agent/dist/core/package-manager.js +74 -53
  366. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  367. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +6 -0
  368. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  369. package/packages/pi-coding-agent/dist/core/resource-loader.js +30 -2
  370. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  371. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
  372. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
  373. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
  374. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  375. package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
  376. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  377. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts +66 -0
  378. package/packages/pi-coding-agent/dist/core/skill-directories.d.ts.map +1 -0
  379. package/packages/pi-coding-agent/dist/core/skill-directories.js +138 -0
  380. package/packages/pi-coding-agent/dist/core/skill-directories.js.map +1 -0
  381. package/packages/pi-coding-agent/dist/core/skills.d.ts +4 -1
  382. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  383. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  384. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  385. package/packages/pi-coding-agent/dist/index.d.ts +1 -0
  386. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  387. package/packages/pi-coding-agent/dist/index.js +1 -0
  388. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  389. package/packages/pi-coding-agent/package.json +7 -7
  390. package/packages/pi-tui/package.json +2 -2
  391. package/packages/rpc-client/dist/rpc-client.js +1 -1
  392. package/packages/rpc-client/dist/rpc-client.js.map +1 -1
  393. package/packages/rpc-client/package.json +2 -2
  394. package/pkg/dist/core/export-html/template.js +53 -4
  395. package/pkg/package.json +1 -1
  396. package/scripts/link-workspace-packages.cjs +14 -4
  397. package/src/resources/GSD-WORKFLOW.md +20 -25
  398. package/src/resources/extensions/ask-user-questions.ts +1 -1
  399. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +130 -51
  400. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +371 -39
  401. package/src/resources/extensions/cursor-cli/index.ts +18 -0
  402. package/src/resources/extensions/cursor-cli/models.ts +65 -0
  403. package/src/resources/extensions/cursor-cli/package.json +11 -0
  404. package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
  405. package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
  406. package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
  407. package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
  408. package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
  409. package/src/resources/extensions/gsd/activity-log.ts +4 -1
  410. package/src/resources/extensions/gsd/artifact-verification.ts +25 -9
  411. package/src/resources/extensions/gsd/auto/closeout.ts +163 -58
  412. package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
  413. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  414. package/src/resources/extensions/gsd/auto/orchestrator.ts +4 -3
  415. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +6 -3
  416. package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
  417. package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
  418. package/src/resources/extensions/gsd/auto/unit-phase.ts +5 -9
  419. package/src/resources/extensions/gsd/auto-artifact-paths.ts +12 -3
  420. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +13 -7
  421. package/src/resources/extensions/gsd/auto-dispatch.ts +35 -7
  422. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
  423. package/src/resources/extensions/gsd/auto-prompts.ts +73 -2
  424. package/src/resources/extensions/gsd/auto-recovery.ts +50 -6
  425. package/src/resources/extensions/gsd/auto-start.ts +9 -6
  426. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +4 -0
  427. package/src/resources/extensions/gsd/auto-worktree-branch-lifecycle.ts +237 -0
  428. package/src/resources/extensions/gsd/auto-worktree-cleanup.ts +166 -0
  429. package/src/resources/extensions/gsd/auto-worktree-conflict-auto-resolve.ts +37 -0
  430. package/src/resources/extensions/gsd/auto-worktree-creation.ts +171 -0
  431. package/src/resources/extensions/gsd/auto-worktree-entry.ts +154 -0
  432. package/src/resources/extensions/gsd/auto-worktree-merge-already-merged.ts +161 -0
  433. package/src/resources/extensions/gsd/auto-worktree-merge-branch-head.ts +83 -0
  434. package/src/resources/extensions/gsd/auto-worktree-merge-cleanup.ts +132 -0
  435. package/src/resources/extensions/gsd/auto-worktree-merge-code-changes.ts +71 -0
  436. package/src/resources/extensions/gsd/auto-worktree-merge-db-ready.ts +112 -0
  437. package/src/resources/extensions/gsd/auto-worktree-merge-integration-branch.ts +99 -0
  438. package/src/resources/extensions/gsd/auto-worktree-merge-message.ts +166 -0
  439. package/src/resources/extensions/gsd/auto-worktree-merge-pre-teardown.ts +93 -0
  440. package/src/resources/extensions/gsd/auto-worktree-merge-stash.ts +213 -0
  441. package/src/resources/extensions/gsd/auto-worktree-merge.ts +391 -0
  442. package/src/resources/extensions/gsd/auto-worktree-milestone-shelter.ts +159 -0
  443. package/src/resources/extensions/gsd/auto-worktree-path-resolution.ts +6 -0
  444. package/src/resources/extensions/gsd/auto-worktree-path-utils.ts +10 -0
  445. package/src/resources/extensions/gsd/auto-worktree-resource-version.ts +44 -0
  446. package/src/resources/extensions/gsd/auto-worktree-runtime-cleanup.ts +119 -0
  447. package/src/resources/extensions/gsd/auto-worktree-session-registry.ts +71 -0
  448. package/src/resources/extensions/gsd/auto-worktree-sync.ts +370 -0
  449. package/src/resources/extensions/gsd/auto-worktree-teardown.ts +161 -0
  450. package/src/resources/extensions/gsd/auto-worktree.ts +69 -2390
  451. package/src/resources/extensions/gsd/auto.ts +32 -19
  452. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -0
  453. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -1
  454. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +148 -7
  455. package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
  456. package/src/resources/extensions/gsd/bootstrap/system-context.ts +1 -1
  457. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +229 -28
  458. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +23 -1
  459. package/src/resources/extensions/gsd/codebase-generator.ts +186 -19
  460. package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
  461. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  462. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +135 -1
  463. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -3
  464. package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
  465. package/src/resources/extensions/gsd/db/queries.ts +49 -9
  466. package/src/resources/extensions/gsd/db/writers/cascades.ts +23 -1
  467. package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
  468. package/src/resources/extensions/gsd/docs/preferences-reference.md +8 -2
  469. package/src/resources/extensions/gsd/doctor-engine-checks.ts +372 -22
  470. package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
  471. package/src/resources/extensions/gsd/doctor-types.ts +5 -1
  472. package/src/resources/extensions/gsd/doctor.ts +95 -3
  473. package/src/resources/extensions/gsd/exec-sandbox.ts +6 -2
  474. package/src/resources/extensions/gsd/flat-phase-migration.ts +132 -25
  475. package/src/resources/extensions/gsd/forensics.ts +1 -1
  476. package/src/resources/extensions/gsd/git-conflict-resolve.ts +2 -2
  477. package/src/resources/extensions/gsd/git-service.ts +67 -17
  478. package/src/resources/extensions/gsd/gsd-db.ts +105 -11
  479. package/src/resources/extensions/gsd/guidance.ts +2 -0
  480. package/src/resources/extensions/gsd/guided-flow.ts +71 -20
  481. package/src/resources/extensions/gsd/key-manager.ts +1 -0
  482. package/src/resources/extensions/gsd/markdown-renderer.ts +40 -10
  483. package/src/resources/extensions/gsd/md-importer.ts +93 -22
  484. package/src/resources/extensions/gsd/migrate-external.ts +23 -6
  485. package/src/resources/extensions/gsd/migration-auto-check.ts +32 -3
  486. package/src/resources/extensions/gsd/milestone-merge-transaction.ts +19 -4
  487. package/src/resources/extensions/gsd/milestone-settlement.ts +1 -1
  488. package/src/resources/extensions/gsd/model-router.ts +4 -5
  489. package/src/resources/extensions/gsd/parallel-merge.ts +1 -1
  490. package/src/resources/extensions/gsd/parallel-orchestrator.ts +3 -1
  491. package/src/resources/extensions/gsd/paths.ts +21 -3
  492. package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
  493. package/src/resources/extensions/gsd/preferences-models.ts +6 -3
  494. package/src/resources/extensions/gsd/preferences-skills.ts +10 -16
  495. package/src/resources/extensions/gsd/preferences-types.ts +30 -0
  496. package/src/resources/extensions/gsd/preferences-validation.ts +97 -2
  497. package/src/resources/extensions/gsd/preferences.ts +58 -4
  498. package/src/resources/extensions/gsd/prompts/plan-slice.md +4 -2
  499. package/src/resources/extensions/gsd/prompts/system.md +6 -6
  500. package/src/resources/extensions/gsd/quick.ts +11 -3
  501. package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
  502. package/src/resources/extensions/gsd/repo-identity.ts +17 -0
  503. package/src/resources/extensions/gsd/repository-registry.ts +7 -1
  504. package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
  505. package/src/resources/extensions/gsd/safe-fs.ts +22 -2
  506. package/src/resources/extensions/gsd/skill-discovery.ts +47 -23
  507. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  508. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +1 -0
  509. package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +3 -1
  510. package/src/resources/extensions/gsd/state/derive/from-db.ts +201 -157
  511. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +169 -1
  512. package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +2 -1
  513. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
  514. package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
  515. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
  516. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
  517. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +111 -0
  518. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
  519. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
  520. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
  521. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
  522. package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +1 -1
  523. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
  524. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +10 -1
  525. package/src/resources/extensions/gsd/tests/auto-worktree-auto-resolve.test.ts +1 -1
  526. package/src/resources/extensions/gsd/tests/auto-worktree-merge-already-merged.test.ts +177 -0
  527. package/src/resources/extensions/gsd/tests/auto-worktree-merge-branch-head.test.ts +118 -0
  528. package/src/resources/extensions/gsd/tests/auto-worktree-merge-cleanup.test.ts +115 -0
  529. package/src/resources/extensions/gsd/tests/auto-worktree-merge-code-changes.test.ts +76 -0
  530. package/src/resources/extensions/gsd/tests/auto-worktree-merge-db-ready.test.ts +140 -0
  531. package/src/resources/extensions/gsd/tests/auto-worktree-merge-integration-branch.test.ts +123 -0
  532. package/src/resources/extensions/gsd/tests/auto-worktree-merge-message.test.ts +114 -0
  533. package/src/resources/extensions/gsd/tests/auto-worktree-merge-pre-teardown.test.ts +84 -0
  534. package/src/resources/extensions/gsd/tests/auto-worktree-merge-stash.test.ts +70 -0
  535. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-shelter.test.ts +52 -0
  536. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +6 -6
  537. package/src/resources/extensions/gsd/tests/auto-worktree-resource-version.test.ts +59 -0
  538. package/src/resources/extensions/gsd/tests/auto-worktree-untracked-content.test.ts +3 -1
  539. package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
  540. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
  541. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +387 -0
  542. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
  543. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +1 -1
  544. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +4 -0
  545. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
  546. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +571 -5
  547. package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
  548. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +1 -1
  549. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +109 -2
  550. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +25 -1
  551. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
  552. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
  553. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +40 -0
  554. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
  555. package/src/resources/extensions/gsd/tests/import-completion-lost-update.test.ts +196 -0
  556. package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +4 -6
  557. package/src/resources/extensions/gsd/tests/integration/auto-stash-merge.test.ts +2 -1
  558. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +3 -3
  559. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +7 -8
  560. package/src/resources/extensions/gsd/tests/integration/feature-branch-lifecycle-integration.test.ts +2 -5
  561. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +76 -0
  562. package/src/resources/extensions/gsd/tests/integration/merge-cwd-restore.test.ts +1 -1
  563. package/src/resources/extensions/gsd/tests/integration/merge-preserve-worktree-on-preteardown-error.test.ts +1 -1
  564. package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +2 -1
  565. package/src/resources/extensions/gsd/tests/integration/milestone-transition-worktree.test.ts +5 -7
  566. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  567. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +2 -4
  568. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +16 -15
  569. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
  570. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +1 -1
  571. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
  572. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +1 -1
  573. package/src/resources/extensions/gsd/tests/merge-self-branch-guard.test.ts +1 -1
  574. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
  575. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +91 -0
  576. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +592 -23
  577. package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +25 -1
  578. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +1 -1
  579. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -3
  580. package/src/resources/extensions/gsd/tests/parallel-worker-lock-contention.test.ts +1 -1
  581. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
  582. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
  583. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +17 -0
  584. package/src/resources/extensions/gsd/tests/plan-task.test.ts +264 -1
  585. package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +1 -4
  586. package/src/resources/extensions/gsd/tests/preferences.test.ts +169 -0
  587. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +147 -0
  588. package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
  589. package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
  590. package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
  591. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +275 -0
  592. package/src/resources/extensions/gsd/tests/reopen-slice.test.ts +52 -0
  593. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
  594. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
  595. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
  596. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +20 -0
  597. package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
  598. package/src/resources/extensions/gsd/tests/skill-discovery.test.ts +46 -0
  599. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +3 -5
  600. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -0
  601. package/src/resources/extensions/gsd/tests/stash-pop-gsd-conflict.test.ts +2 -1
  602. package/src/resources/extensions/gsd/tests/stash-queued-context-files.test.ts +2 -1
  603. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
  604. package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
  605. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +1 -1
  606. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +1 -1
  607. package/src/resources/extensions/gsd/tests/system-flat-phase-layout.test.ts +33 -0
  608. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +4 -4
  609. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +2 -1
  610. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +4 -4
  611. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  612. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +259 -0
  613. package/src/resources/extensions/gsd/tests/uat-stuck-loop-orphaned-worktree.test.ts +1 -1
  614. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
  615. package/src/resources/extensions/gsd/tests/unit-registry.test.ts +81 -0
  616. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  617. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +52 -0
  618. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
  619. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +208 -1
  620. package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
  621. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -1
  622. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +1 -1
  623. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +1 -3
  624. package/src/resources/extensions/gsd/tests/worktree-expected-warnings.test.ts +1 -1
  625. package/src/resources/extensions/gsd/tests/worktree-git-pathspec.test.ts +1 -1
  626. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
  627. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
  628. package/src/resources/extensions/gsd/tests/worktree-main-branch.test.ts +1 -1
  629. package/src/resources/extensions/gsd/tests/worktree-preferences-sync.test.ts +1 -1
  630. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +2 -1
  631. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +36 -3
  632. package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +1 -1
  633. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +1 -1
  634. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
  635. package/src/resources/extensions/gsd/tool-contract.ts +29 -0
  636. package/src/resources/extensions/gsd/tools/plan-task.ts +107 -9
  637. package/src/resources/extensions/gsd/tools/reopen-slice.ts +5 -0
  638. package/src/resources/extensions/gsd/tools/reopen-task.ts +13 -5
  639. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
  640. package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
  641. package/src/resources/extensions/gsd/unit-registry.ts +48 -2
  642. package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
  643. package/src/resources/extensions/gsd/unmerged-milestone-guard.ts +1 -1
  644. package/src/resources/extensions/gsd/validation-block-guard.ts +2 -1
  645. package/src/resources/extensions/gsd/workspace-git-preflight.ts +2 -2
  646. package/src/resources/extensions/gsd/workspace.ts +42 -8
  647. package/src/resources/extensions/gsd/worktree-command.ts +1 -1
  648. package/src/resources/extensions/gsd/worktree-lifecycle.ts +154 -26
  649. package/src/resources/extensions/gsd/worktree-manager.ts +143 -5
  650. package/src/resources/extensions/gsd/worktree-reentry.ts +2 -1
  651. package/src/resources/extensions/shared/interview-ui.ts +58 -11
  652. package/src/resources/extensions/shared/tests/interview-ui-border.test.ts +32 -0
  653. package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
  654. package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
  655. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
  656. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → gXoWec4UNqcR0tkuHwYCr}/_buildManifest.js +0 -0
  657. /package/dist/web/standalone/.next/static/{zQLcXm0Za8lexoXtgDpMG → gXoWec4UNqcR0tkuHwYCr}/_ssgManifest.js +0 -0
@@ -1,2392 +1,71 @@
1
1
  // Project/App: gsd-pi
2
- // File Purpose: Auto-mode worktree lifecycle, merge, and cleanup management.
3
-
4
- /**
5
- * GSD Auto-Worktree -- lifecycle management for auto-mode worktrees.
6
- *
7
- * Auto-mode creates worktrees with `milestone/<MID>` branches (distinct from
8
- * manual `/worktree` which uses `worktree/<name>` branches). This module
9
- * manages create, enter, detect, and teardown for auto-mode worktrees.
10
- */
11
-
12
- import {
13
- existsSync,
14
- cpSync,
15
- readFileSync,
16
- readdirSync,
17
- mkdirSync,
18
- realpathSync,
19
- rmSync,
20
- unlinkSync,
21
- lstatSync as lstatSyncFn,
22
- } from "node:fs";
23
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
24
- import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
25
- import {
26
- reconcileWorktreeDb,
27
- isDbAvailable,
28
- getMilestone,
29
- getMilestoneSlices,
30
- getSliceTasks,
31
- } from "./gsd-db.js";
32
- import {
33
- closeWorkflowDatabase,
34
- getWorkflowDatabasePath,
35
- openWorkflowDatabasePath,
36
- } from "./db-workspace.js";
37
- import { atomicWriteSync } from "./atomic-write.js";
38
- import { execFileSync } from "node:child_process";
39
- import {
40
- canonicalPhaseDirName,
41
- gsdRoot,
42
- milestonesDir,
43
- resolveGsdPathContract,
44
- resolveMilestonePath,
45
- } from "./paths.js";
46
- import {
47
- createWorktree,
48
- removeWorktree,
49
- resolveGitDir,
50
- worktreePath,
51
- isInsideWorktreesDir,
52
- } from "./worktree-manager.js";
53
- import { worktreePathFor } from "./worktree-placement.js";
54
- import {
55
- detectWorktreeName,
56
- resolveGitHeadPath,
57
- nudgeGitBranchCache,
58
- } from "./worktree.js";
59
- import {
60
- isGsdWorktreePath,
61
- projectRootFromWorktreePath,
62
- normalizeWorktreePathForCompare,
63
- resolveWorktreeProjectRoot,
64
- } from "./worktree-root.js";
65
- import { autoResolveSafeConflictPaths } from "./git-conflict-resolve.js";
66
- import { MergeConflictError, readIntegrationBranch, resolveMilestoneIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
67
- import { publishMilestone } from "./publication.js";
68
- import { debugLog } from "./debug-logger.js";
69
- import { logWarning, logError } from "./workflow-logger.js";
70
- import {
71
- checkoutBranchWithStashGuard,
72
- cleanupConflictState,
73
- gsdJsonlFilesWithConflictMarkers,
74
- hasConflictMarkers,
75
- popStashByRef,
76
- removeMergeStateFiles,
77
- stashAlreadyExistsFilesFromError,
78
- stashRefFromError,
79
- } from "./worktree-git-recovery.js";
80
- import { loadEffectiveGSDPreferences } from "./preferences.js";
81
- import { MILESTONE_ID_RE } from "./milestone-ids.js";
82
- import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
83
- import { classifyProject } from "./detection.js";
84
- import {
85
- nativeGetCurrentBranch,
86
- nativeDetectMainBranch,
87
- nativeWorkingTreeStatus,
88
- nativeAddAllWithExclusions,
89
- nativeCommit,
90
- nativeCheckoutBranch,
91
- nativeMergeSquash,
92
- nativeMergeRegular,
93
- nativeConflictFiles,
94
- nativeCheckoutTheirs,
95
- nativeAddPaths,
96
- nativeRmForce,
97
- nativeBranchDelete,
98
- nativeBranchForceReset,
99
- nativeBranchExists,
100
- nativeDiffNumstat,
101
- nativeUpdateRef,
102
- nativeIsAncestor,
103
- nativeWorktreeList,
104
- nativeLsFiles,
105
- } from "./native-git-bridge.js";
106
- import {
107
- CLOSEOUT_CONSISTENCY_BLOCKED_REASON,
108
- } from "./closeout-consistency-gate.js";
109
- import {
110
- formatCloseoutProofBlock,
111
- proveMilestoneCloseout,
112
- } from "./milestone-closeout-proof.js";
113
- import { gsdHome } from "./gsd-home.js";
114
- import { type MilestoneScope, type GsdWorkspace, createWorkspace } from "./workspace.js";
115
- import { WorktreeStateProjection } from "./worktree-state-projection.js";
116
-
117
- const PROJECT_PREFERENCES_FILE = "PREFERENCES.md";
118
- const LEGACY_PROJECT_PREFERENCES_FILE = "preferences.md";
119
- const LEGACY_DEEP_SETUP_RUNTIME_UNIT_FILES = new Set([
120
- "workflow-preferences-WORKFLOW-PREFS.json",
121
- "discuss-project-PROJECT.json",
122
- "discuss-requirements-REQUIREMENTS.json",
123
- "research-decision-RESEARCH-DECISION.json",
124
- "research-project-RESEARCH-PROJECT.json",
125
- ]);
126
-
127
- // ─── Shared Constants & Helpers ─────────────────────────────────────────────
128
-
129
- /**
130
- * Root-level .gsd/ projections copied from project root into worktrees for
131
- * compatibility. Project root remains the canonical state/projection root.
132
- */
133
- const ROOT_STATE_FILES = [
134
- "DECISIONS.md",
135
- "REQUIREMENTS.md",
136
- "PROJECT.md",
137
- "KNOWLEDGE.md",
138
- "OVERRIDES.md",
139
- "QUEUE.md",
140
- "completed-units.json",
141
- "metrics.json",
142
- "mcp.json",
143
- // NOTE: project preferences are intentionally NOT in ROOT_STATE_FILES.
144
- // Forward-sync (main → worktree) is handled explicitly in syncGsdStateToWorktree().
145
- // Back-sync (worktree → main) must NEVER overwrite the project root's copy
146
- // because the project root is authoritative for preferences (#2684).
147
- ] as const;
148
-
149
- /**
150
- * Check if two filesystem paths resolve to the same real location.
151
- * Returns false if either path cannot be resolved (e.g. doesn't exist).
152
- */
153
- function isSamePath(a: string, b: string): boolean {
154
- try {
155
- return realpathSync(a) === realpathSync(b);
156
- } catch (e) {
157
- if ((e as NodeJS.ErrnoException).code === "ENOENT") return false;
158
- logWarning("worktree", `isSamePath failed: ${(e as Error).message}`);
159
- return false;
160
- }
161
- }
162
-
163
- export function _isSamePath(a: string, b: string): boolean {
164
- return isSamePath(a, b);
165
- }
166
-
167
- export function _resolveAutoWorktreeStartPoint(
168
- integrationBranch: string | null | undefined,
169
- gitMainBranch: string | null | undefined,
170
- branchExists: (branch: string) => boolean,
171
- ): string | undefined {
172
- if (integrationBranch) return integrationBranch;
173
- return gitMainBranch &&
174
- typeof gitMainBranch === "string" &&
175
- gitMainBranch.length > 0 &&
176
- branchExists(gitMainBranch)
177
- ? gitMainBranch
178
- : undefined;
179
- }
180
-
181
- function importUntrackedProjectRootContentIntoEmptyWorktree(
182
- projectRoot: string,
183
- worktreeRoot: string,
184
- milestoneId: string,
185
- ): number {
186
- const worktreeClassification = classifyProject(worktreeRoot);
187
- if (worktreeClassification.kind !== "greenfield") return 0;
188
-
189
- const projectRootClassification = classifyProject(projectRoot);
190
- if (
191
- projectRootClassification.kind === "greenfield" ||
192
- projectRootClassification.kind === "invalid-repo" ||
193
- projectRootClassification.untrackedFiles.length === 0
194
- ) {
195
- return 0;
196
- }
197
-
198
- let copied = 0;
199
- for (const relPath of projectRootClassification.untrackedFiles) {
200
- const src = join(projectRoot, relPath);
201
- if (!existsSync(src)) continue;
202
-
203
- const dst = join(worktreeRoot, relPath);
204
- if (existsSync(dst)) continue;
205
-
206
- mkdirSync(dirname(dst), { recursive: true });
207
- cpSync(src, dst, { recursive: true, force: false });
208
- copied++;
209
- }
210
-
211
- if (copied > 0) {
212
- debugLog("createAutoWorktree", {
213
- phase: "import-untracked-project-content",
214
- milestoneId,
215
- copied,
216
- });
217
- }
218
-
219
- return copied;
220
- }
221
-
222
- export function _shouldReconcileWorktreeDb(
223
- worktreeDbPath: string,
224
- mainDbPath: string,
225
- pathExists: (path: string) => boolean = existsSync,
226
- samePath: (a: string, b: string) => boolean = isSamePath,
227
- ): boolean {
228
- return pathExists(worktreeDbPath) && !samePath(worktreeDbPath, mainDbPath);
229
- }
230
-
231
- export function _isExpectedWorktreeUnlinkError(
232
- code: string | undefined,
233
- ): boolean {
234
- return code === "ENOENT" || code === "EISDIR";
235
- }
236
-
237
- function stripGsdDisplayPrefix(value: string | undefined | null, id: string): string | undefined {
238
- const raw = String(value ?? "").trim();
239
- if (!raw) return undefined;
240
- const lower = raw.toLowerCase();
241
- const idLower = id.toLowerCase();
242
- if (lower.startsWith(`${idLower}:`)) return raw.slice(id.length + 1).trim() || undefined;
243
- return raw;
244
- }
245
-
246
- // ─── Module State ──────────────────────────────────────────────────────────
247
-
248
- /** Active workspace registry — replaces the legacy `originalBase` singleton. */
249
- let activeWorkspace: GsdWorkspace | null = null;
250
-
251
- /**
252
- * Optional override for the shelter restore copy step (milestone merge #2505).
253
- * Production leaves this null so restoreShelter uses the real cpSync; tests
254
- * inject a throwing function to deterministically exercise the best-effort
255
- * failure path (auto-worktree.ts:1809) and the shelter-retention guarantee,
256
- * which is otherwise unreachable because shelter and restore run synchronously
257
- * in one mergeMilestoneToMain call (the filesystem cannot change between them).
258
- * @internal
259
- */
260
- let _restoreEntryFn: ((src: string, dest: string) => void) | null = null;
261
-
262
- function setActiveWorkspace(ws: GsdWorkspace | null): void {
263
- activeWorkspace = ws;
264
- }
265
-
266
- function getActiveWorkspace(): GsdWorkspace | null {
267
- return activeWorkspace;
268
- }
269
-
270
- function gitPathspecForWorktreePath(basePath: string, targetPath: string): string | null {
271
- let base = basePath;
272
- let target = targetPath;
273
- try {
274
- base = execFileSync("git", ["rev-parse", "--show-toplevel"], {
275
- cwd: basePath,
276
- stdio: ["ignore", "pipe", "ignore"],
277
- encoding: "utf-8",
278
- }).trim() || basePath;
279
- } catch {
280
- /* keep original */
281
- void base;
282
- }
283
- try {
284
- base = realpathSync.native(base);
285
- } catch {
286
- /* keep original */
287
- void base;
288
- }
289
- try {
290
- target = realpathSync.native(targetPath);
291
- } catch {
292
- /* keep original */
293
- void target;
294
- }
295
-
296
- const rel = relative(base, target);
297
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return null;
298
- return rel.replaceAll("\\", "/");
299
- }
300
-
301
- export function _gitPathspecForWorktreePath(basePath: string, targetPath: string): string | null {
302
- return gitPathspecForWorktreePath(basePath, targetPath);
303
- }
304
-
305
- function findRegularMergeChangedPaths(basePath: string, milestoneBranch: string, mainBranch: string): Set<string> {
306
- const changedPaths = new Set<string>();
307
- let mergeLog = "";
308
- try {
309
- mergeLog = execFileSync("git", ["rev-list", "--merges", "--parents", mainBranch], {
310
- cwd: basePath,
311
- stdio: ["ignore", "pipe", "pipe"],
312
- encoding: "utf-8",
313
- }).trim();
314
- } catch (err) {
315
- logWarning("worktree", `regular merge lookup failed: ${err instanceof Error ? err.message : String(err)}`);
316
- return changedPaths;
317
- }
318
-
319
- for (const line of mergeLog.split("\n").filter(Boolean)) {
320
- const [mergeCommit, firstParent, ...otherParents] = line.split(" ");
321
- if (!mergeCommit || !firstParent || otherParents.length === 0) continue;
322
- const mergedMilestone = otherParents.some((parent) => {
323
- try {
324
- return nativeIsAncestor(basePath, milestoneBranch, parent);
325
- } catch {
326
- return false;
327
- }
328
- });
329
- if (!mergedMilestone) continue;
330
-
331
- try {
332
- const output = execFileSync("git", ["diff", "--name-only", firstParent, mergeCommit], {
333
- cwd: basePath,
334
- stdio: ["ignore", "pipe", "pipe"],
335
- encoding: "utf-8",
336
- }).trim();
337
- for (const path of output.split("\n").filter(Boolean)) {
338
- if (!path.startsWith(".gsd/")) changedPaths.add(path);
339
- }
340
- } catch (err) {
341
- logWarning("worktree", `regular merge diff lookup failed: ${err instanceof Error ? err.message : String(err)}`);
342
- }
343
- return changedPaths;
344
- }
345
-
346
- return changedPaths;
347
- }
348
-
349
- function clearProjectRootStateFiles(basePath: string, milestoneId: string): void {
350
- const gsdDir = gsdRoot(basePath);
351
- // Phase C pt 2: auto.lock removed from this list — the file is gone
352
- // (migrated to the workers + unit_dispatches + runtime_kv tables). The
353
- // remaining transient files (STATE.md, {MID}-META.json) are still
354
- // worth removing on teardown.
355
- const transientFiles = [
356
- join(gsdDir, "STATE.md"),
357
- join(gsdDir, "milestones", milestoneId, `${milestoneId}-META.json`),
358
- ];
359
-
360
- for (const file of transientFiles) {
361
- try {
362
- unlinkSync(file);
363
- } catch (err) {
364
- // ENOENT is expected — file may not exist (#3597)
365
- if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
366
- logWarning("worktree", `file unlink failed: ${err instanceof Error ? err.message : String(err)}`);
367
- }
368
- }
369
- }
370
-
371
- // Clean up legacy synced milestone directories and runtime/units.
372
- // Older versions copied these into the project root during execution.
373
- // If they remain as untracked files when we attempt
374
- // `git merge --squash`, git rejects the merge with "local changes would
375
- // be overwritten", causing silent data loss (#1738).
376
- const syncedDirs = [
377
- join(gsdDir, "milestones", milestoneId),
378
- join(gsdDir, "runtime", "units"),
379
- ];
380
-
381
- for (const dir of syncedDirs) {
382
- try {
383
- if (existsSync(dir)) {
384
- const pathspec = gitPathspecForWorktreePath(basePath, dir);
385
- if (!pathspec) continue;
386
-
387
- // Only remove files that are untracked by git — tracked files are
388
- // managed by the branch checkout and should not be deleted.
389
- const untrackedOutput = execFileSync(
390
- "git",
391
- ["ls-files", "--others", "--exclude-standard", pathspec],
392
- { cwd: basePath, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
393
- ).trim();
394
- if (untrackedOutput) {
395
- for (const f of untrackedOutput.split("\n").filter(Boolean)) {
396
- try {
397
- unlinkSync(join(basePath, f));
398
- } catch (err) {
399
- // ENOENT/EISDIR are expected for already-removed or directory entries (#3597)
400
- const code = (err as NodeJS.ErrnoException).code;
401
- if (!_isExpectedWorktreeUnlinkError(code)) {
402
- logWarning("worktree", `untracked file unlink failed: ${err instanceof Error ? err.message : String(err)}`);
403
- }
404
- }
405
- }
406
- }
407
- }
408
- } catch (err) {
409
- /* non-fatal — git command may fail if not in repo */
410
- logWarning("worktree", `untracked file cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
411
- }
412
- }
413
- }
414
-
415
- // ─── Build Artifact Auto-Resolve ─────────────────────────────────────────────
416
-
417
- /** Patterns for machine-generated build artifacts that can be safely
418
- * auto-resolved by accepting --theirs during merge. These files are
419
- * regenerable and never contain meaningful manual edits. */
420
- export const SAFE_AUTO_RESOLVE_PATTERNS: RegExp[] = [
421
- /\.tsbuildinfo$/,
422
- /\.pyc$/,
423
- /\/__pycache__\//,
424
- /\.DS_Store$/,
425
- /\.map$/,
426
- // Regenerable dependency lockfiles. These are fully derivable from their
427
- // manifests by the package manager, so accepting the merge side during
428
- // auto-resolve clears the conflict markers without losing meaningful edits —
429
- // and stops auto-mode from hard-pausing on a lockfile conflict (issue #828).
430
- /(?:^|\/)pnpm-lock\.yaml$/,
431
- /(?:^|\/)package-lock\.json$/,
432
- /(?:^|\/)npm-shrinkwrap\.json$/,
433
- /(?:^|\/)yarn\.lock$/,
434
- /(?:^|\/)bun\.lockb?$/,
435
- /(?:^|\/)Cargo\.lock$/,
436
- /(?:^|\/)composer\.lock$/,
437
- /(?:^|\/)Gemfile\.lock$/,
438
- /(?:^|\/)Pipfile\.lock$/,
439
- /(?:^|\/)poetry\.lock$/,
440
- ];
441
-
442
- /** Returns true if the file path is safe to auto-resolve during merge.
443
- * Covers `.gsd/` state files, common build artifacts, and regenerable
444
- * dependency lockfiles. */
445
- export const isSafeToAutoResolve = (filePath: string): boolean =>
446
- filePath.startsWith(".gsd/") ||
447
- SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
448
-
449
- // ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
450
-
451
- /**
452
- * Sync milestone artifacts from project root INTO worktree before deriveState.
453
- * Covers the case where the LLM wrote artifacts to the main repo filesystem
454
- * (e.g. via absolute paths) but the worktree has stale data. Also deletes
455
- * gsd.db in the worktree so it rebuilds from fresh disk state (#853).
456
- * Non-fatal — sync failure should never block dispatch.
457
- */
458
- /**
459
- * Path-string entry point to WorktreeStateProjection.projectRootToWorktree.
460
- * Production code goes through the Module class; this delegator survives so
461
- * the projection-invariant tests (#1886, #2184, #2478, #2821) can exercise
462
- * the bodies with raw paths.
463
- */
464
- export function syncProjectRootToWorktree(
465
- projectRoot: string,
466
- worktreePath_: string,
467
- milestoneId: string | null,
468
- ): void {
469
- new WorktreeStateProjection().projectRootToWorktreePaths(projectRoot, worktreePath_, milestoneId);
470
- }
471
-
472
- /**
473
- * Path-string entry point to WorktreeStateProjection.projectWorktreeToRoot.
474
- * Production code goes through the Module class; this delegator survives so
475
- * the projection-invariant tests can exercise the body with raw paths.
476
- */
477
- export function syncStateToProjectRoot(
478
- worktreePath_: string,
479
- projectRoot: string,
480
- milestoneId: string | null,
481
- ): void {
482
- new WorktreeStateProjection().projectWorktreeToRootPaths(worktreePath_, projectRoot, milestoneId);
483
- }
484
-
485
- // ─── Resource Staleness ───────────────────────────────────────────────────
486
-
487
- /**
488
- * Read the resource version (semver) from the managed-resources manifest.
489
- * Uses gsdVersion instead of syncedAt so that launching a second session
490
- * doesn't falsely trigger staleness (#804).
491
- */
492
- export function readResourceVersion(): string | null {
493
- const agentDir =
494
- process.env.GSD_CODING_AGENT_DIR || join(gsdHome(), "agent");
495
- const manifestPath = join(agentDir, "managed-resources.json");
496
- try {
497
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
498
- return typeof manifest?.gsdVersion === "string"
499
- ? manifest.gsdVersion
500
- : null;
501
- } catch (e) {
502
- logWarning("worktree", `readResourceVersion failed: ${(e as Error).message}`);
503
- return null;
504
- }
505
- }
506
-
507
- /**
508
- * Check if managed resources have been updated since session start.
509
- * Returns a warning message if stale, null otherwise.
510
- */
511
- export function checkResourcesStale(
512
- versionOnStart: string | null,
513
- ): string | null {
514
- if (versionOnStart === null) return null;
515
- const current = readResourceVersion();
516
- if (current === null) return null;
517
- if (current !== versionOnStart) {
518
- return "GSD resources were updated since this session started. Restart gsd to load the new code.";
519
- }
520
- return null;
521
- }
522
-
523
- // ─── Stale Worktree Escape ────────────────────────────────────────────────
524
-
525
- /**
526
- * Detect and escape a stale worktree cwd (#608).
527
- *
528
- * After milestone completion + merge, the worktree directory is removed but
529
- * the process cwd may still point inside `.gsd/worktrees/<MID>/`.
530
- * When a new session starts, `process.cwd()` is passed as `base` to startAuto
531
- * and all subsequent writes land in the wrong directory. This function detects
532
- * that scenario and chdir back to the project root.
533
- *
534
- * Returns the corrected base path.
535
- */
536
- export function escapeStaleWorktree(base: string): string {
537
- const projectRoot = projectRootFromWorktreePath(base);
538
- if (projectRoot === null) return base;
539
-
540
- // Guard: If the candidate project root's .gsd IS the user-level ~/.gsd,
541
- // the string-slice heuristic matched the wrong /.gsd/ boundary. This happens
542
- // when .gsd is a symlink into ~/.gsd/projects/<hash> and process.cwd()
543
- // resolved through the symlink. Returning ~ would be catastrophic (#1676).
544
- const candidateGsd = normalizeWorktreePathForCompare(join(projectRoot, ".gsd"));
545
- const gsdHomeNorm = normalizeWorktreePathForCompare(gsdHome());
546
- if (candidateGsd === gsdHomeNorm || candidateGsd.startsWith(gsdHomeNorm + "/")) {
547
- // Don't chdir to home — return base unchanged.
548
- // resolveProjectRoot() in worktree.ts has the full git-file-based recovery
549
- // and will be called by the caller (startAuto → projectRoot()).
550
- return base;
551
- }
552
-
553
- try {
554
- process.chdir(projectRoot);
555
- } catch (e) {
556
- // If chdir fails, return the original — caller will handle errors downstream
557
- logWarning("worktree", `escapeStaleWorktree chdir failed: ${(e as Error).message}`);
558
- return base;
559
- }
560
- return projectRoot;
561
- }
562
-
563
- /**
564
- * Clean stale runtime unit files for completed milestones.
565
- *
566
- * After restart, stale runtime/units/*.json from prior milestones can
567
- * cause deriveState to resume the wrong milestone (#887). Removes files
568
- * for milestones that have a SUMMARY (fully complete).
569
- */
570
- export function cleanStaleRuntimeUnits(
571
- gsdRootPath: string,
572
- hasMilestoneSummary: (mid: string) => boolean,
573
- ): number {
574
- const runtimeUnitsDir = join(gsdRootPath, "runtime", "units");
575
- if (!existsSync(runtimeUnitsDir)) return 0;
576
-
577
- let cleaned = 0;
578
- try {
579
- for (const file of readdirSync(runtimeUnitsDir)) {
580
- if (!file.endsWith(".json")) continue;
581
- if (LEGACY_DEEP_SETUP_RUNTIME_UNIT_FILES.has(file)) {
582
- try {
583
- unlinkSync(join(runtimeUnitsDir, file));
584
- cleaned++;
585
- } catch (err) {
586
- /* non-fatal */
587
- logWarning("worktree", `stale runtime unit unlink failed (${file}): ${err instanceof Error ? err.message : String(err)}`);
588
- }
589
- continue;
590
- }
591
- const staleDiscussMatch = file.match(/^discuss-milestone-(.+)\.json$/);
592
- if (staleDiscussMatch && !MILESTONE_ID_RE.test(staleDiscussMatch[1])) {
593
- try {
594
- unlinkSync(join(runtimeUnitsDir, file));
595
- cleaned++;
596
- } catch (err) {
597
- /* non-fatal */
598
- logWarning("worktree", `stale runtime unit unlink failed (${file}): ${err instanceof Error ? err.message : String(err)}`);
599
- }
600
- continue;
601
- }
602
- const midMatch = file.match(/(M\d+(?:-[a-z0-9]{6})?)/);
603
- if (!midMatch) continue;
604
- if (hasMilestoneSummary(midMatch[1])) {
605
- try {
606
- unlinkSync(join(runtimeUnitsDir, file));
607
- cleaned++;
608
- } catch (err) {
609
- /* non-fatal */
610
- logWarning("worktree", `stale runtime unit unlink failed (${file}): ${err instanceof Error ? err.message : String(err)}`);
611
- }
612
- }
613
- }
614
- } catch (err) {
615
- /* non-fatal */
616
- logWarning("worktree", `stale runtime unit cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
617
- }
618
- return cleaned;
619
- }
620
-
621
- // ─── Worktree ↔ Main Repo Sync (#1311) ──────────────────────────────────────
622
-
623
- /**
624
- * Scope-typed variant of syncGsdStateToWorktree.
625
- *
626
- * Takes an explicit (rootScope, worktreeScope) pair. Note: milestoneId is not
627
- * used by syncGsdStateToWorktree — this variant only requires workspace
628
- * identity. Asserts both scopes belong to the same workspace identity to
629
- * prevent silent mismatch bugs.
630
- */
631
- export function syncGsdStateToWorktreeByScope(
632
- rootScope: MilestoneScope,
633
- worktreeScope: MilestoneScope,
634
- ): { synced: string[] } {
635
- if (rootScope.workspace.identityKey !== worktreeScope.workspace.identityKey) {
636
- throw new Error(
637
- `syncGsdStateToWorktreeByScope: scope identity mismatch — ` +
638
- `rootScope.identityKey="${rootScope.workspace.identityKey}" ` +
639
- `worktreeScope.identityKey="${worktreeScope.workspace.identityKey}"`,
640
- );
641
- }
642
- const mainBasePath = rootScope.workspace.projectRoot;
643
- const worktreePath_ = worktreeScope.workspace.worktreeRoot ?? worktreeScope.workspace.projectRoot;
644
- return syncGsdStateToWorktree(mainBasePath, worktreePath_);
645
- }
646
-
647
- /**
648
- * Sync .gsd/ state from the main repo into the worktree.
649
- *
650
- * When .gsd/ is a symlink to the external state directory, both the main
651
- * repo and worktree share the same directory — no sync needed.
652
- *
653
- * When .gsd/ is a real directory (e.g., git-tracked or manage_gitignore:false),
654
- * the worktree has its own copy that may be stale. This function copies
655
- * missing milestones, CONTEXT, ROADMAP, DECISIONS, REQUIREMENTS, and
656
- * PROJECT files from the main repo's .gsd/ into the worktree's .gsd/.
657
- *
658
- * Only adds missing content — never overwrites existing files in the worktree.
659
- * Worktree files are compatibility projections; DB/project root remains
660
- * authoritative for runtime state.
661
- * @deprecated Use syncGsdStateToWorktreeByScope instead.
662
- * TODO(C-future): remove once all callers migrated.
663
- */
664
- export function syncGsdStateToWorktree(
665
- mainBasePath: string,
666
- worktreePath_: string,
667
- ): { synced: string[] } {
668
- const contract = resolveGsdPathContract(worktreePath_, mainBasePath);
669
- const mainGsd = contract.projectGsd;
670
- const wtGsd = contract.worktreeGsd ?? join(worktreePath_, ".gsd");
671
- const synced: string[] = [];
672
-
673
- // If both resolve to the same directory (symlink), no sync needed
674
- if (isSamePath(mainGsd, wtGsd)) return { synced };
675
-
676
- if (!existsSync(mainGsd)) return { synced };
677
- mkdirSync(wtGsd, { recursive: true });
678
-
679
- // Sync root-level .gsd/ files (DECISIONS, REQUIREMENTS, PROJECT, KNOWLEDGE, etc.)
680
- for (const f of ROOT_STATE_FILES) {
681
- const src = join(mainGsd, f);
682
- const dst = join(wtGsd, f);
683
- if (existsSync(src) && !existsSync(dst)) {
684
- try {
685
- cpSync(src, dst);
686
- synced.push(f);
687
- } catch (err) {
688
- /* non-fatal */
689
- logWarning("worktree", `file copy failed (${f}): ${err instanceof Error ? err.message : String(err)}`);
690
- }
691
- }
692
- }
693
-
694
- // Forward-sync project preferences from project root to worktree (additive only).
695
- // Prefer the canonical uppercase file name, but keep the legacy lowercase
696
- // fallback so older repos still work on case-sensitive filesystems.
697
- {
698
- const worktreeHasPreferences = existsSync(join(wtGsd, PROJECT_PREFERENCES_FILE))
699
- || existsSync(join(wtGsd, LEGACY_PROJECT_PREFERENCES_FILE));
700
- if (!worktreeHasPreferences) {
701
- for (const file of [PROJECT_PREFERENCES_FILE, LEGACY_PROJECT_PREFERENCES_FILE] as const) {
702
- const src = join(mainGsd, file);
703
- const dst = join(wtGsd, file);
704
- if (existsSync(src)) {
705
- try {
706
- cpSync(src, dst);
707
- synced.push(file);
708
- } catch (err) {
709
- /* non-fatal */
710
- logWarning("worktree", `preferences copy failed (${file}): ${err instanceof Error ? err.message : String(err)}`);
711
- }
712
- break;
713
- }
714
- }
715
- }
716
- }
717
-
718
- // Sync milestone/phase trees: copy entire directories that are missing.
719
- // Support both flat-phase (phases/) and legacy (milestones/) layouts.
720
- for (const layoutSegment of ["phases", "milestones"] as const) {
721
- const mainMilestonesDir = join(mainGsd, layoutSegment);
722
- const wtMilestonesDir = join(wtGsd, layoutSegment);
723
- if (existsSync(mainMilestonesDir)) {
724
- try {
725
- mkdirSync(wtMilestonesDir, { recursive: true });
726
- const mainMilestones = readdirSync(mainMilestonesDir, {
727
- withFileTypes: true,
728
- })
729
- .filter((d) => d.isDirectory())
730
- .map((d) => d.name);
731
-
732
- for (const mid of mainMilestones) {
733
- const srcDir = join(mainMilestonesDir, mid);
734
- const dstDir = join(wtMilestonesDir, mid);
735
-
736
- if (!existsSync(dstDir)) {
737
- // Entire milestone missing from worktree — copy it
738
- try {
739
- cpSync(srcDir, dstDir, { recursive: true });
740
- synced.push(`milestones/${mid}/`);
741
- } catch (err) {
742
- /* non-fatal */
743
- logWarning("worktree", `milestone copy failed (${mid}): ${err instanceof Error ? err.message : String(err)}`);
744
- }
745
- } else {
746
- // Milestone directory exists but may be missing files (stale snapshot).
747
- // Sync individual top-level milestone files (CONTEXT, ROADMAP, RESEARCH, etc.)
748
- try {
749
- const srcFiles = readdirSync(srcDir).filter(
750
- (f) => f.endsWith(".md") || f.endsWith(".json"),
751
- );
752
- for (const f of srcFiles) {
753
- const srcFile = join(srcDir, f);
754
- const dstFile = join(dstDir, f);
755
- if (!existsSync(dstFile)) {
756
- try {
757
- const srcStat = lstatSyncFn(srcFile);
758
- if (srcStat.isFile()) {
759
- cpSync(srcFile, dstFile);
760
- synced.push(`milestones/${mid}/${f}`);
761
- }
762
- } catch (err) {
763
- /* non-fatal */
764
- logWarning("worktree", `milestone file copy failed (${mid}/${f}): ${err instanceof Error ? err.message : String(err)}`);
765
- }
766
- }
767
- }
768
-
769
- // Sync slices directory if it exists in main but not in worktree
770
- const srcSlicesDir = join(srcDir, "slices");
771
- const dstSlicesDir = join(dstDir, "slices");
772
- if (existsSync(srcSlicesDir) && !existsSync(dstSlicesDir)) {
773
- try {
774
- cpSync(srcSlicesDir, dstSlicesDir, { recursive: true });
775
- synced.push(`milestones/${mid}/slices/`);
776
- } catch (err) {
777
- /* non-fatal */
778
- logWarning("worktree", `slices copy failed (${mid}): ${err instanceof Error ? err.message : String(err)}`);
779
- }
780
- } else if (existsSync(srcSlicesDir) && existsSync(dstSlicesDir)) {
781
- // Both exist — sync missing slice directories
782
- const srcSlices = readdirSync(srcSlicesDir, {
783
- withFileTypes: true,
784
- })
785
- .filter((d) => d.isDirectory())
786
- .map((d) => d.name);
787
- for (const sid of srcSlices) {
788
- const srcSlice = join(srcSlicesDir, sid);
789
- const dstSlice = join(dstSlicesDir, sid);
790
- if (!existsSync(dstSlice)) {
791
- try {
792
- cpSync(srcSlice, dstSlice, { recursive: true });
793
- synced.push(`milestones/${mid}/slices/${sid}/`);
794
- } catch (err) {
795
- /* non-fatal */
796
- logWarning("worktree", `slice copy failed (${mid}/${sid}): ${err instanceof Error ? err.message : String(err)}`);
797
- }
798
- }
799
- }
800
- }
801
- } catch (err) {
802
- /* non-fatal */
803
- logWarning("worktree", `milestone file sync failed: ${err instanceof Error ? err.message : String(err)}`);
804
- }
805
- }
806
- }
807
- } catch (err) {
808
- /* non-fatal */
809
- logWarning("worktree", `milestone directory sync failed: ${err instanceof Error ? err.message : String(err)}`);
810
- }
811
- }
812
- }
813
-
814
- return { synced };
815
- }
816
-
817
- /**
818
- * Sync compatibility artifacts from worktree back to the main external state
819
- * directory. Canonical workflow state lives in the project DB; worktree .gsd
820
- * content is legacy projection/diagnostic data only.
821
- *
822
- * Syncs:
823
- * 1. Legacy worktree DBs are reconciled into the canonical project DB.
824
- * 2. Runtime diagnostic files may be copied for operator visibility.
825
- *
826
- * Markdown milestone directories are projections and are not copied from
827
- * worktrees into the project root. Current workflow state must arrive through
828
- * the shared project DB or the pre-upgrade DB reconciliation path above.
829
- */
830
- export function syncWorktreeStateBack(
831
- mainBasePath: string,
832
- worktreePath: string,
833
- milestoneId: string,
834
- ): { synced: string[] } {
835
- return new WorktreeStateProjection().finalizeProjectionForMergePaths(
836
- mainBasePath,
837
- worktreePath,
838
- milestoneId,
839
- );
840
- }
2
+ // File Purpose: Compatibility barrel for auto-mode worktree modules.
3
+
4
+ /**
5
+ * GSD Auto-Worktree compatibility barrel.
6
+ *
7
+ * New code should import from the focused modules (`auto-worktree-creation`,
8
+ * `auto-worktree-teardown`, `auto-worktree-sync`, `auto-worktree-merge`, etc.).
9
+ * This file preserves the historical interface while the module extraction
10
+ * completes.
11
+ */
12
+
13
+ export {
14
+ autoWorktreeBranch,
15
+ enterBranchModeForMilestone,
16
+ fastForwardReusedMilestoneBranchIfSafe,
17
+ _isBranchCheckedOutElsewhere,
18
+ _resolveAutoWorktreeStartPoint,
19
+ } from "./auto-worktree-branch-lifecycle.js";
20
+
21
+ export {
22
+ _gitPathspecForWorktreePath,
23
+ _isExpectedWorktreeUnlinkError,
24
+ _isSamePath,
25
+ _shouldReconcileWorktreeDb,
26
+ } from "./auto-worktree-cleanup.js";
27
+
28
+ export {
29
+ isSafeToAutoResolve,
30
+ SAFE_AUTO_RESOLVE_PATTERNS,
31
+ } from "./auto-worktree-conflict-auto-resolve.js";
32
+
33
+ export { createAutoWorktree } from "./auto-worktree-creation.js";
34
+
35
+ export {
36
+ enterAutoWorktree,
37
+ getAutoWorktreePath,
38
+ isInAutoWorktree,
39
+ } from "./auto-worktree-entry.js";
40
+
41
+ export {
42
+ mergeMilestoneToMain,
43
+ _setRestoreEntryFnForTests,
44
+ } from "./auto-worktree-merge.js";
45
+
46
+ export {
47
+ checkResourcesStale,
48
+ readResourceVersion,
49
+ } from "./auto-worktree-resource-version.js";
50
+
51
+ export {
52
+ cleanStaleRuntimeUnits,
53
+ escapeStaleWorktree,
54
+ } from "./auto-worktree-runtime-cleanup.js";
55
+
56
+ export {
57
+ getActiveAutoWorktreeContext,
58
+ getAutoWorktreeOriginalBase,
59
+ _resetAutoWorktreeOriginalBaseForTests,
60
+ } from "./auto-worktree-session-registry.js";
61
+
62
+ export {
63
+ syncGsdStateToWorktree,
64
+ syncGsdStateToWorktreeByScope,
65
+ syncProjectRootToWorktree,
66
+ syncStateToProjectRoot,
67
+ syncWorktreeStateBack,
68
+ } from "./auto-worktree-sync.js";
69
+
70
+ export { teardownAutoWorktree } from "./auto-worktree-teardown.js";
841
71
  export { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
842
-
843
- // ─── Auto-Worktree Branch Naming ───────────────────────────────────────────
844
-
845
- /** Returns the git branch name for a milestone worktree (`milestone/<MID>`). */
846
- export function autoWorktreeBranch(milestoneId: string): string {
847
- return `milestone/${milestoneId}`;
848
- }
849
-
850
- function normalizeLocalBranchRef(branch: string): string {
851
- return branch.startsWith("refs/heads/")
852
- ? branch.slice("refs/heads/".length)
853
- : branch;
854
- }
855
-
856
- // ─── Branch-mode Entry ─────────────────────────────────────────────────────
857
-
858
- /**
859
- * Enter branch isolation mode for a milestone.
860
- *
861
- * Creates `milestone/<MID>` from the integration branch (if it doesn't
862
- * exist yet) and checks out to it. No worktree directory is created — the
863
- * project root is the working copy; only HEAD changes.
864
- *
865
- * Uses the same 3-tier integration-branch fallback as createAutoWorktree:
866
- * 1. META.json recorded integration branch
867
- * 2. git.main_branch preference
868
- * 3. nativeDetectMainBranch (origin/HEAD auto-detection)
869
- */
870
- export function enterBranchModeForMilestone(
871
- basePath: string,
872
- milestoneId: string,
873
- ): void {
874
- const branch = autoWorktreeBranch(milestoneId);
875
- const branchExists = nativeBranchExists(basePath, branch);
876
-
877
- if (!branchExists) {
878
- // Create the milestone branch from the integration branch start-point.
879
- const integrationBranch =
880
- readIntegrationBranch(basePath, milestoneId) ?? undefined;
881
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
882
- const startPoint =
883
- _resolveAutoWorktreeStartPoint(
884
- integrationBranch,
885
- gitPrefs?.main_branch,
886
- (branchName) => nativeBranchExists(basePath, branchName),
887
- ) ??
888
- nativeDetectMainBranch(basePath);
889
-
890
- // TOCTOU ancestry guard (Issue #4980 HIGH-3).
891
- //
892
- // The outer `branchExists` check at line 1012 is racy: a concurrent
893
- // process (parallel-orchestrator worker, side-by-side `gsd` instance,
894
- // or manual `git branch` invocation) may have created the branch with
895
- // real commits between that check and this point. `nativeBranchForceReset`
896
- // does `git branch -f`, which silently overwrites the branch ref —
897
- // orphaning any commits not reachable from `startPoint`. Re-check
898
- // immediately before the destructive call and refuse if the branch
899
- // suddenly exists with non-ancestor commits.
900
- //
901
- // Note: under single-threaded execution this is rarely reached, but it
902
- // is NOT dead code — it is the only barrier against a TOCTOU-induced
903
- // commit loss in this code path.
904
- const concurrentlyCreated = nativeBranchExists(basePath, branch);
905
- if (
906
- concurrentlyCreated &&
907
- !nativeIsAncestor(basePath, branch, startPoint)
908
- ) {
909
- throw new GSDError(
910
- GSD_GIT_ERROR,
911
- `Branch "${branch}" was created concurrently with commits not reachable from "${startPoint}". ` +
912
- `Refusing to force-reset — would orphan prior work. ` +
913
- `Resume the existing milestone or run \`git branch -D ${branch}\` to discard.`,
914
- );
915
- }
916
- // nativeBranchForceReset creates (or resets) branch at startPoint,
917
- // then checkout switches HEAD to it.
918
- nativeBranchForceReset(basePath, branch, startPoint);
919
- debugLog("auto-worktree", {
920
- action: "enterBranchMode",
921
- milestoneId,
922
- branch,
923
- startPoint,
924
- created: true,
925
- });
926
- } else {
927
- debugLog("auto-worktree", {
928
- action: "enterBranchMode",
929
- milestoneId,
930
- branch,
931
- reused: true,
932
- });
933
- }
934
-
935
- checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
936
- }
937
-
938
- // ─── Public API ────────────────────────────────────────────────────────────
939
-
940
- /**
941
- * Create a new auto-worktree for a milestone, chdir into it, and store
942
- * the original base path for later teardown.
943
- *
944
- * Atomic: chdir + originalBase update happen in the same try block
945
- * to prevent split-brain.
946
- */
947
-
948
- /**
949
- * Forward-merge plan checkbox state from the project root into a freshly
950
- * re-attached worktree (#778).
951
- *
952
- * Phase C: deleted. Writers in workflow-projections.ts, triage-resolution.ts,
953
- * rule-registry.ts, and auto-post-unit.ts now route through
954
- * s.canonicalProjectRoot, so non-symlinked worktrees no longer need a local
955
- * .gsd/ projection — the project-root .gsd/ is the only authoritative source
956
- * for both reads and writes. copyPlanningArtifacts and reconcilePlanCheckboxes
957
- * (both formerly here) became dead.
958
- */
959
-
960
- /**
961
- * True when `branch` is checked out in any worktree listed by
962
- * `git worktree list --porcelain`. Used to gate ref updates that would
963
- * otherwise leave a concurrent worktree's HEAD inconsistent with its
964
- * index/working tree (Codex peer-review of #5538-followup).
965
- *
966
- * Best-effort: a `nativeWorktreeList` failure returns true so we err on
967
- * the side of NOT moving the ref. Better to skip a fast-forward than to
968
- * silently corrupt another worktree.
969
- */
970
- export function _isBranchCheckedOutElsewhere(
971
- basePath: string,
972
- branch: string,
973
- ): boolean {
974
- try {
975
- const entries = nativeWorktreeList(basePath);
976
- return entries.some((entry) => entry.branch === branch);
977
- } catch {
978
- return true;
979
- }
980
- }
981
-
982
- /**
983
- * Resolve the integration branch using the same 3-tier fallback as the
984
- * fresh-create path: META.json → git.main_branch preference → detected
985
- * main branch. Returns null when no usable target exists.
986
- */
987
- function _resolveIntegrationBranchForReuse(
988
- basePath: string,
989
- milestoneId: string,
990
- ): string | null {
991
- const fromMeta = readIntegrationBranch(basePath, milestoneId);
992
- if (fromMeta) return fromMeta;
993
-
994
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
995
- const fromPref = gitPrefs?.main_branch &&
996
- typeof gitPrefs.main_branch === "string" &&
997
- gitPrefs.main_branch.length > 0 &&
998
- nativeBranchExists(basePath, gitPrefs.main_branch)
999
- ? gitPrefs.main_branch
1000
- : null;
1001
- if (fromPref) return fromPref;
1002
-
1003
- try {
1004
- return nativeDetectMainBranch(basePath);
1005
- } catch {
1006
- return null;
1007
- }
1008
- }
1009
-
1010
- function safeCwd(fallback: string): string {
1011
- try {
1012
- return process.cwd();
1013
- } catch {
1014
- return fallback;
1015
- }
1016
- }
1017
-
1018
- /**
1019
- * When reusing an existing milestone branch, fast-forward it onto the
1020
- * integration branch when that's safe (branch is a strict ancestor of
1021
- * integration — no commits would be lost). Skips when the branch has its
1022
- * own commits ahead of integration, when the integration branch can't be
1023
- * resolved, or when any git operation fails — the merge gate at milestone
1024
- * completion will surface real divergence as a conflict.
1025
- *
1026
- * The previous behavior re-attached the worktree to whatever stale tip
1027
- * the branch held, which caused new milestone work to fork from a base
1028
- * missing prior milestones' merges (#5538-followup).
1029
- */
1030
- export function fastForwardReusedMilestoneBranchIfSafe(
1031
- basePath: string,
1032
- milestoneId: string,
1033
- branch: string,
1034
- ): void {
1035
- try {
1036
- const integrationBranch = _resolveIntegrationBranchForReuse(basePath, milestoneId);
1037
- if (!integrationBranch || integrationBranch === branch) return;
1038
- if (!nativeBranchExists(basePath, integrationBranch)) return;
1039
-
1040
- // Pure fast-forward only: branch must be a strict ancestor of integration.
1041
- // If the branch has its own commits ahead, leave it alone.
1042
- if (!nativeIsAncestor(basePath, branch, integrationBranch)) {
1043
- debugLog("createAutoWorktree", {
1044
- phase: "skip-ff-branch-not-ancestor",
1045
- milestoneId,
1046
- branch,
1047
- integration: integrationBranch,
1048
- });
1049
- return;
1050
- }
1051
-
1052
- // Codex peer-review: `nativeUpdateRef` succeeds even when the branch is
1053
- // currently checked out in another worktree, leaving that worktree's HEAD
1054
- // inconsistent with its index/work tree. Skip the fast-forward if any
1055
- // listed worktree has this branch checked out — the merge gate at
1056
- // milestone-completion will surface stale-base divergence as a conflict
1057
- // instead of silently corrupting the other worktree's state.
1058
- if (_isBranchCheckedOutElsewhere(basePath, branch)) {
1059
- debugLog("createAutoWorktree", {
1060
- phase: "skip-ff-branch-checked-out-elsewhere",
1061
- milestoneId,
1062
- branch,
1063
- });
1064
- return;
1065
- }
1066
-
1067
- nativeUpdateRef(basePath, `refs/heads/${branch}`, integrationBranch);
1068
- debugLog("createAutoWorktree", {
1069
- phase: "fast-forward-reused-branch",
1070
- milestoneId,
1071
- branch,
1072
- integration: integrationBranch,
1073
- });
1074
- } catch (err) {
1075
- debugLog("createAutoWorktree", {
1076
- phase: "fast-forward-reused-branch-failed",
1077
- milestoneId,
1078
- branch,
1079
- error: err instanceof Error ? err.message : String(err),
1080
- });
1081
- }
1082
- }
1083
-
1084
- export function createAutoWorktree(
1085
- basePath: string,
1086
- milestoneId: string,
1087
- ): string {
1088
- basePath = resolveWorktreeProjectRoot(basePath);
1089
-
1090
- // Check if repo has commits — git worktree requires a valid HEAD
1091
- try {
1092
- execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: basePath, stdio: "pipe" });
1093
- } catch {
1094
- throw new GSDError(
1095
- GSD_GIT_ERROR,
1096
- `Cannot create worktree: repository has no commits yet. Worktree isolation requires at least one commit.`,
1097
- );
1098
- }
1099
-
1100
- const branch = autoWorktreeBranch(milestoneId);
1101
-
1102
- // Check if the milestone branch already exists — it survives auto-mode
1103
- // stop/pause and contains committed work from prior sessions. If it exists,
1104
- // re-attach the worktree to it WITHOUT resetting. Only create a fresh branch
1105
- // from the integration branch when no prior work exists.
1106
- const branchExists = nativeBranchExists(basePath, branch);
1107
-
1108
- let info: { name: string; path: string; branch: string; exists: boolean };
1109
- if (branchExists) {
1110
- // #5538-followup: fast-forward the reused branch onto the integration
1111
- // branch when safe so the next milestone forks from up-to-date code.
1112
- // Without this, a milestone that was created before another milestone
1113
- // merged into main would carry a stale base into its worktree.
1114
- fastForwardReusedMilestoneBranchIfSafe(basePath, milestoneId, branch);
1115
-
1116
- // Re-attach worktree to the existing milestone branch (preserving commits)
1117
- info = createWorktree(basePath, milestoneId, {
1118
- branch,
1119
- reuseExistingBranch: true,
1120
- });
1121
- } else {
1122
- // Fresh start — create branch from integration branch.
1123
- // Use the same 3-tier fallback as mergeMilestoneToMain (#3461):
1124
- // 1. META.json integration branch (explicit per-milestone override)
1125
- // 2. git.main_branch preference (user's configured working branch)
1126
- // 3. nativeDetectMainBranch (origin/HEAD auto-detection)
1127
- // Without tier 2, projects with main_branch=dev but origin/HEAD→master
1128
- // would fork worktrees from the wrong (stale) branch.
1129
- const integrationBranch =
1130
- readIntegrationBranch(basePath, milestoneId) ?? undefined;
1131
- const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
1132
- const startPoint = _resolveAutoWorktreeStartPoint(
1133
- integrationBranch,
1134
- gitPrefs?.main_branch,
1135
- (branchName) => nativeBranchExists(basePath, branchName),
1136
- );
1137
- info = createWorktree(basePath, milestoneId, {
1138
- branch,
1139
- startPoint,
1140
- });
1141
- }
1142
-
1143
- // Phase C: copyPlanningArtifacts and reconcilePlanCheckboxes were
1144
- // deleted. Both addressed the same problem (worktree-local .gsd/
1145
- // projection lagging behind project-root state) by maintaining a stale
1146
- // copy. Now that auto-mode writers in workflow-projections.ts,
1147
- // triage-resolution.ts, rule-registry.ts, and auto-post-unit.ts route
1148
- // through s.canonicalProjectRoot, the worktree never needs a local
1149
- // .gsd/ — both reads and writes converge on the project-root .gsd/.
1150
- // The original concerns (#759, #778) no longer apply because there is
1151
- // no second copy to drift.
1152
-
1153
- importUntrackedProjectRootContentIntoEmptyWorktree(basePath, info.path, milestoneId);
1154
-
1155
- // Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
1156
- const hookError = runWorktreePostCreateHook(basePath, info.path);
1157
- if (hookError) {
1158
- // Non-fatal — log but don't prevent worktree usage
1159
- logWarning("reconcile", hookError, { worktree: info.name });
1160
- }
1161
-
1162
- const previousCwd = process.cwd();
1163
-
1164
- try {
1165
- process.chdir(info.path);
1166
- setActiveWorkspace(createWorkspace(basePath));
1167
- } catch (err) {
1168
- // If chdir fails, the worktree was created but we couldn't enter it.
1169
- // Don't set activeWorkspace -- caller can retry or clean up.
1170
- throw new GSDError(
1171
- GSD_IO_ERROR,
1172
- `Auto-worktree created at ${info.path} but chdir failed: ${err instanceof Error ? err.message : String(err)}`,
1173
- );
1174
- }
1175
-
1176
- nudgeGitBranchCache(previousCwd);
1177
- return info.path;
1178
- }
1179
-
1180
- // Phase C: copyPlanningArtifacts removed. Planning artifacts now live
1181
- // only at the project root .gsd/; auto-mode writers (workflow-projections,
1182
- // triage-resolution, rule-registry, regenerateIfMissing,
1183
- // resolveHookArtifactPath) all route through s.canonicalProjectRoot.
1184
- // Worktrees are pure git checkouts — they no longer maintain a parallel
1185
- // .gsd/ projection. The gsd.db has always lived at the project root via
1186
- // the shared-WAL R012 contract; that is unchanged.
1187
-
1188
- /**
1189
- * Teardown an auto-worktree: chdir back to original base, then remove
1190
- * the worktree and its branch.
1191
- */
1192
- export function teardownAutoWorktree(
1193
- originalBasePath: string,
1194
- milestoneId: string,
1195
- opts: { preserveBranch?: boolean; preserveWorktree?: boolean } = {},
1196
- ): void {
1197
- originalBasePath = resolveWorktreeProjectRoot(originalBasePath);
1198
-
1199
- const branch = autoWorktreeBranch(milestoneId);
1200
- const { preserveBranch = false, preserveWorktree = false } = opts;
1201
- const previousCwd = safeCwd(originalBasePath);
1202
-
1203
- // Wrap the entire teardown body in a single try/finally so activeWorkspace
1204
- // is ALWAYS cleared — even if process.chdir throws (e.g. originalBasePath
1205
- // was deleted before teardown ran). Previously the finally only covered
1206
- // removeWorktree, leaving the registry stale on a chdir failure (H3 fix).
1207
- try {
1208
- try {
1209
- process.chdir(originalBasePath);
1210
- } catch (err) {
1211
- throw new GSDError(
1212
- GSD_IO_ERROR,
1213
- `Failed to chdir back to ${originalBasePath} during teardown: ${err instanceof Error ? err.message : String(err)}`,
1214
- );
1215
- }
1216
-
1217
- // Mirror cleanup steps from mergeMilestoneToMain abort path:
1218
-
1219
- // 1. Remove transient state files (STATE.md, auto.lock, {MID}-META.json).
1220
- // Non-fatal — must not block teardown.
1221
- try {
1222
- clearProjectRootStateFiles(originalBasePath, milestoneId);
1223
- } catch (err) {
1224
- logWarning("worktree", `clearProjectRootStateFiles failed during teardown: ${err instanceof Error ? err.message : String(err)}`);
1225
- }
1226
-
1227
- // 2. Reconcile worktree-local gsd.db into project root DB if both exist.
1228
- // Non-fatal — handles legacy worktrees that have a local copy.
1229
- if (isDbAvailable()) {
1230
- try {
1231
- const contract = resolveGsdPathContract(previousCwd, originalBasePath);
1232
- const worktreeDbPath = join(contract.worktreeGsd ?? join(previousCwd, ".gsd"), "gsd.db");
1233
- const mainDbPath = contract.projectDb;
1234
- if (_shouldReconcileWorktreeDb(worktreeDbPath, mainDbPath)) {
1235
- reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1236
- }
1237
- } catch (err) {
1238
- /* non-fatal */
1239
- logError("worktree", `DB reconciliation failed during teardown: ${err instanceof Error ? err.message : String(err)}`);
1240
- }
1241
- }
1242
-
1243
- nudgeGitBranchCache(previousCwd);
1244
-
1245
- // 3. Remove the worktree unless this exit path explicitly preserves it
1246
- // (slice-parallel dispatch stops the parent loop but keeps the parent
1247
- // milestone worktree for restart/re-entry).
1248
- if (!preserveWorktree) {
1249
- removeWorktree(originalBasePath, milestoneId, {
1250
- branch,
1251
- deleteBranch: !preserveBranch,
1252
- });
1253
- }
1254
-
1255
- // Verify cleanup succeeded — warn if the worktree directory is still on disk.
1256
- // On Windows, bash-based cleanup can silently fail when paths contain
1257
- // backslashes (#1436), leaving ~1 GB+ orphaned directories.
1258
- const wtDir = worktreePath(originalBasePath, milestoneId);
1259
- if (!preserveWorktree && existsSync(wtDir)) {
1260
- logWarning(
1261
- "reconcile",
1262
- `Worktree directory still exists after teardown: ${wtDir}. ` +
1263
- `This is likely an orphaned directory consuming disk space. ` +
1264
- `Remove it manually with: rm -rf "${wtDir.replaceAll("\\", "/")}"`,
1265
- { worktree: milestoneId },
1266
- );
1267
- // Attempt a direct filesystem removal as a fallback — but ONLY if the
1268
- // path is safely inside .gsd/worktrees/ to prevent #2365 data loss.
1269
- if (isInsideWorktreesDir(originalBasePath, wtDir)) {
1270
- try {
1271
- rmSync(wtDir, { recursive: true, force: true });
1272
- } catch (err) {
1273
- // Non-fatal — the warning above tells the user how to clean up
1274
- logWarning("worktree", `worktree directory removal failed: ${err instanceof Error ? err.message : String(err)}`);
1275
- }
1276
- } else {
1277
- console.error(
1278
- `[GSD] REFUSING fallback rmSync — path is outside .gsd/worktrees/: ${wtDir}`,
1279
- );
1280
- }
1281
- }
1282
- } finally {
1283
- // Clear module state unconditionally — regardless of which step above
1284
- // failed. A stale activeWorkspace causes getActiveAutoWorktreeContext()
1285
- // to return wrong data for subsequent operations.
1286
- setActiveWorkspace(null);
1287
- }
1288
- }
1289
-
1290
- /**
1291
- * Detect if the process is currently inside an auto-worktree.
1292
- * Uses the current directory structure plus git branch prefix so detection
1293
- * still works after process restart when module state has been reset.
1294
- */
1295
- export function isInAutoWorktree(basePath: string): boolean {
1296
- const targetPath = isGsdWorktreePath(basePath) ? basePath : safeCwd("");
1297
- if (!isGsdWorktreePath(targetPath)) return false;
1298
-
1299
- const storedBase = getAutoWorktreeOriginalBase();
1300
- const projectRoot = resolveWorktreeProjectRoot(basePath, storedBase);
1301
- const targetProjectRoot = resolveWorktreeProjectRoot(targetPath, storedBase);
1302
- if (
1303
- normalizeWorktreePathForCompare(projectRoot) !==
1304
- normalizeWorktreePathForCompare(targetProjectRoot)
1305
- ) {
1306
- return false;
1307
- }
1308
-
1309
- try {
1310
- const branch = nativeGetCurrentBranch(targetPath);
1311
- return branch.startsWith("milestone/");
1312
- } catch {
1313
- return false;
1314
- }
1315
- }
1316
-
1317
- /**
1318
- * Get the filesystem path for an auto-worktree, or null if it doesn't exist
1319
- * or is not a valid git worktree.
1320
- *
1321
- * Validates that the path is a real git worktree (has a .git file with a
1322
- * gitdir: pointer) rather than just a stray directory. This prevents
1323
- * mis-detection of leftover directories as active worktrees (#695).
1324
- */
1325
- export function getAutoWorktreePath(
1326
- basePath: string,
1327
- milestoneId: string,
1328
- ): string | null {
1329
- basePath = resolveWorktreeProjectRoot(basePath);
1330
-
1331
- // basePath is already the resolved project root — go straight to placement
1332
- // instead of worktreePath(), which would re-resolve the root.
1333
- const p = worktreePathFor(basePath, milestoneId);
1334
- if (!existsSync(p)) return null;
1335
-
1336
- // Validate this is a real git worktree, not a stray directory.
1337
- // A git worktree has a .git *file* (not directory) containing "gitdir: <path>".
1338
- const gitPath = join(p, ".git");
1339
- if (!existsSync(gitPath)) return null;
1340
- try {
1341
- const content = readFileSync(gitPath, "utf8").trim();
1342
- if (!content.startsWith("gitdir: ")) return null;
1343
- } catch (e) {
1344
- logWarning("worktree", `getAutoWorktreePath .git read failed: ${(e as Error).message}`);
1345
- return null;
1346
- }
1347
-
1348
- return p;
1349
- }
1350
-
1351
- /**
1352
- * Enter an existing auto-worktree (chdir into it, store originalBase).
1353
- * Use for resume -- the worktree already exists from a prior create.
1354
- *
1355
- * Atomic: chdir + originalBase update in same try block.
1356
- */
1357
- export function enterAutoWorktree(
1358
- basePath: string,
1359
- milestoneId: string,
1360
- ): string {
1361
- basePath = resolveWorktreeProjectRoot(basePath);
1362
-
1363
- const p = worktreePath(basePath, milestoneId);
1364
- if (!existsSync(p)) {
1365
- throw new GSDError(
1366
- GSD_IO_ERROR,
1367
- `Auto-worktree for ${milestoneId} does not exist at ${p}`,
1368
- );
1369
- }
1370
-
1371
- // Validate this is a real git worktree, not a stray directory (#695)
1372
- const gitPath = join(p, ".git");
1373
- if (!existsSync(gitPath)) {
1374
- throw new GSDError(
1375
- GSD_GIT_ERROR,
1376
- `Auto-worktree path ${p} exists but is not a git worktree (no .git)`,
1377
- );
1378
- }
1379
- try {
1380
- const content = readFileSync(gitPath, "utf8").trim();
1381
- if (!content.startsWith("gitdir: ")) {
1382
- throw new GSDError(
1383
- GSD_GIT_ERROR,
1384
- `Auto-worktree path ${p} has a .git but it is not a worktree gitdir pointer`,
1385
- );
1386
- }
1387
- } catch (err) {
1388
- if (err instanceof Error && err.message.includes("worktree")) throw err;
1389
- throw new GSDError(
1390
- GSD_IO_ERROR,
1391
- `Auto-worktree path ${p} exists but .git is unreadable`,
1392
- );
1393
- }
1394
-
1395
- const previousCwd = process.cwd();
1396
-
1397
- try {
1398
- process.chdir(p);
1399
- setActiveWorkspace(createWorkspace(basePath));
1400
- } catch (err) {
1401
- throw new GSDError(
1402
- GSD_IO_ERROR,
1403
- `Failed to enter auto-worktree at ${p}: ${err instanceof Error ? err.message : String(err)}`,
1404
- );
1405
- }
1406
-
1407
- nudgeGitBranchCache(previousCwd);
1408
- return p;
1409
- }
1410
-
1411
- /**
1412
- * Get the original project root stored when entering an auto-worktree.
1413
- * Returns null if not currently in an auto-worktree.
1414
- */
1415
- export function getAutoWorktreeOriginalBase(): string | null {
1416
- return getActiveWorkspace()?.projectRoot ?? null;
1417
- }
1418
-
1419
- /**
1420
- * Test-only — resets the module-level `activeWorkspace` registry between
1421
- * runs. Production code never clears the registry directly; tests call this
1422
- * in `beforeEach`/`afterEach` to isolate registry-mutating cases. Renaming
1423
- * the underscore-prefixed `_*ForTest` exports it joins (slice 7 / step G of
1424
- * ADR-016) was deliberate: those wrapped real production helpers and lost
1425
- * the suffix; this one stays as the only legitimate test-scaffolding export
1426
- * because it has no production caller.
1427
- */
1428
- export function _resetAutoWorktreeOriginalBaseForTests(): void {
1429
- setActiveWorkspace(null);
1430
- }
1431
-
1432
- /**
1433
- * Inject an override for the shelter restore copy step, returning a function
1434
- * that restores the default (real cpSync) behavior. Used by tests to
1435
- * deterministically exercise the best-effort restore-failure path
1436
- * (auto-worktree.ts:1809) and the #2505 shelter-retention guarantee — which is
1437
- * otherwise unreachable because shelter + restore run synchronously in one
1438
- * mergeMilestoneToMain call. No production caller.
1439
- * @internal
1440
- */
1441
- export function _setRestoreEntryFnForTests(
1442
- fn: ((src: string, dest: string) => void) | null,
1443
- ): () => void {
1444
- _restoreEntryFn = fn;
1445
- return () => { _restoreEntryFn = null; };
1446
- }
1447
-
1448
- export function getActiveAutoWorktreeContext(): {
1449
- originalBase: string;
1450
- worktreeName: string;
1451
- branch: string;
1452
- } | null {
1453
- const ws = getActiveWorkspace();
1454
- if (!ws) return null;
1455
- const originalBase = ws.projectRoot;
1456
- const cwd = process.cwd();
1457
- if (!isGsdWorktreePath(cwd)) return null;
1458
- const cwdProjectRoot = resolveWorktreeProjectRoot(cwd, originalBase);
1459
- if (
1460
- normalizeWorktreePathForCompare(cwdProjectRoot) !==
1461
- normalizeWorktreePathForCompare(originalBase)
1462
- ) {
1463
- return null;
1464
- }
1465
- const worktreeName = detectWorktreeName(cwd);
1466
- if (!worktreeName) return null;
1467
- const branch = nativeGetCurrentBranch(cwd);
1468
- if (!branch.startsWith("milestone/")) return null;
1469
- return {
1470
- originalBase,
1471
- worktreeName,
1472
- branch,
1473
- };
1474
- }
1475
-
1476
- // ─── Merge Milestone -> Main ───────────────────────────────────────────────
1477
-
1478
- /**
1479
- * Auto-commit any dirty (uncommitted) state in the given directory.
1480
- * Returns true if a commit was made, false if working tree was clean.
1481
- */
1482
- function autoCommitDirtyState(cwd: string): boolean {
1483
- try {
1484
- const status = nativeWorkingTreeStatus(cwd);
1485
- if (!status) return false;
1486
- nativeAddAllWithExclusions(cwd, RUNTIME_EXCLUSION_PATHS);
1487
- const result = nativeCommit(
1488
- cwd,
1489
- "chore: auto-commit before milestone merge",
1490
- );
1491
- return result !== null;
1492
- } catch (e) {
1493
- debugLog("autoCommitDirtyState", { error: String(e) });
1494
- throw new GSDError(
1495
- GSD_GIT_ERROR,
1496
- `Failed to auto-commit dirty worktree state before milestone merge: ${e instanceof Error ? e.message : String(e)}`,
1497
- );
1498
- }
1499
- }
1500
-
1501
- /**
1502
- * Squash-merge the milestone branch into main with a rich commit message
1503
- * listing all completed slices, then tear down the worktree.
1504
- *
1505
- * Sequence:
1506
- * 1. Auto-commit dirty worktree state
1507
- * 2. chdir to originalBasePath
1508
- * 3. git checkout main
1509
- * 4. git merge --squash milestone/<MID>
1510
- * 5. git commit with rich message
1511
- * 6. Auto-push if enabled
1512
- * 7. Delete milestone branch
1513
- * 8. Remove worktree directory
1514
- * 9. Clear originalBase
1515
- *
1516
- * On merge conflict: throws MergeConflictError.
1517
- * On "nothing to commit" after squash: safe only if milestone work is already
1518
- * on the integration branch. Throws if unanchored code changes would be lost.
1519
- *
1520
- * @internal **Do not call directly.** This is the inner squash-merge primitive
1521
- * for the Worktree Lifecycle Module (ADR-016 phase 2 / A3, issue #5619).
1522
- * Production callers must go through `WorktreeLifecycle.mergeMilestoneStandalone`
1523
- * or `WorktreeLifecycle.exitMilestone({ merge: true })`. The export keyword
1524
- * is preserved only so `auto.ts:buildWorktreeLifecycleDeps()` can wire this
1525
- * function through the Module's deps seam — that is the construction of the
1526
- * seam, not a bypass.
1527
- */
1528
- export function mergeMilestoneToMain(
1529
- originalBasePath_: string,
1530
- milestoneId: string,
1531
- roadmapContent: string,
1532
- ): { commitMessage: string; pushed: boolean; prCreated: boolean; codeFilesChanged: boolean } {
1533
- const worktreeCwd = process.cwd();
1534
- const milestoneBranch = autoWorktreeBranch(milestoneId);
1535
-
1536
- // 1. Auto-commit dirty state before leaving.
1537
- // Guard: when we entered through an auto-worktree (originalBase is set),
1538
- // only auto-commit when cwd is on the milestone branch. In parallel mode,
1539
- // cwd may be on the integration branch after a prior merge's
1540
- // MergeConflictError left cwd unrestored. Auto-committing on the
1541
- // integration branch captures dirty files from OTHER milestones under a
1542
- // misleading commit message, contaminating the main branch (#2929).
1543
- //
1544
- // When activeWorkspace is null (branch mode, no worktree), autoCommitDirtyState
1545
- // runs unconditionally — the caller is responsible for cwd placement.
1546
- {
1547
- let shouldAutoCommit = true;
1548
- if (getActiveWorkspace() !== null) {
1549
- try {
1550
- const currentBranch = nativeGetCurrentBranch(worktreeCwd);
1551
- shouldAutoCommit = currentBranch === milestoneBranch;
1552
- } catch {
1553
- // If we can't determine the branch, skip the auto-commit to be safe
1554
- shouldAutoCommit = false;
1555
- }
1556
- }
1557
- if (shouldAutoCommit) {
1558
- autoCommitDirtyState(worktreeCwd);
1559
- }
1560
- }
1561
-
1562
- // Reconcile worktree DB into main DB before leaving worktree context.
1563
- // Skip when both paths resolve to the same physical file (shared WAL /
1564
- // symlink layout) — ATTACHing a WAL-mode file to itself corrupts the
1565
- // database (#2823).
1566
- if (isDbAvailable()) {
1567
- const contract = resolveGsdPathContract(worktreeCwd, originalBasePath_);
1568
- const worktreeDbPath = join(contract.worktreeGsd ?? join(worktreeCwd, ".gsd"), "gsd.db");
1569
- const mainDbPath = contract.projectDb;
1570
- try {
1571
- const activeDbPath = getWorkflowDatabasePath();
1572
- if (activeDbPath && _shouldReconcileWorktreeDb(activeDbPath, mainDbPath)) {
1573
- closeWorkflowDatabase();
1574
- if (!openWorkflowDatabasePath(mainDbPath)) {
1575
- throw new Error(`cannot open project DB at ${mainDbPath}`);
1576
- }
1577
- }
1578
- if (_shouldReconcileWorktreeDb(worktreeDbPath, mainDbPath)) {
1579
- reconcileWorktreeDb(mainDbPath, worktreeDbPath);
1580
- }
1581
- } catch (err) {
1582
- const message = `DB reconciliation failed before milestone ${milestoneId} merge: ${err instanceof Error ? err.message : String(err)}`;
1583
- logError("worktree", message);
1584
- throw new GSDError(GSD_GIT_ERROR, `${message}. Recovery reason: ${CLOSEOUT_CONSISTENCY_BLOCKED_REASON}.`);
1585
- }
1586
-
1587
- const closeoutProof = proveMilestoneCloseout(milestoneId);
1588
- if (!closeoutProof.ok) {
1589
- throw new GSDError(GSD_GIT_ERROR, formatCloseoutProofBlock(closeoutProof));
1590
- }
1591
- }
1592
-
1593
- // 2. Get completed slices for commit message
1594
- let completedSlices: { id: string; title: string; tasks: Array<{ id: string; title: string }> }[] = [];
1595
- if (isDbAvailable()) {
1596
- completedSlices = getMilestoneSlices(milestoneId)
1597
- .filter(s => s.status === "complete")
1598
- .map(s => ({
1599
- id: s.id,
1600
- title: stripGsdDisplayPrefix(s.title, s.id) ?? s.id,
1601
- tasks: getSliceTasks(milestoneId, s.id)
1602
- .filter((task) => task.status === "complete")
1603
- .map((task) => ({
1604
- id: task.id,
1605
- title: stripGsdDisplayPrefix(task.title, task.id) ?? task.id,
1606
- })),
1607
- }));
1608
- }
1609
- // Fallback: parse roadmap content when DB is unavailable
1610
- if (completedSlices.length === 0 && roadmapContent) {
1611
- const sliceRe = /- \[x\] \*\*(\w+):\s*(.+?)\*\*/gi;
1612
- let m: RegExpExecArray | null;
1613
- while ((m = sliceRe.exec(roadmapContent)) !== null) {
1614
- completedSlices.push({ id: m[1], title: m[2], tasks: [] });
1615
- }
1616
- }
1617
-
1618
- // 3. chdir to original base
1619
- // Note: previousCwd captures the cwd at this point — i.e. the worktree cwd
1620
- // entering the function. Subsequent throws restore to previousCwd, leaving
1621
- // the caller in worktree-cwd; callers (worktree-resolver) are responsible
1622
- // for any further cwd movement on the error path.
1623
- const previousCwd = process.cwd();
1624
- process.chdir(originalBasePath_);
1625
-
1626
- // 4. Resolve integration branch via shared resolver so stale/invalid
1627
- // milestone metadata can recover to configured/detected fallbacks.
1628
- const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
1629
- const branchResolution = resolveMilestoneIntegrationBranch(originalBasePath_, milestoneId, prefs);
1630
- const mainBranch = branchResolution.effectiveBranch ?? nativeDetectMainBranch(originalBasePath_);
1631
-
1632
- // Fail closed when the resolved integration branch is the milestone branch
1633
- // itself (#5024). Stale or corrupt metadata (e.g. integrationBranch recorded
1634
- // as "milestone/<MID>") would otherwise let the squash merge resolve to a
1635
- // self-merge: nothing-to-commit + empty self-diff in the post-merge safety
1636
- // check (#1792) collapse to a false success, and the worktree-resolver
1637
- // emits worktree-merged for work that never landed on a distinct
1638
- // integration branch.
1639
- if (normalizeLocalBranchRef(mainBranch) === milestoneBranch) {
1640
- process.chdir(previousCwd);
1641
- throw new GSDError(
1642
- GSD_GIT_ERROR,
1643
- `Resolved integration branch "${mainBranch}" is the same ref as milestone branch ` +
1644
- `"${milestoneBranch}" — refusing to self-merge. ${branchResolution.reason}. ` +
1645
- `Repair milestone integration metadata before retrying milestone completion.`,
1646
- );
1647
- }
1648
-
1649
- // Remove transient project-root state files before any branch or merge
1650
- // operation. Untracked milestone metadata can otherwise block squash merges.
1651
- clearProjectRootStateFiles(originalBasePath_, milestoneId);
1652
-
1653
- // 5. Checkout integration branch (skip if already current — avoids git error
1654
- // when main is already checked out in the project-root worktree, #757)
1655
- //
1656
- // Refuse to proceed if the project root is in detached HEAD state. Silently
1657
- // running `nativeCheckoutBranch(mainBranch)` on a detached HEAD would
1658
- // abandon the user's deliberately-checked-out commit (mid-bisect, reviewing
1659
- // a tag, CI checkout-sha) without warning. (Issue #4980 HIGH-10)
1660
- const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_);
1661
- if (!currentBranchAtBase || currentBranchAtBase.length === 0) {
1662
- process.chdir(previousCwd);
1663
- throw new GSDError(
1664
- GSD_GIT_ERROR,
1665
- `Project root is in detached HEAD state — cannot perform milestone merge. ` +
1666
- `Checkout an integration branch (e.g. \`git checkout ${mainBranch}\`) before resuming.`,
1667
- );
1668
- }
1669
- if (currentBranchAtBase !== mainBranch) {
1670
- nativeCheckoutBranch(originalBasePath_, mainBranch);
1671
- }
1672
-
1673
- // 6. Build rich commit message
1674
- const dbMilestone = getMilestone(milestoneId);
1675
- let milestoneTitle = stripGsdDisplayPrefix(dbMilestone?.title, milestoneId) ?? "";
1676
- // Fallback: parse title from roadmap content header (e.g. "# M020: Backend foundation")
1677
- if (!milestoneTitle && roadmapContent) {
1678
- const titleMatch = roadmapContent.match(new RegExp(`^#\\s+${milestoneId}:\\s*(.+)`, "m"));
1679
- if (titleMatch) milestoneTitle = titleMatch[1].trim();
1680
- }
1681
- milestoneTitle = milestoneTitle || milestoneId;
1682
- const subject = `feat: ${milestoneTitle}`;
1683
- const milestoneContext = milestoneTitle === milestoneId
1684
- ? `Milestone: ${milestoneId}`
1685
- : `Milestone: ${milestoneId} - ${milestoneTitle}`;
1686
- let body = "";
1687
- if (completedSlices.length > 0) {
1688
- const sliceLines = completedSlices
1689
- .map((s) => `- ${s.id}: ${s.title}`)
1690
- .join("\n");
1691
- const taskLines = completedSlices
1692
- .flatMap((s) => s.tasks.map((task) => `- ${s.id}/${task.id}: ${task.title}`))
1693
- .join("\n");
1694
- const taskBlock = taskLines ? `\n\nCompleted tasks:\n${taskLines}` : "";
1695
- body = `\n\nCompleted slices:\n${sliceLines}${taskBlock}\n\n${milestoneContext}\nGSD-Milestone: ${milestoneId}\nBranch: ${milestoneBranch}`;
1696
- } else {
1697
- body = `\n\n${milestoneContext}\nGSD-Milestone: ${milestoneId}\nBranch: ${milestoneBranch}`;
1698
- }
1699
- const commitMessage = subject + body;
1700
-
1701
- // 6b. Reconcile worktree HEAD with milestone branch ref (#1846).
1702
- // When the worktree HEAD detaches and advances past the named branch,
1703
- // the branch ref becomes stale. Squash-merging the stale ref silently
1704
- // orphans all commits between the branch ref and the actual worktree HEAD.
1705
- // Fix: fast-forward the branch ref to the worktree HEAD before merging.
1706
- // Only applies when merging from an actual worktree (worktreeCwd differs
1707
- // from originalBasePath_).
1708
- if (worktreeCwd !== originalBasePath_) {
1709
- try {
1710
- const worktreeHead = execFileSync("git", ["rev-parse", "HEAD"], {
1711
- cwd: worktreeCwd,
1712
- stdio: ["ignore", "pipe", "pipe"],
1713
- encoding: "utf-8",
1714
- }).trim();
1715
- const branchHead = execFileSync("git", ["rev-parse", milestoneBranch], {
1716
- cwd: originalBasePath_,
1717
- stdio: ["ignore", "pipe", "pipe"],
1718
- encoding: "utf-8",
1719
- }).trim();
1720
-
1721
- if (worktreeHead && branchHead && worktreeHead !== branchHead) {
1722
- if (nativeIsAncestor(originalBasePath_, branchHead, worktreeHead)) {
1723
- // Worktree HEAD is strictly ahead — fast-forward the branch ref
1724
- nativeUpdateRef(
1725
- originalBasePath_,
1726
- `refs/heads/${milestoneBranch}`,
1727
- worktreeHead,
1728
- );
1729
- debugLog("mergeMilestoneToMain", {
1730
- action: "fast-forward-branch-ref",
1731
- milestoneBranch,
1732
- oldRef: branchHead.slice(0, 8),
1733
- newRef: worktreeHead.slice(0, 8),
1734
- });
1735
- } else {
1736
- // Diverged — fail loudly rather than silently losing commits
1737
- process.chdir(previousCwd);
1738
- throw new GSDError(
1739
- GSD_GIT_ERROR,
1740
- `Worktree HEAD (${worktreeHead.slice(0, 8)}) diverged from ` +
1741
- `${milestoneBranch} (${branchHead.slice(0, 8)}). ` +
1742
- `Manual reconciliation required before merge.`,
1743
- );
1744
- }
1745
- }
1746
- } catch (err) {
1747
- // Re-throw GSDError (divergence); swallow rev-parse failures
1748
- // (e.g. worktree dir already removed by external cleanup)
1749
- if (err instanceof GSDError) throw err;
1750
- debugLog("mergeMilestoneToMain", {
1751
- action: "reconcile-skipped",
1752
- reason: String(err),
1753
- });
1754
- }
1755
- }
1756
-
1757
- // Already regular-merged milestones can skip the squash path and proceed to cleanup (#5831).
1758
- if (nativeIsAncestor(originalBasePath_, milestoneBranch, mainBranch)) {
1759
- const codeChanges = nativeDiffNumstat(
1760
- originalBasePath_,
1761
- mainBranch,
1762
- milestoneBranch,
1763
- ).filter((entry) => !entry.path.startsWith(".gsd/"));
1764
- if (codeChanges.length > 0) {
1765
- const regularMergeChangedPaths = findRegularMergeChangedPaths(
1766
- originalBasePath_,
1767
- milestoneBranch,
1768
- mainBranch,
1769
- );
1770
- const unanchoredCodeChanges = codeChanges.filter((entry) =>
1771
- regularMergeChangedPaths.has(entry.path)
1772
- );
1773
- if (unanchoredCodeChanges.length > 0) {
1774
- process.chdir(previousCwd);
1775
- throw new GSDError(
1776
- GSD_GIT_ERROR,
1777
- `Milestone branch "${milestoneBranch}" is reachable from "${mainBranch}" ` +
1778
- `but has ${unanchoredCodeChanges.length} milestone-touched code file(s) not on current "${mainBranch}". ` +
1779
- `Aborting worktree teardown to prevent data loss.`,
1780
- );
1781
- }
1782
- }
1783
- debugLog("mergeMilestoneToMain", {
1784
- action: "skip-squash-already-merged",
1785
- milestoneId,
1786
- milestoneBranch,
1787
- mainBranch,
1788
- });
1789
- try {
1790
- clearProjectRootStateFiles(originalBasePath_, milestoneId);
1791
- } catch (err) {
1792
- logWarning("worktree", `clearProjectRootStateFiles failed during already-merged cleanup: ${err instanceof Error ? err.message : String(err)}`);
1793
- }
1794
- try {
1795
- removeWorktree(originalBasePath_, milestoneId, {
1796
- branch: milestoneBranch,
1797
- deleteBranch: false,
1798
- });
1799
- } catch (err) {
1800
- logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
1801
- }
1802
- try {
1803
- nativeBranchDelete(originalBasePath_, milestoneBranch);
1804
- } catch (err) {
1805
- logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
1806
- }
1807
- setActiveWorkspace(null);
1808
- nudgeGitBranchCache(previousCwd);
1809
- try {
1810
- process.chdir(originalBasePath_);
1811
- } catch (err) {
1812
- logWarning("worktree", `chdir to project root after already-merged cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1813
- }
1814
- return { commitMessage, pushed: false, prCreated: false, codeFilesChanged: true };
1815
- }
1816
-
1817
- // 7. Shelter queued milestone directories before the squash merge (#2505).
1818
- // The milestone branch may contain copies of queued milestone dirs (via
1819
- // copyPlanningArtifacts), so `git merge --squash` rejects when those same
1820
- // files exist as untracked in the working tree. Temporarily move them to
1821
- // a backup location, then restore after the merge+commit.
1822
- //
1823
- // MUST run BEFORE the pre-merge stash (step 7a) so `--include-untracked`
1824
- // does not sweep queued CONTEXT files into the stash. If stash pop later
1825
- // fails, files trapped inside the stash are permanently lost (#2505).
1826
- const planningDir = milestonesDir(originalBasePath_);
1827
- const shelterDir = join(gsdRoot(originalBasePath_), ".milestone-shelter");
1828
- const shelteredDirs: string[] = [];
1829
- let shelterRestored = false;
1830
- const mergeDirNames = new Set<string>([milestoneId]);
1831
- const resolvedMergeDir = resolveMilestonePath(originalBasePath_, milestoneId);
1832
- if (resolvedMergeDir) {
1833
- mergeDirNames.add(basename(resolvedMergeDir));
1834
- } else {
1835
- mergeDirNames.add(
1836
- canonicalPhaseDirName(milestoneId, getMilestone(milestoneId)?.title),
1837
- );
1838
- }
1839
-
1840
- // Helper: restore sheltered milestone directories (#2505).
1841
- // Called on both success and error paths to ensure queued CONTEXT files
1842
- // are never permanently lost. Idempotent — the error path may fire after
1843
- // the success path has already restored and removed the shelter dir; a
1844
- // second call is a no-op instead of logging a misleading "shelter restore
1845
- // failed: ENOENT" error for shelter sources that were cleaned up legitimately.
1846
- const restoreShelter = (): void => {
1847
- if (shelterRestored) return;
1848
- shelterRestored = true;
1849
- if (shelteredDirs.length === 0) return;
1850
- let restoreFailed = false;
1851
- for (const dirName of shelteredDirs) {
1852
- const src = join(shelterDir, dirName);
1853
- // If the shelter source is missing the restore cannot proceed for this
1854
- // entry. Distinguish "legitimately missing" (shelter dir removed by a
1855
- // prior successful restore or never copied) from a surprising ENOENT
1856
- // inside an otherwise-populated shelter.
1857
- if (!existsSync(src)) {
1858
- logWarning(
1859
- "worktree",
1860
- `shelter source missing for ${dirName}; skipping restore (shelter already cleaned or entry never staged)`,
1861
- );
1862
- continue;
1863
- }
1864
- try {
1865
- mkdirSync(planningDir, { recursive: true });
1866
- // Test seam: when _restoreEntryFn is injected, route the copy through it
1867
- // so the best-effort failure path (and shelter-retention guarantee) can
1868
- // be exercised deterministically. Production leaves it null → real cpSync.
1869
- const dest = join(planningDir, dirName);
1870
- if (_restoreEntryFn) {
1871
- _restoreEntryFn(src, dest);
1872
- } else {
1873
- cpSync(src, dest, { recursive: true, force: true });
1874
- }
1875
- } catch (err) { /* best-effort */
1876
- restoreFailed = true;
1877
- logError("worktree", `shelter restore failed (${dirName}): ${err instanceof Error ? err.message : String(err)}`);
1878
- }
1879
- }
1880
- // Preserve the shelter if any per-entry restore failed — it is the only
1881
- // surviving copy of the queued milestone dirs (sources were deleted during
1882
- // shelter). Deleting it here would permanently lose those files (#2505).
1883
- if (restoreFailed) {
1884
- logWarning("worktree", `shelter retained at ${shelterDir} — manual recovery required for unrestored entries`);
1885
- return;
1886
- }
1887
- if (existsSync(shelterDir)) {
1888
- try { rmSync(shelterDir, { recursive: true, force: true }); } catch (err) { /* best-effort */
1889
- logWarning("worktree", `shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1890
- }
1891
- }
1892
- };
1893
-
1894
- try {
1895
- if (existsSync(planningDir)) {
1896
- const entries = readdirSync(planningDir, { withFileTypes: true });
1897
- for (const entry of entries) {
1898
- if (!entry.isDirectory()) continue;
1899
- // Only shelter directories that do NOT belong to the milestone being merged
1900
- if (mergeDirNames.has(entry.name)) continue;
1901
- const srcDir = join(planningDir, entry.name);
1902
- const dstDir = join(shelterDir, entry.name);
1903
- try {
1904
- mkdirSync(shelterDir, { recursive: true });
1905
- cpSync(srcDir, dstDir, { recursive: true, force: true });
1906
- rmSync(srcDir, { recursive: true, force: true });
1907
- shelteredDirs.push(entry.name);
1908
- } catch (err) {
1909
- // Non-fatal — if shelter fails, the merge may still succeed
1910
- logWarning("worktree", `milestone shelter failed (${entry.name}): ${err instanceof Error ? err.message : String(err)}`);
1911
- }
1912
- }
1913
- }
1914
- } catch (err) {
1915
- // Non-fatal — proceed with merge; untracked files may block it
1916
- logWarning("worktree", `milestone shelter operation failed: ${err instanceof Error ? err.message : String(err)}`);
1917
- }
1918
-
1919
- // 7a. Stash pre-existing dirty files so the squash merge is not blocked by
1920
- // unrelated local changes (#2151). Includes untracked files to handle
1921
- // locally-added files that conflict with tracked files on the milestone
1922
- // branch. Passing NO pathspec lets git skip gitignored paths silently;
1923
- // adding an explicit pathspec trips a `git add`-style fatal on ignored
1924
- // entries (e.g. a gitignored `.gsd` symlink under ADR-002) (#4573).
1925
- // Queued CONTEXT files under `.gsd/phases/*` (or legacy milestones/*)
1926
- // are already sheltered in step 7 above, so they won't be swept into the stash.
1927
- // On Windows, SQLite holds mandatory file locks on the gsd.db WAL/SHM
1928
- // sidecars while the connection is open. `git stash --include-untracked`
1929
- // walks those files and fails with EBUSY (#4704). Close the DB before
1930
- // stashing so Windows releases the handles; reopen after. No-op on
1931
- // POSIX, where advisory locks don't block git.
1932
- const needsDbCycle = process.platform === "win32" && isDbAvailable();
1933
- const dbPathToReopen = needsDbCycle ? getWorkflowDatabasePath() : null;
1934
- if (needsDbCycle) {
1935
- try {
1936
- closeWorkflowDatabase();
1937
- } catch (err) {
1938
- logWarning("worktree", `pre-stash db close failed: ${err instanceof Error ? err.message : String(err)}`);
1939
- }
1940
- }
1941
-
1942
- let stashed = false;
1943
- // Embed a unique marker in the stash message so subsequent pop/drop targets
1944
- // the entry we created, not whatever happens to be at stash@{0} (concurrent
1945
- // milestone merges share the project-root stash list and can shift positions).
1946
- // (Issue #4980 HIGH-6)
1947
- let stashMarker: string | null = null;
1948
- try {
1949
- const status = execFileSync("git", ["status", "--porcelain"], {
1950
- cwd: originalBasePath_,
1951
- stdio: ["ignore", "pipe", "pipe"],
1952
- encoding: "utf-8",
1953
- }).trim();
1954
- if (status) {
1955
- stashMarker = `gsd-pre-merge:${milestoneId}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
1956
- execFileSync(
1957
- "git",
1958
- ["stash", "push", "--include-untracked", "-m", `gsd: pre-merge stash for ${milestoneId} [${stashMarker}]`],
1959
- { cwd: originalBasePath_, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
1960
- );
1961
- stashed = true;
1962
- }
1963
- } catch (err) {
1964
- // Stash failure is non-fatal — proceed without stash and let the merge
1965
- // report the dirty tree if it fails.
1966
- logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
1967
- }
1968
-
1969
- // 7b. Clean up stale merge state before attempting the merge (#2912).
1970
- // A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
1971
- // or interrupted operation) causes git merge to refuse with
1972
- // "fatal: You have not concluded your merge (MERGE_HEAD exists)".
1973
- // Defensively remove merge artifacts before starting.
1974
- removeMergeStateFiles(originalBasePath_, "pre-merge");
1975
-
1976
- // 8. Merge — respect merge_strategy preference (#549).
1977
- // "squash" (default): stages changes without a merge commit; caller commits.
1978
- // "merge": --no-ff --no-commit so caller can supply the commit message while
1979
- // git records a real merge commit (MERGE_HEAD present when nativeCommit runs).
1980
- const effectiveStrategy = prefs.merge_strategy === "merge" ? "merge" : "squash";
1981
- const mergeResult = effectiveStrategy === "merge"
1982
- ? nativeMergeRegular(originalBasePath_, milestoneBranch)
1983
- : nativeMergeSquash(originalBasePath_, milestoneBranch);
1984
- if (needsDbCycle && dbPathToReopen) {
1985
- try {
1986
- openWorkflowDatabasePath(dbPathToReopen);
1987
- } catch (err) {
1988
- logWarning("worktree", `post-merge db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
1989
- }
1990
- }
1991
-
1992
- if (!mergeResult.success) {
1993
- // Dirty working tree — the merge was rejected before it started (e.g.
1994
- // untracked .gsd/ files left by syncStateToProjectRoot). Preserve the
1995
- // milestone branch so commits are not lost.
1996
- if (mergeResult.conflicts.includes("__dirty_working_tree__")) {
1997
- // Defensively clean merge state — the native path may leave MERGE_HEAD
1998
- // even when the merge is rejected (#2912).
1999
- removeMergeStateFiles(originalBasePath_, "dirty-tree rejection");
2000
-
2001
- // Pop stash before throwing so local work is not lost.
2002
- if (stashed) {
2003
- try {
2004
- popStashByRef(originalBasePath_, stashMarker);
2005
- } catch (err) { /* stash pop conflict is non-fatal */
2006
- logWarning("worktree", `git stash pop failed: ${err instanceof Error ? err.message : String(err)}`);
2007
- }
2008
- }
2009
- restoreShelter();
2010
- // Restore cwd so the caller is not stranded on the integration branch
2011
- process.chdir(previousCwd);
2012
- // Surface the actual dirty filenames from git stderr instead of
2013
- // generically blaming .gsd/ (#2151).
2014
- const fileList = mergeResult.dirtyFiles?.length
2015
- ? `Dirty files:\n${mergeResult.dirtyFiles.map((f) => ` ${f}`).join("\n")}`
2016
- : `Check \`git status\` in the project root for details.`;
2017
- throw new GSDError(
2018
- GSD_GIT_ERROR,
2019
- `${effectiveStrategy === "merge" ? "Merge" : "Squash merge"} of ${milestoneBranch} rejected: working tree has dirty or untracked files ` +
2020
- `that conflict with the merge. ${fileList}`,
2021
- );
2022
- }
2023
-
2024
- // Check for conflicts — use merge result first, fall back to nativeConflictFiles
2025
- const conflictedFiles =
2026
- mergeResult.conflicts.length > 0
2027
- ? mergeResult.conflicts
2028
- : nativeConflictFiles(originalBasePath_);
2029
-
2030
- if (conflictedFiles.length > 0) {
2031
- // Separate auto-resolvable conflicts (GSD state files + build artifacts)
2032
- // from real code conflicts. GSD state files diverge between branches
2033
- // during normal operation. Build artifacts are machine-generated and
2034
- // regenerable. Both are safe to accept from the milestone branch.
2035
- const { resolved: autoResolved, remaining: codeConflicts } = autoResolveSafeConflictPaths(
2036
- originalBasePath_,
2037
- conflictedFiles,
2038
- );
2039
- if (autoResolved.length > 0) {
2040
- logWarning("worktree", `auto-resolved safe merge conflicts: ${autoResolved.join(", ")}`);
2041
- }
2042
-
2043
- // If there are still real code conflicts, escalate
2044
- if (codeConflicts.length > 0) {
2045
- cleanupConflictState(originalBasePath_);
2046
-
2047
- // Pop stash before throwing so local work is not lost (#2151).
2048
- if (stashed) {
2049
- try {
2050
- popStashByRef(originalBasePath_, stashMarker);
2051
- } catch (err) { /* stash pop conflict is non-fatal */
2052
- logWarning("worktree", `git stash pop failed: ${err instanceof Error ? err.message : String(err)}`);
2053
- }
2054
- }
2055
- restoreShelter();
2056
- // Restore cwd so the caller is not stranded on the integration branch.
2057
- // Without this, the next mergeMilestoneToMain call in a parallel merge
2058
- // sequence uses process.cwd() (now the project root) as worktreeCwd,
2059
- // causing autoCommitDirtyState to commit unrelated milestone files to
2060
- // the integration branch (#2929).
2061
- process.chdir(previousCwd);
2062
- throw new MergeConflictError(
2063
- codeConflicts,
2064
- effectiveStrategy,
2065
- milestoneBranch,
2066
- mainBranch,
2067
- );
2068
- }
2069
- }
2070
- // No conflicts detected — possibly "already up to date", fall through to commit
2071
- }
2072
-
2073
- // 9. Commit (handle nothing-to-commit gracefully)
2074
- const commitResult = nativeCommit(originalBasePath_, commitMessage);
2075
- const nothingToCommit = commitResult === null;
2076
-
2077
- // 9a. Clean up merge state files left by git merge --squash (#1853, #2912).
2078
- // git only removes SQUASH_MSG when the commit reads it directly (plain
2079
- // `git commit`). nativeCommit uses `-F -` (stdin) or libgit2, neither
2080
- // of which trigger git's SQUASH_MSG cleanup. MERGE_HEAD is created by
2081
- // libgit2's merge even in squash mode and is not removed by nativeCommit.
2082
- // If left on disk, doctor reports `corrupt_merge_state` on every subsequent run.
2083
- removeMergeStateFiles(originalBasePath_, "post-commit");
2084
-
2085
- // 9a-ii. Restore stashed files now that the merge+commit is complete (#2151).
2086
- // Pop after commit so stashed changes do not interfere with the squash merge
2087
- // or the commit content. Conflict on pop is non-fatal — the stash entry is
2088
- // preserved and the user can resolve manually with `git stash pop`.
2089
- if (stashed) {
2090
- let stashRefForDrop: string | null = null;
2091
- try {
2092
- stashRefForDrop = popStashByRef(originalBasePath_, stashMarker);
2093
- } catch (e) {
2094
- stashRefForDrop = stashRefFromError(e);
2095
- logWarning("worktree", `git stash pop failed, attempting conflict resolution: ${(e as Error).message}`);
2096
- // Stash pop after squash merge can conflict on .gsd/ state files that
2097
- // diverged between branches. Left unresolved, these UU entries block
2098
- // every subsequent merge. Auto-resolve them the same way we handle
2099
- // .gsd/ conflicts during the merge itself: accept HEAD (the just-committed
2100
- // version) and drop the now-applied stash.
2101
- const uu = nativeConflictFiles(originalBasePath_);
2102
- const gsdUU = uu.filter((f) => f.startsWith(".gsd/"));
2103
- const nonGsdUU = uu.filter((f) => !f.startsWith(".gsd/"));
2104
- const stashPopMessage = e instanceof Error ? e.message : String(e);
2105
- const isUntrackedRestoreFailure = stashPopMessage.includes("could not restore untracked files from stash");
2106
- const gsdContentConflicts: string[] = [];
2107
- const alreadyExists = stashAlreadyExistsFilesFromError(e);
2108
-
2109
- // Untracked-file restore failures can leave marker conflicts in tracked
2110
- // .gsd JSONL files without producing `U` status entries.
2111
- if (isUntrackedRestoreFailure) {
2112
- gsdContentConflicts.push(...gsdJsonlFilesWithConflictMarkers(originalBasePath_));
2113
- }
2114
- const gsdConflictFiles = [...new Set([...gsdUU, ...gsdContentConflicts])];
2115
-
2116
- if (gsdConflictFiles.length > 0) {
2117
- for (const f of gsdConflictFiles) {
2118
- try {
2119
- // Accept the committed (HEAD) version of the state file
2120
- execFileSync("git", ["checkout", "HEAD", "--", f], {
2121
- cwd: originalBasePath_,
2122
- stdio: ["ignore", "pipe", "pipe"],
2123
- encoding: "utf-8",
2124
- });
2125
- nativeAddPaths(originalBasePath_, [f]);
2126
- } catch (e) {
2127
- // Last resort: remove the conflicted state file
2128
- logWarning("worktree", `checkout HEAD failed for ${f}, removing: ${(e as Error).message}`);
2129
- nativeRmForce(originalBasePath_, [f]);
2130
- }
2131
- }
2132
- }
2133
-
2134
- if (gsdConflictFiles.length > 0 && nonGsdUU.length === 0) {
2135
- // All detected conflicts were .gsd/ files. Before dropping, verify no
2136
- // unresolved non-.gsd conflict markers or unmerged entries remain.
2137
- const remainingUnmerged = nativeConflictFiles(originalBasePath_);
2138
- const nonGsdUnmerged = remainingUnmerged.filter((f) => !f.startsWith(".gsd/"));
2139
- const markerCandidates = Array.from(new Set([
2140
- ...nonGsdUnmerged,
2141
- ...nativeLsFiles(originalBasePath_, "."),
2142
- ])).filter((f) => !f.startsWith(".gsd/"));
2143
- const nonGsdMarkerConflicts = markerCandidates.filter((f) =>
2144
- hasConflictMarkers(join(originalBasePath_, f)),
2145
- );
2146
- const hasRemainingNonGsdConflicts = nonGsdUnmerged.length > 0 || nonGsdMarkerConflicts.length > 0;
2147
- if (hasRemainingNonGsdConflicts) {
2148
- const files = Array.from(new Set([...nonGsdUnmerged, ...nonGsdMarkerConflicts]));
2149
- logWarning("reconcile", "Leaving stash because non-.gsd conflicts remain after auto-resolution", {
2150
- files: files.join(", "),
2151
- });
2152
- }
2153
-
2154
- // No non-.gsd conflicts remain — safe to drop the stash.
2155
- if (!hasRemainingNonGsdConflicts && stashRefForDrop) {
2156
- try {
2157
- execFileSync("git", ["stash", "drop", stashRefForDrop], {
2158
- cwd: originalBasePath_,
2159
- stdio: ["ignore", "pipe", "pipe"],
2160
- encoding: "utf-8",
2161
- });
2162
- } catch (err) { /* stash may already be consumed */
2163
- logWarning("worktree", `git stash drop failed: ${err instanceof Error ? err.message : String(err)}`);
2164
- }
2165
- } else if (!hasRemainingNonGsdConflicts) {
2166
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic drop");
2167
- }
2168
- } else if (
2169
- gsdUU.length === 0 &&
2170
- nonGsdUU.length === 0 &&
2171
- alreadyExists.length > 0
2172
- ) {
2173
- // Untracked-file restore failure from stash pop where all collided paths
2174
- // already exist after merge (committed on target). Safe to drop the stash
2175
- // for the full alreadyExists set — they were untracked on source by
2176
- // definition of the "already exists, no checkout" failure.
2177
- if (stashRefForDrop) {
2178
- try {
2179
- execFileSync("git", ["stash", "drop", stashRefForDrop], {
2180
- cwd: originalBasePath_,
2181
- stdio: ["ignore", "pipe", "pipe"],
2182
- encoding: "utf-8",
2183
- });
2184
- } catch (err) { /* stash may already be consumed */
2185
- logWarning("worktree", `git stash drop failed: ${err instanceof Error ? err.message : String(err)}`);
2186
- }
2187
- } else {
2188
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic drop");
2189
- }
2190
- } else if (nonGsdUU.length > 0) {
2191
- // Non-.gsd conflicts remain — leave stash for manual resolution
2192
- logWarning("reconcile", "Stash pop conflict on non-.gsd files after merge", {
2193
- files: nonGsdUU.join(", "),
2194
- });
2195
- } else {
2196
- logWarning(
2197
- "worktree",
2198
- "git stash pop failed without resolvable conflict files; leaving stash for manual recovery",
2199
- );
2200
- }
2201
- }
2202
- }
2203
-
2204
- // 9a-iii. Restore sheltered queued milestone directories (#2505).
2205
- restoreShelter();
2206
-
2207
- // 9b. Safety check (#1792): if nothing was committed, verify the milestone
2208
- // work is already on the integration branch before allowing teardown.
2209
- // Compare only non-.gsd/ paths — .gsd/ state files diverge normally and
2210
- // are auto-resolved during the squash merge.
2211
- if (nothingToCommit) {
2212
- const numstat = nativeDiffNumstat(
2213
- originalBasePath_,
2214
- mainBranch,
2215
- milestoneBranch,
2216
- );
2217
- const codeChanges = numstat.filter(
2218
- (entry) => !entry.path.startsWith(".gsd/"),
2219
- );
2220
- if (codeChanges.length > 0) {
2221
- // Milestone has unanchored code changes — abort teardown.
2222
- process.chdir(previousCwd);
2223
- throw new GSDError(
2224
- GSD_GIT_ERROR,
2225
- `${effectiveStrategy === "merge" ? "Merge" : "Squash merge"} produced nothing to commit but milestone branch "${milestoneBranch}" ` +
2226
- `has ${codeChanges.length} code file(s) not on "${mainBranch}". ` +
2227
- `Aborting worktree teardown to prevent data loss.`,
2228
- );
2229
- }
2230
- }
2231
-
2232
- // 9c. Detect whether any non-.gsd/ code files were actually merged (#1906).
2233
- // When a milestone only produced .gsd/ metadata (summaries, roadmaps) but no
2234
- // real code, the user sees "milestone complete" but nothing changed in their
2235
- // codebase. Surface this so the caller can warn the user.
2236
- //
2237
- // Bug #4385 fix: use `git diff-tree --root` instead of `git diff HEAD~1 HEAD`.
2238
- // `HEAD~1` does not exist on initial commits and is unreliable on shallow clones
2239
- // and merge commits. `diff-tree --root` handles all three cases correctly.
2240
- // The empty-tree hash (4b825dc…) is the universal fallback for refs that don't exist.
2241
- const GIT_EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
2242
- let codeFilesChanged = false;
2243
- if (!nothingToCommit) {
2244
- try {
2245
- const diffTreeOutput = execFileSync(
2246
- "git",
2247
- ["diff-tree", "--root", "--no-commit-id", "-r", "--name-only", "HEAD"],
2248
- { cwd: originalBasePath_, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
2249
- ).trim();
2250
- const mergedFiles = diffTreeOutput ? diffTreeOutput.split("\n").filter(Boolean) : [];
2251
- codeFilesChanged = mergedFiles.some((f) => !f.startsWith(".gsd/"));
2252
- } catch (e) {
2253
- // diff-tree failed (e.g. unborn HEAD in a brand-new repo) — fall back to
2254
- // comparing against the empty tree so initial-commit repos still report changes.
2255
- try {
2256
- const fallbackOutput = execFileSync(
2257
- "git",
2258
- ["diff", "--name-only", GIT_EMPTY_TREE, "HEAD"],
2259
- { cwd: originalBasePath_, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
2260
- ).trim();
2261
- const fallbackFiles = fallbackOutput ? fallbackOutput.split("\n").filter(Boolean) : [];
2262
- codeFilesChanged = fallbackFiles.some((f) => !f.startsWith(".gsd/"));
2263
- } catch {
2264
- // Truly unable to determine — assume code was changed to avoid silent data loss
2265
- logWarning("worktree", `diff-tree and empty-tree fallback both failed (assuming code changed): ${(e as Error).message}`);
2266
- codeFilesChanged = true;
2267
- }
2268
- }
2269
- }
2270
-
2271
- const finalizeMilestoneCleanup = (): void => {
2272
- // 12. Remove worktree directory first (must happen before branch deletion)
2273
- try {
2274
- removeWorktree(originalBasePath_, milestoneId, {
2275
- branch: milestoneBranch,
2276
- deleteBranch: false,
2277
- });
2278
- } catch (err) {
2279
- // Best-effort -- worktree dir may already be gone
2280
- logWarning("worktree", `worktree removal failed: ${err instanceof Error ? err.message : String(err)}`);
2281
- }
2282
-
2283
- // 13. Delete milestone branch (after worktree removal so ref is unlocked)
2284
- try {
2285
- nativeBranchDelete(originalBasePath_, milestoneBranch);
2286
- } catch (err) {
2287
- // Best-effort
2288
- logWarning("worktree", `git branch-delete failed: ${err instanceof Error ? err.message : String(err)}`);
2289
- }
2290
-
2291
- // 14. Clear module state
2292
- setActiveWorkspace(null);
2293
- nudgeGitBranchCache(previousCwd);
2294
-
2295
- // 15. Anchor cwd at the project root on success-return. Step 12 removed
2296
- // the worktree dir; if cwd was inside it, every subsequent process.cwd()
2297
- // would throw ENOENT and trip auto/run-unit.ts:50's session-failed cancel
2298
- // path (the de73fb43d regression that closes headless gsd auto). Step 3
2299
- // already chdir'd here, but defending the success-return contract makes
2300
- // future maintainers safe against intervening chdir's between step 3 and
2301
- // here.
2302
- try {
2303
- // process.cwd() can throw ENOENT when cwd was removed, so attempt
2304
- // recovery directly.
2305
- process.chdir(originalBasePath_);
2306
- } catch (err) {
2307
- logWarning("worktree", `chdir to project root after merge failed: ${err instanceof Error ? err.message : String(err)}`);
2308
- debugLog("mergeMilestoneToMain", {
2309
- phase: "post-merge-chdir-failed",
2310
- target: originalBasePath_,
2311
- error: err instanceof Error ? err.message : String(err),
2312
- });
2313
- }
2314
- };
2315
-
2316
- let shouldCleanup = false;
2317
- try {
2318
- // 10/9b. Publication (auto-push / draft PR) — Publication module seam (ADR-034).
2319
- const publication = publishMilestone({
2320
- basePath: originalBasePath_,
2321
- milestoneId,
2322
- milestoneTitle,
2323
- integrationBranch: mainBranch,
2324
- milestoneBranch,
2325
- sliceSummaries: completedSlices.map((slice) => `### ${slice.id}\n${slice.title}`),
2326
- nothingToCommit,
2327
- prefs: {
2328
- autoPush: prefs.auto_push === true,
2329
- autoPr: prefs.auto_pr === true,
2330
- remote: prefs.remote,
2331
- prTargetBranch: prefs.pr_target_branch,
2332
- },
2333
- });
2334
- const { pushed, prCreated } = publication;
2335
-
2336
- // 11. Guard removed — step 9b (#1792) now handles this with a smarter check:
2337
- // throws only when the milestone has unanchored code changes, passes
2338
- // through when the code is genuinely already on the integration branch.
2339
-
2340
- // 11a. Pre-teardown safety net (#1853): if the worktree still has uncommitted
2341
- // changes (e.g. nativeHasChanges cache returned stale false), abort teardown.
2342
- // Committing here would be too late: the squash merge to the integration
2343
- // branch already happened, so a new milestone-branch commit would not be
2344
- // included and branch deletion could drop the only ref to that work.
2345
- //
2346
- // Guard: only run when worktreeCwd is on the milestone branch (#2929).
2347
- // In parallel mode or branch-mode merges, worktreeCwd may be the project
2348
- // root on the integration branch. Committing dirty state there would
2349
- // capture unrelated files from other milestones.
2350
- if (existsSync(worktreeCwd)) {
2351
- let preTeardownBranch: string | null = null;
2352
- try {
2353
- preTeardownBranch = nativeGetCurrentBranch(worktreeCwd);
2354
- } catch (err) {
2355
- debugLog("mergeMilestoneToMain", { phase: "pre-teardown-branch-detect-failed", error: String(err) });
2356
- }
2357
- const isOnMilestoneBranch = preTeardownBranch === milestoneBranch;
2358
-
2359
- if (isOnMilestoneBranch) {
2360
- try {
2361
- const dirtyCheck = nativeWorkingTreeStatus(worktreeCwd);
2362
- if (dirtyCheck) {
2363
- process.chdir(previousCwd);
2364
- throw new GSDError(
2365
- GSD_GIT_ERROR,
2366
- `Milestone worktree still has uncommitted changes after squash merge. ` +
2367
- `Aborting teardown to preserve ${milestoneBranch}. Status:\n${dirtyCheck}`,
2368
- );
2369
- }
2370
- } catch (e) {
2371
- if (e instanceof GSDError) throw e;
2372
- debugLog("mergeMilestoneToMain", {
2373
- phase: "pre-teardown-dirty-check-error",
2374
- error: String(e),
2375
- });
2376
- }
2377
- }
2378
- }
2379
-
2380
- shouldCleanup = true;
2381
- return { commitMessage, pushed, prCreated, codeFilesChanged };
2382
- } finally {
2383
- if (shouldCleanup) {
2384
- finalizeMilestoneCleanup();
2385
- } else {
2386
- logWarning(
2387
- "worktree",
2388
- `Skipping worktree cleanup for ${milestoneBranch}; merge did not reach safe-cleanup point and milestone work is preserved for manual recovery.`,
2389
- );
2390
- }
2391
- }
2392
- }