@opengsd/gsd-pi 1.2.0-dev.4c756166 → 1.2.0-dev.955e4da0

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 (246) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/bg-shell/utilities.js +2 -2
  3. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  4. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +8 -2
  5. package/dist/resources/extensions/gsd/auto/orchestrator.js +33 -4
  6. package/dist/resources/extensions/gsd/auto/phases.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-post-unit.js +8 -6
  8. package/dist/resources/extensions/gsd/auto-start.js +8 -13
  9. package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.js +13 -270
  11. package/dist/resources/extensions/gsd/auto.js +4 -7
  12. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -6
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -3
  14. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +26 -4
  15. package/dist/resources/extensions/gsd/captures.js +5 -13
  16. package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
  17. package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
  18. package/dist/resources/extensions/gsd/db/engine.js +755 -0
  19. package/dist/resources/extensions/gsd/db/queries.js +372 -0
  20. package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
  21. package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
  22. package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
  23. package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
  24. package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
  25. package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
  26. package/dist/resources/extensions/gsd/doctor-environment.js +8 -10
  27. package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -3
  28. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +9 -2
  29. package/dist/resources/extensions/gsd/git-service.js +1 -0
  30. package/dist/resources/extensions/gsd/gitignore.js +3 -0
  31. package/dist/resources/extensions/gsd/gsd-db.js +171 -2048
  32. package/dist/resources/extensions/gsd/guided-flow.js +34 -3
  33. package/dist/resources/extensions/gsd/migrate/safety.js +17 -9
  34. package/dist/resources/extensions/gsd/migration-auto-check.js +24 -3
  35. package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
  36. package/dist/resources/extensions/gsd/model-router.js +3 -0
  37. package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
  38. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +7 -5
  39. package/dist/resources/extensions/gsd/paths.js +10 -24
  40. package/dist/resources/extensions/gsd/preferences.js +14 -0
  41. package/dist/resources/extensions/gsd/recovery-classification.js +12 -1
  42. package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
  43. package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
  44. package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
  45. package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
  46. package/dist/resources/extensions/gsd/status-guards.js +56 -8
  47. package/dist/resources/extensions/gsd/tools/complete-slice.js +24 -43
  48. package/dist/resources/extensions/gsd/tools/exec-tool.js +5 -5
  49. package/dist/resources/extensions/gsd/tools/reopen-milestone.js +11 -29
  50. package/dist/resources/extensions/gsd/tools/reopen-slice.js +14 -33
  51. package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
  52. package/dist/resources/extensions/gsd/undo.js +8 -7
  53. package/dist/resources/extensions/gsd/worktree-git-recovery.js +287 -0
  54. package/dist/resources/extensions/gsd/worktree-lifecycle.js +9 -1
  55. package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
  56. package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
  57. package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
  58. package/dist/resources/extensions/gsd/worktree-root.js +17 -6
  59. package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
  60. package/dist/resources/extensions/gsd/worktree-session-state.js +12 -10
  61. package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
  62. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +13 -13
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  101. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  102. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  103. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  104. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  105. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  106. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  107. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  108. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  109. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  110. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  111. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  112. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  113. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  115. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  116. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  117. package/dist/web/standalone/.next/server/app/index.html +1 -1
  118. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  119. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  120. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  121. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app-paths-manifest.json +13 -13
  125. package/dist/web/standalone/.next/server/chunks/{5047.js → 5942.js} +2 -2
  126. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  127. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  128. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  129. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  130. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  131. package/dist/worktree-status-banner.js +7 -3
  132. package/package.json +1 -1
  133. package/packages/cloud-mcp-gateway/package.json +2 -2
  134. package/packages/contracts/package.json +1 -1
  135. package/packages/daemon/package.json +4 -4
  136. package/packages/gsd-agent-core/package.json +5 -5
  137. package/packages/gsd-agent-modes/package.json +7 -7
  138. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  139. package/packages/mcp-server/dist/workflow-tools.js +30 -21
  140. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  141. package/packages/mcp-server/package.json +3 -3
  142. package/packages/native/package.json +1 -1
  143. package/packages/pi-agent-core/package.json +1 -1
  144. package/packages/pi-ai/dist/models.generated.d.ts +266 -35
  145. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  146. package/packages/pi-ai/dist/models.generated.js +235 -46
  147. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  148. package/packages/pi-ai/package.json +1 -1
  149. package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
  151. package/packages/pi-coding-agent/dist/core/capability-patches.js.map +1 -1
  152. package/packages/pi-coding-agent/package.json +7 -7
  153. package/packages/pi-tui/package.json +2 -2
  154. package/packages/rpc-client/package.json +2 -2
  155. package/pkg/package.json +1 -1
  156. package/src/resources/extensions/bg-shell/utilities.ts +2 -2
  157. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  158. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +6 -0
  159. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +28 -0
  160. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -1
  161. package/src/resources/extensions/gsd/auto/orchestrator.ts +39 -5
  162. package/src/resources/extensions/gsd/auto/phases.ts +10 -1
  163. package/src/resources/extensions/gsd/auto-post-unit.ts +12 -5
  164. package/src/resources/extensions/gsd/auto-start.ts +8 -14
  165. package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
  166. package/src/resources/extensions/gsd/auto-worktree.ts +20 -280
  167. package/src/resources/extensions/gsd/auto.ts +12 -9
  168. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +10 -6
  169. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -3
  170. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +25 -3
  171. package/src/resources/extensions/gsd/captures.ts +5 -14
  172. package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
  173. package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
  174. package/src/resources/extensions/gsd/db/engine.ts +809 -0
  175. package/src/resources/extensions/gsd/db/queries.ts +453 -0
  176. package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
  177. package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
  178. package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
  179. package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
  180. package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
  181. package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
  182. package/src/resources/extensions/gsd/doctor-environment.ts +8 -11
  183. package/src/resources/extensions/gsd/doctor-git-checks.ts +3 -3
  184. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +10 -3
  185. package/src/resources/extensions/gsd/git-service.ts +1 -0
  186. package/src/resources/extensions/gsd/gitignore.ts +3 -0
  187. package/src/resources/extensions/gsd/gsd-db.ts +173 -2373
  188. package/src/resources/extensions/gsd/guided-flow.ts +34 -3
  189. package/src/resources/extensions/gsd/migrate/safety.ts +15 -7
  190. package/src/resources/extensions/gsd/migration-auto-check.ts +28 -3
  191. package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
  192. package/src/resources/extensions/gsd/model-router.ts +3 -0
  193. package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
  194. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +6 -5
  195. package/src/resources/extensions/gsd/paths.ts +9 -22
  196. package/src/resources/extensions/gsd/preferences.ts +18 -0
  197. package/src/resources/extensions/gsd/recovery-classification.ts +14 -1
  198. package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
  199. package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
  200. package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
  201. package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
  202. package/src/resources/extensions/gsd/status-guards.ts +59 -8
  203. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +123 -0
  204. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +3 -1
  205. package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
  206. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
  207. package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
  208. package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +22 -0
  209. package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
  210. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
  211. package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +5 -4
  212. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +1 -1
  213. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
  214. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +85 -1
  215. package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
  216. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +91 -1
  217. package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
  218. package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
  219. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +43 -6
  220. package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
  221. package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
  222. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
  223. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +22 -1
  224. package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
  225. package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
  226. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +3 -1
  227. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
  228. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
  229. package/src/resources/extensions/gsd/tests/write-gate.test.ts +42 -0
  230. package/src/resources/extensions/gsd/tools/complete-slice.ts +23 -58
  231. package/src/resources/extensions/gsd/tools/exec-tool.ts +5 -5
  232. package/src/resources/extensions/gsd/tools/reopen-milestone.ts +11 -38
  233. package/src/resources/extensions/gsd/tools/reopen-slice.ts +14 -42
  234. package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
  235. package/src/resources/extensions/gsd/undo.ts +9 -8
  236. package/src/resources/extensions/gsd/worktree-git-recovery.ts +308 -0
  237. package/src/resources/extensions/gsd/worktree-lifecycle.ts +10 -1
  238. package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
  239. package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
  240. package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
  241. package/src/resources/extensions/gsd/worktree-root.ts +17 -6
  242. package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
  243. package/src/resources/extensions/gsd/worktree-session-state.ts +12 -10
  244. package/src/resources/skills/gsd-browser/SKILL.md +1 -1
  245. /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_buildManifest.js +0 -0
  246. /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_ssgManifest.js +0 -0
