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