@@ -11,8 +11,7 @@
11
11
  */
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { join } from "node:path";
14
- import { isClosedStatus } from "../status-guards.js";
15
- import { transaction, insertMilestone, insertSlice, getSlice, getSliceTasks, getMilestone, updateSliceStatus, setSliceSummaryMd, saveGateResult, getPendingGatesForTurn, getMilestoneSlices, updateMilestoneStatus, } from "../gsd-db.js";
14
+ import { completeSliceCascade, setSliceSummaryMd, saveGateResult, getPendingGatesForTurn, } from "../gsd-db.js";
16
15
  import { getGatesForTurn } from "../gate-registry.js";
17
16
  import { gsdProjectionRoot, clearPathCache, resolveMilestoneFile } from "../paths.js";
18
17
  import { resolveCanonicalMilestoneRoot } from "../worktree-manager.js";
@@ -295,52 +294,34 @@ export async function handleCompleteSlice(params, basePath) {
295
294
  error: `UAT requires browser verification (opening a page in a browser, navigating to a page or localhost, screenshots) but declares "UAT mode: artifact-driven", which only runs static/file checks and would defer the browser work to a human. Use a mode that actually verifies the UI: "browser-executable" (interactive browser tools), "runtime-executable" (a browser test command such as playwright), or a browser-inclusive "mixed"/"live-runtime". Re-author the UAT Type section and complete the slice again.`,
296
295
  };
297
296
  }
298
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
297
+ // ── Atomic completion cascade (guards + writes in one transaction) ───────
299
298
  const completedAt = new Date().toISOString();
300
299
  let guardError = null;
301
300
  let existingSummaryMd = "";
302
301
  let duplicateComplete = false;
303
- transaction(() => {
304
- // State machine preconditions (inside txn for atomicity).
305
- // Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
306
- // Only block if they exist and are closed.
307
- const milestone = getMilestone(params.milestoneId);
308
- if (milestone && isClosedStatus(milestone.status)) {
309
- guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
310
- return;
311
- }
312
- const slice = getSlice(params.milestoneId, params.sliceId);
313
- existingSummaryMd = slice?.full_summary_md?.trim() ?? "";
314
- if (slice && isClosedStatus(slice.status)) {
315
- duplicateComplete = true;
316
- return;
317
- }
318
- // Verify all tasks are complete
319
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
320
- if (tasks.length === 0) {
321
- guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
322
- return;
323
- }
324
- const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
325
- if (incompleteTasks.length > 0) {
326
- const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
327
- guardError = `incomplete tasks: ${incompleteIds}`;
328
- return;
329
- }
330
- // All guards passed — perform writes. Preserve existing planning metadata:
331
- // completion should not overwrite title/risk/depends/demo/sequence.
332
- insertMilestone({ id: params.milestoneId, title: params.milestoneId });
333
- if (!slice) {
334
- insertSlice({ id: params.sliceId, milestoneId: params.milestoneId, title: params.sliceTitle || params.sliceId });
335
- }
336
- updateSliceStatus(params.milestoneId, params.sliceId, "complete", completedAt);
337
- const updatedSlices = getMilestoneSlices(params.milestoneId);
338
- if (milestone?.status === "planned" &&
339
- updatedSlices.length > 0 &&
340
- updatedSlices.every((s) => isClosedStatus(s.status))) {
341
- updateMilestoneStatus(params.milestoneId, "active");
342
- }
302
+ const outcome = completeSliceCascade(params.milestoneId, params.sliceId, {
303
+ sliceTitle: params.sliceTitle,
304
+ completedAt,
343
305
  });
306
+ if (outcome.ok) {
307
+ existingSummaryMd = outcome.existingSummaryMd;
308
+ duplicateComplete = outcome.duplicate;
309
+ }
310
+ else {
311
+ switch (outcome.reason) {
312
+ case "milestone-closed":
313
+ guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${outcome.status})`;
314
+ break;
315
+ case "no-tasks":
316
+ guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
317
+ break;
318
+ case "incomplete-tasks": {
319
+ const incompleteIds = outcome.incomplete.map((t) => `${t.id} (status: ${t.status})`).join(", ");
320
+ guardError = `incomplete tasks: ${incompleteIds}`;
321
+ break;
322
+ }
323
+ }
324
+ }
344
325
  if (duplicateComplete) {
345
326
  const staleSummaryPath = sliceSummaryPath(artifactBasePath, params.milestoneId, params.sliceId);
346
327
  const duplicateIsStale = isStaleWrite("complete-slice");
@@ -4,6 +4,7 @@ import { EXEC_DEFAULTS, runExecSandbox, } from "../exec-sandbox.js";
4
4
  import { realpathSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import { isContextModeEnabled } from "../preferences-types.js";
7
+ import { findWorktreeSegment } from "../worktree-root.js";
7
8
  import { contextModeDisabledResult } from "./context-mode-tool-result.js";
8
9
  const UAT_EXEC_INTENTS = [
9
10
  "uat-artifact-check",
@@ -141,12 +142,11 @@ function normalizeScanPath(value) {
141
142
  }
142
143
  function parseWorktreeBase(baseDir) {
143
144
  const normalizedBase = normalizeScanPath(baseDir);
144
- const marker = "/.gsd/worktrees/";
145
- const markerIndex = normalizedBase.indexOf(marker);
146
- if (markerIndex <= 0)
145
+ const segment = findWorktreeSegment(normalizedBase);
146
+ if (!segment || segment.gsdIdx <= 0)
147
147
  return null;
148
148
  return {
149
- originalRoot: normalizedBase.slice(0, markerIndex),
149
+ originalRoot: normalizedBase.slice(0, segment.gsdIdx),
150
150
  worktreeRoot: normalizedBase,
151
151
  };
152
152
  }
@@ -234,7 +234,7 @@ function scriptReferencesOriginalRootFromWorktree(script, baseDir) {
234
234
  return false;
235
235
  const normalizedScript = script.replace(/\\/g, "/");
236
236
  return comparablePathVariants(parsed.originalRoot).some((originalRoot) => {
237
- const originalRootPattern = new RegExp(`${escapeRegExp(originalRoot)}(?=$|[\\s'"\\\`;)&|<>]|/(?!\\.gsd/worktrees(?:/|$)))`);
237
+ const originalRootPattern = new RegExp(`${escapeRegExp(originalRoot)}(?=$|[\\s'"\\\`;)&|<>]|/(?!\\.gsd(?:-worktrees|/worktrees)(?:/|$)))`);
238
238
  return originalRootPattern.test(normalizedScript);
239
239
  });
240
240
  }
@@ -7,9 +7,8 @@
7
7
  * artifacts so the DB-filesystem reconciler does not auto-correct
8
8
  * entities back to "complete".
9
9
  */
10
- import { getMilestone, getMilestoneSlices, getSliceTasks, reopenMilestoneStatus, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
10
+ import { getMilestoneSlices, getSliceTasks, reopenMilestoneCascade, } from "../gsd-db.js";
11
11
  import { invalidateStateCache } from "../state.js";
12
- import { isClosedStatus } from "../status-guards.js";
13
12
  import { renderAllProjections } from "../workflow-projections.js";
14
13
  import { writeManifest } from "../workflow-manifest.js";
15
14
  import { appendEvent } from "../workflow-events.js";
@@ -23,35 +22,18 @@ export async function handleReopenMilestone(params, basePath) {
23
22
  if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
24
23
  return { error: "milestoneId is required and must be a non-empty string" };
25
24
  }
26
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
27
- let guardError = null;
28
- let slicesResetCount = 0;
29
- let tasksResetCount = 0;
30
- transaction(() => {
31
- const milestone = getMilestone(params.milestoneId);
32
- if (!milestone) {
33
- guardError = `milestone not found: ${params.milestoneId}`;
34
- return;
25
+ // ── Atomic reopen cascade (guards + writes in one transaction) ───────────
26
+ const outcome = reopenMilestoneCascade(params.milestoneId);
27
+ if (!outcome.ok) {
28
+ switch (outcome.reason) {
29
+ case "milestone-not-found":
30
+ return { error: `milestone not found: ${params.milestoneId}` };
31
+ case "milestone-not-closed":
32
+ return { error: `milestone ${params.milestoneId} is not closed (status: ${outcome.status}) — nothing to reopen` };
35
33
  }
36
- if (!isClosedStatus(milestone.status)) {
37
- guardError = `milestone ${params.milestoneId} is not closed (status: ${milestone.status}) — nothing to reopen`;
38
- return;
39
- }
40
- reopenMilestoneStatus(params.milestoneId);
41
- const slices = getMilestoneSlices(params.milestoneId);
42
- slicesResetCount = slices.length;
43
- for (const slice of slices) {
44
- updateSliceStatus(params.milestoneId, slice.id, "in_progress");
45
- const tasks = getSliceTasks(params.milestoneId, slice.id);
46
- tasksResetCount += tasks.length;
47
- for (const task of tasks) {
48
- updateTaskStatus(params.milestoneId, slice.id, task.id, "pending");
49
- }
50
- }
51
- });
52
- if (guardError) {
53
- return { error: guardError };
54
34
  }
35
+ const slicesResetCount = outcome.slicesReset;
36
+ const tasksResetCount = outcome.tasksReset;
55
37
  // ── Invalidate caches ────────────────────────────────────────────────────
56
38
  invalidateStateCache();
57
39
  // ── Clean up stale filesystem artifacts (M12 fix) ────────────────────────
@@ -9,9 +9,8 @@
9
9
  */
10
10
  // GSD — reopen-slice tool handler
11
11
  // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
12
- import { getMilestone, getSlice, getSliceTasks, updateSliceStatus, updateTaskStatus, transaction, } from "../gsd-db.js";
12
+ import { getSliceTasks, reopenSliceCascade, } from "../gsd-db.js";
13
13
  import { invalidateStateCache } from "../state.js";
14
- import { isClosedStatus } from "../status-guards.js";
15
14
  import { renderAllProjections } from "../workflow-projections.js";
16
15
  import { writeManifest } from "../workflow-manifest.js";
17
16
  import { appendEvent } from "../workflow-events.js";
@@ -27,39 +26,21 @@ export async function handleReopenSlice(params, basePath) {
27
26
  if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") {
28
27
  return { error: "milestoneId is required and must be a non-empty string" };
29
28
  }
30
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
31
- let guardError = null;
32
- let tasksResetCount = 0;
33
- transaction(() => {
34
- const milestone = getMilestone(params.milestoneId);
35
- if (!milestone) {
36
- guardError = `milestone not found: ${params.milestoneId}`;
37
- return;
29
+ // ── Atomic reopen cascade (guards + writes in one transaction) ───────────
30
+ const outcome = reopenSliceCascade(params.milestoneId, params.sliceId);
31
+ if (!outcome.ok) {
32
+ switch (outcome.reason) {
33
+ case "milestone-not-found":
34
+ return { error: `milestone not found: ${params.milestoneId}` };
35
+ case "milestone-closed":
36
+ return { error: `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${outcome.status})` };
37
+ case "slice-not-found":
38
+ return { error: `slice not found: ${params.milestoneId}/${params.sliceId}` };
39
+ case "slice-not-complete":
40
+ return { error: `slice ${params.sliceId} is not complete (status: ${outcome.status}) — nothing to reopen` };
38
41
  }
39
- if (isClosedStatus(milestone.status)) {
40
- guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
41
- return;
42
- }
43
- const slice = getSlice(params.milestoneId, params.sliceId);
44
- if (!slice) {
45
- guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
46
- return;
47
- }
48
- if (!isClosedStatus(slice.status)) {
49
- guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
50
- return;
51
- }
52
- // Fetch tasks inside txn so the list is consistent with the slice status check
53
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
54
- tasksResetCount = tasks.length;
55
- updateSliceStatus(params.milestoneId, params.sliceId, "in_progress");
56
- for (const task of tasks) {
57
- updateTaskStatus(params.milestoneId, params.sliceId, task.id, "pending");
58
- }
59
- });
60
- if (guardError) {
61
- return { error: guardError };
62
42
  }
43
+ const tasksResetCount = outcome.tasksReset;
63
44
  // ── Invalidate caches ────────────────────────────────────────────────────
64
45
  invalidateStateCache();
65
46
  // ── Clean up stale filesystem artifacts (M12 fix) ────────────────────────
@@ -9,8 +9,7 @@
9
9
  * This function performs DB writes only. The MCP wrapper in
10
10
  * bootstrap/db-tools.ts handles state-cache invalidation and STATE.md rebuild.
11
11
  */
12
- import { getSlice, getSliceTasks, isDbAvailable, transaction, updateSliceStatus, updateTaskStatus, } from "../gsd-db.js";
13
- import { isClosedStatus } from "../status-guards.js";
12
+ import { isDbAvailable, skipSliceCascade, } from "../gsd-db.js";
14
13
  /**
15
14
  * Mark a slice as "skipped" and cascade the skip to every non-closed task in
16
15
  * that slice. Runs as a single transaction so slice status and task statuses
@@ -39,40 +38,23 @@ export function handleSkipSlice(params) {
39
38
  if (!isDbAvailable()) {
40
39
  throw new Error("handleSkipSlice: GSD database is not available");
41
40
  }
42
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ────
43
- let guardError = null;
44
- let guardCode = null;
45
- let wasAlreadySkipped = false;
46
- let tasksSkipped = 0;
47
- transaction(() => {
48
- const slice = getSlice(params.milestoneId, params.sliceId);
49
- if (!slice) {
50
- guardError = `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`;
51
- guardCode = "slice_not_found";
52
- return;
41
+ // ── Atomic skip cascade (guards + writes in one transaction) ────────────
42
+ const outcome = skipSliceCascade(params.milestoneId, params.sliceId);
43
+ if (!outcome.ok) {
44
+ switch (outcome.reason) {
45
+ case "slice-not-found":
46
+ return {
47
+ ...base,
48
+ error: `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`,
49
+ errorCode: "slice_not_found",
50
+ };
51
+ case "slice-already-complete":
52
+ return {
53
+ ...base,
54
+ error: `Slice ${params.sliceId} is already complete — cannot skip.`,
55
+ errorCode: "already_complete",
56
+ };
53
57
  }
54
- if (slice.status === "complete" || slice.status === "done") {
55
- guardError = `Slice ${params.sliceId} is already complete — cannot skip.`;
56
- guardCode = "already_complete";
57
- return;
58
- }
59
- wasAlreadySkipped = slice.status === "skipped";
60
- if (!wasAlreadySkipped) {
61
- updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
62
- }
63
- // Cascade: mark every non-closed task as skipped so milestone completion
64
- // doesn't trip the deep-task guard (#4375). Closed tasks (complete/done/
65
- // skipped) are left untouched — we never downgrade.
66
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
67
- for (const task of tasks) {
68
- if (!isClosedStatus(task.status)) {
69
- updateTaskStatus(params.milestoneId, params.sliceId, task.id, "skipped");
70
- tasksSkipped++;
71
- }
72
- }
73
- });
74
- if (guardError) {
75
- return { ...base, error: guardError, errorCode: guardCode ?? undefined };
76
58
  }
77
- return { ...base, tasksSkipped, wasAlreadySkipped };
59
+ return { ...base, tasksSkipped: outcome.tasksSkipped, wasAlreadySkipped: outcome.wasAlreadySkipped };
78
60
  }
@@ -11,7 +11,7 @@ import { deriveState } from "./state.js";
11
11
  import { invalidateAllCaches } from "./cache.js";
12
12
  import { gsdRoot, resolveTasksDir, resolveSlicePath, resolveTaskFile, buildTaskFileName, buildSliceFileName } from "./paths.js";
13
13
  import { sendDesktopNotification } from "./notifications.js";
14
- import { getTask, getSlice, getSliceTasks, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
14
+ import { getTask, getSlice, getSliceTasks, updateTaskStatus, resetSliceCascade } from "./gsd-db.js";
15
15
  import { renderPlanCheckboxes, renderRoadmapCheckboxes } from "./markdown-renderer.js";
16
16
  /**
17
17
  * Undo the last completed unit: revert git commits,
@@ -275,20 +275,21 @@ export async function handleResetSlice(args, ctx, _pi, basePath) {
275
275
  `Run /gsd reset-slice ${rawId} --force to confirm.`, "warning");
276
276
  return;
277
277
  }
278
- // Reset all tasks
279
- let tasksReset = 0;
278
+ // Reset all task statuses to "pending" and the slice to "active" in one
279
+ // atomic commit (DB is source of truth). Previously a per-task updateTaskStatus
280
+ // loop + a separate updateSliceStatus, which could leave a partial reset if
281
+ // interrupted mid-loop.
282
+ resetSliceCascade(mid, sid);
283
+ // Delete task summary files — projection cleanup, separate from the DB reset.
284
+ const tasksReset = tasks.length;
280
285
  let summariesDeleted = 0;
281
286
  for (const t of tasks) {
282
- updateTaskStatus(mid, sid, t.id, "pending");
283
- tasksReset++;
284
287
  const summaryPath = resolveTaskFile(basePath, mid, sid, t.id, "SUMMARY");
285
288
  if (summaryPath && existsSync(summaryPath)) {
286
289
  unlinkSync(summaryPath);
287
290
  summariesDeleted++;
288
291
  }
289
292
  }
290
- // Reset slice status
291
- updateSliceStatus(mid, sid, "active");
292
293
  // Delete slice summary and UAT files
293
294
  let sliceFilesDeleted = 0;
294
295
  const slicePath = resolveSlicePath(basePath, mid, sid);
@@ -0,0 +1,287 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Git checkout/stash/merge-state recovery primitives for worktree operations.
3
+ /**
4
+ * Worktree Git Recovery — the recurring-bug hot spot, in one place.
5
+ *
6
+ * Owns the verbs that recover a repository from interrupted or conflicting
7
+ * git operations during worktree transitions:
8
+ *
9
+ * - `checkoutBranchWithStashGuard` — branch switch with stash protection,
10
+ * including the stash-pop EEXIST collision recovery for `.gsd/` runtime
11
+ * files (force-checkout + targeted stash drop).
12
+ * - `removeMergeStateFiles` — clears SQUASH_MSG / MERGE_HEAD / etc. left by
13
+ * a failed merge so subsequent merges don't fail on stale state.
14
+ * - `cleanupConflictState` — merge-abort + index reset + state-file cleanup
15
+ * after a conflicted (including squash) merge.
16
+ * - stash helpers (`popStashByRef`, `stashRefFromError`,
17
+ * `stashAlreadyExistsFilesFromError`, `gsdJsonlFilesWithConflictMarkers`)
18
+ * used by the merge pipeline in auto-worktree.ts.
19
+ *
20
+ * Extracted from auto-worktree.ts so recovery fixes land here instead of as
21
+ * embedded special cases in a 2,600-line orchestration module, and so the
22
+ * rules can be tested against scripted git states.
23
+ *
24
+ * The State Reconciliation drift repair (`state-reconciliation/drift/
25
+ * merge-state.ts`) keeps its own merge-state primitive by design — drift
26
+ * repairs own their raw primitives (see CONTEXT.md, Drift repair).
27
+ */
28
+ import { execFileSync } from "node:child_process";
29
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
30
+ import { isAbsolute, join, resolve } from "node:path";
31
+ import { debugLog } from "./debug-logger.js";
32
+ import { logError, logWarning } from "./workflow-logger.js";
33
+ import { nativeAddPaths, nativeCheckoutBranch, nativeConflictFiles, nativeLsFiles, nativeMergeAbort, nativeWorkingTreeStatus, } from "./native-git-bridge.js";
34
+ import { resolveGitDir } from "./worktree-manager.js";
35
+ /**
36
+ * Pop the stash entry created with `stashMarker` in its subject, resolving it
37
+ * to a concrete `stash@{n}` ref first so a concurrent stash push cannot make
38
+ * `git stash pop` grab the wrong entry.
39
+ *
40
+ * If `stashMarker` is null or no longer present in the stash list (e.g. a
41
+ * concurrent process popped/dropped it), leaves the stash list untouched and
42
+ * returns null.
43
+ *
44
+ * Throws on pop failure so callers can handle conflict cases the same way
45
+ * they would with the prior `git stash pop` form. When throwing after a
46
+ * targeted pop attempt, the error is annotated with the targeted stash ref.
47
+ *
48
+ * (Issue #4980 HIGH-6)
49
+ */
50
+ export function popStashByRef(basePath, stashMarker) {
51
+ let popArg = null;
52
+ if (stashMarker) {
53
+ try {
54
+ const list = execFileSync("git", ["stash", "list", "--format=%gd%x00%s"], {
55
+ cwd: basePath,
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ encoding: "utf-8",
58
+ }).trim().split("\n").filter(Boolean);
59
+ for (const entry of list) {
60
+ const [ref, subject] = entry.split("\0");
61
+ if (ref && subject?.includes(stashMarker)) {
62
+ popArg = ref;
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ catch (err) {
68
+ logWarning("worktree", `stash list lookup failed; leaving stash untouched: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ }
71
+ if (!popArg) {
72
+ logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic pop");
73
+ return null;
74
+ }
75
+ try {
76
+ execFileSync("git", ["stash", "pop", popArg], {
77
+ cwd: basePath,
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ encoding: "utf-8",
80
+ });
81
+ }
82
+ catch (err) {
83
+ if (err && typeof err === "object") {
84
+ err.stashRef = popArg;
85
+ }
86
+ throw err;
87
+ }
88
+ return popArg;
89
+ }
90
+ /**
91
+ * Extract a stash ref annotation injected by popStashByRef() when git stash
92
+ * pop fails and we need to conditionally drop the exact stash entry later.
93
+ */
94
+ export function stashRefFromError(err) {
95
+ if (!err || typeof err !== "object")
96
+ return null;
97
+ const stashRef = err.stashRef;
98
+ return typeof stashRef === "string" && stashRef.length > 0 ? stashRef : null;
99
+ }
100
+ export function stashAlreadyExistsFilesFromError(err) {
101
+ if (!err || typeof err !== "object")
102
+ return [];
103
+ const stderr = err.stderr;
104
+ const stderrText = typeof stderr === "string"
105
+ ? stderr
106
+ : stderr instanceof Uint8Array
107
+ ? Buffer.from(stderr).toString("utf-8")
108
+ : "";
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ const text = `${stderrText}\n${message}`;
111
+ const files = new Set();
112
+ for (const line of text.split("\n")) {
113
+ const m = line.match(/^(.*?)\s+already exists, no checkout\s*$/i);
114
+ if (!m)
115
+ continue;
116
+ const filePath = m[1]?.trim();
117
+ if (filePath)
118
+ files.add(filePath);
119
+ }
120
+ return [...files];
121
+ }
122
+ /**
123
+ * Detect whether an on-disk file still contains unresolved merge conflict
124
+ * markers from a failed stash-pop or merge attempt.
125
+ *
126
+ * Returns false when the file cannot be read.
127
+ */
128
+ export function hasConflictMarkers(filePath) {
129
+ try {
130
+ const content = readFileSync(filePath, "utf-8");
131
+ return content.includes("<<<<<<<") && content.includes("=======") && content.includes(">>>>>>>");
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ export function gsdJsonlFilesWithConflictMarkers(basePath) {
138
+ return nativeLsFiles(basePath, ".gsd/*.jsonl").filter((f) => hasConflictMarkers(join(basePath, f)));
139
+ }
140
+ export function removeMergeStateFiles(basePath, contextLabel) {
141
+ try {
142
+ for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_MODE", "MERGE_HEAD", "AUTO_MERGE"]) {
143
+ const rawPath = execFileSync("git", ["rev-parse", "--git-path", f], {
144
+ cwd: basePath,
145
+ stdio: ["ignore", "pipe", "pipe"],
146
+ encoding: "utf-8",
147
+ }).trim();
148
+ const p = rawPath.length > 0
149
+ ? (isAbsolute(rawPath) ? rawPath : resolve(basePath, rawPath))
150
+ : join(resolveGitDir(basePath), f);
151
+ if (existsSync(p))
152
+ unlinkSync(p);
153
+ }
154
+ }
155
+ catch (err) {
156
+ logError("worktree", `${contextLabel} merge state cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
157
+ }
158
+ }
159
+ export function cleanupConflictState(basePath) {
160
+ // Merge conflicts can leave unmerged index entries; merge-abort alone is not
161
+ // enough for squash merges (MERGE_HEAD is never written). Reset the merge
162
+ // index, then remove merge message files that native/libgit2 paths may have
163
+ // created.
164
+ try {
165
+ nativeMergeAbort(basePath);
166
+ }
167
+ catch (err) {
168
+ // MERGE_HEAD absent (squash merge path) — abort is a no-op, which is fine.
169
+ debugLog("conflict-cleanup:merge-abort-skipped", {
170
+ error: err instanceof Error ? err.message : String(err),
171
+ });
172
+ }
173
+ try {
174
+ execFileSync("git", ["reset", "--merge"], {
175
+ cwd: basePath,
176
+ stdio: ["ignore", "pipe", "pipe"],
177
+ encoding: "utf-8",
178
+ });
179
+ }
180
+ catch (err) {
181
+ logError("worktree", `git reset --merge failed after merge conflict: ${err instanceof Error ? err.message : String(err)}`);
182
+ }
183
+ removeMergeStateFiles(basePath, "conflict");
184
+ }
185
+ export function checkoutBranchWithStashGuard(basePath, branch, reason) {
186
+ let stashMarker = null;
187
+ let stashed = false;
188
+ const status = nativeWorkingTreeStatus(basePath).trim();
189
+ if (status.length > 0) {
190
+ stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
191
+ const stashListBefore = execFileSync("git", ["stash", "list"], {
192
+ cwd: basePath,
193
+ stdio: ["ignore", "pipe", "pipe"],
194
+ encoding: "utf-8",
195
+ });
196
+ execFileSync("git", ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`], {
197
+ cwd: basePath,
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ encoding: "utf-8",
200
+ });
201
+ const stashListAfter = execFileSync("git", ["stash", "list"], {
202
+ cwd: basePath,
203
+ stdio: ["ignore", "pipe", "pipe"],
204
+ encoding: "utf-8",
205
+ });
206
+ stashed = stashListAfter !== stashListBefore;
207
+ }
208
+ // Checkout and stash-restore are split so we can distinguish two failure
209
+ // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
210
+ // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
211
+ // the working-tree changes remain in the stash list. We surface a distinct
212
+ // error in case (b) so callers don't assume the branch switch was rolled back.
213
+ try {
214
+ nativeCheckoutBranch(basePath, branch);
215
+ }
216
+ catch (checkoutErr) {
217
+ if (stashed) {
218
+ try {
219
+ popStashByRef(basePath, stashMarker);
220
+ }
221
+ catch (restoreErr) {
222
+ logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
223
+ }
224
+ }
225
+ throw checkoutErr;
226
+ }
227
+ if (stashed) {
228
+ try {
229
+ popStashByRef(basePath, stashMarker);
230
+ }
231
+ catch (popErr) {
232
+ const msg = popErr instanceof Error ? popErr.message : String(popErr);
233
+ const stderr = popErr && typeof popErr === "object"
234
+ ? popErr.stderr
235
+ : undefined;
236
+ const stderrText = typeof stderr === "string"
237
+ ? stderr
238
+ : stderr instanceof Uint8Array
239
+ ? Buffer.from(stderr).toString("utf-8")
240
+ : "";
241
+ const stashPopMessage = `${stderrText}\n${msg}`.trim();
242
+ const alreadyExists = stashAlreadyExistsFilesFromError(popErr);
243
+ const gsdAlreadyExists = alreadyExists.filter((f) => f.startsWith(".gsd/"));
244
+ const nonGsdAlreadyExists = alreadyExists.filter((f) => !f.startsWith(".gsd/"));
245
+ const isUntrackedRestoreFailure = stashPopMessage.includes("could not restore untracked files from stash");
246
+ const stashRefForDrop = stashRefFromError(popErr);
247
+ const nonGsdUnmerged = nativeConflictFiles(basePath).filter((f) => !f.startsWith(".gsd/"));
248
+ const gsdContentConflicts = isUntrackedRestoreFailure
249
+ ? gsdJsonlFilesWithConflictMarkers(basePath)
250
+ : [];
251
+ const gsdConflictFiles = [...new Set([...gsdAlreadyExists, ...gsdContentConflicts])];
252
+ if (isUntrackedRestoreFailure &&
253
+ gsdConflictFiles.length > 0 &&
254
+ nonGsdAlreadyExists.length === 0 &&
255
+ nonGsdUnmerged.length === 0) {
256
+ for (const f of gsdConflictFiles) {
257
+ execFileSync("git", ["checkout", "HEAD", "--", f], {
258
+ cwd: basePath,
259
+ stdio: ["ignore", "pipe", "pipe"],
260
+ encoding: "utf-8",
261
+ });
262
+ nativeAddPaths(basePath, [f]);
263
+ }
264
+ if (stashRefForDrop) {
265
+ try {
266
+ execFileSync("git", ["stash", "drop", stashRefForDrop], {
267
+ cwd: basePath,
268
+ stdio: ["ignore", "pipe", "pipe"],
269
+ encoding: "utf-8",
270
+ });
271
+ }
272
+ catch (err) { /* stash may already be consumed */
273
+ logWarning("worktree", `git stash drop failed: ${err instanceof Error ? err.message : String(err)}`);
274
+ }
275
+ }
276
+ else {
277
+ logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic drop");
278
+ }
279
+ return;
280
+ }
281
+ const wrapped = new Error(`checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`);
282
+ if (stashRefForDrop)
283
+ wrapped.stashRef = stashRefForDrop;
284
+ throw wrapped;
285
+ }
286
+ }
287
+ }
@@ -327,7 +327,15 @@ export function _enterMilestoneCore(s, deps, milestoneId, ctx, opts = {}) {
327
327
  // Handles the case where originalBasePath is falsy and basePath is itself
328
328
  // a worktree path — prevents double-nested worktree paths (#3729).
329
329
  const basePath = resolveWorktreeProjectRoot(s.basePath, s.originalBasePath);
330
- const mode = opts.modeOverride ?? getIsolationMode(basePath);
330
+ // A stranded-recovery session that adopted the milestone branch in the
331
+ // project root must keep re-entering in that mode: the root checkout holds
332
+ // the branch, so creating the canonical worktree would fail with "already
333
+ // in use by another worktree". The override clears when the recovered
334
+ // milestone merges (_mergeAndExit), restoring configured isolation for
335
+ // subsequent milestones.
336
+ const mode = opts.modeOverride ??
337
+ s.strandedRecoveryIsolationMode ??
338
+ getIsolationMode(basePath);
331
339
  if (s.isolationDegraded) {
332
340
  if (mode === "worktree") {
333
341
  try {