@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
@@ -20,7 +20,7 @@ import {
20
20
  unlinkSync,
21
21
  lstatSync as lstatSyncFn,
22
22
  } from "node:fs";
23
- import { dirname, isAbsolute, join, relative, resolve, sep as pathSep } from "node:path";
23
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
24
24
  import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
25
25
  import {
26
26
  reconcileWorktreeDb,
@@ -50,6 +50,7 @@ import {
50
50
  nudgeGitBranchCache,
51
51
  } from "./worktree.js";
52
52
  import {
53
+ findWorktreeSegment,
53
54
  isGsdWorktreePath,
54
55
  normalizeWorktreePathForCompare,
55
56
  resolveWorktreeProjectRoot,
@@ -62,6 +63,19 @@ import {
62
63
  } from "./pull-request-process.js";
63
64
  import { debugLog } from "./debug-logger.js";
64
65
  import { logWarning, logError } from "./workflow-logger.js";
66
+ import {
67
+ checkoutBranchWithStashGuard,
68
+ cleanupConflictState,
69
+ gsdJsonlFilesWithConflictMarkers,
70
+ hasConflictMarkers,
71
+ popStashByRef,
72
+ removeMergeStateFiles,
73
+ stashAlreadyExistsFilesFromError,
74
+ stashRefFromError,
75
+ } from "./worktree-git-recovery.js";
76
+
77
+ // Re-export for existing callers/tests (auto-start.ts, checkout-branch-stash-guard.test.ts).
78
+ export { checkoutBranchWithStashGuard } from "./worktree-git-recovery.js";
65
79
  import { loadEffectiveGSDPreferences } from "./preferences.js";
66
80
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
67
81
  import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
@@ -85,7 +99,6 @@ import {
85
99
  nativeDiffNumstat,
86
100
  nativeUpdateRef,
87
101
  nativeIsAncestor,
88
- nativeMergeAbort,
89
102
  nativeWorktreeList,
90
103
  nativeLsFiles,
91
104
  } from "./native-git-bridge.js";
@@ -132,111 +145,6 @@ const ROOT_STATE_FILES = [
132
145
  // because the project root is authoritative for preferences (#2684).
133
146
  ] as const;
134
147
 
135
- /**
136
- * Pop a stash entry by tracking the unique marker embedded in its message so
137
- * concurrent stash operations against the same project root cannot cause us to
138
- * pop the wrong entry.
139
- *
140
- * If `stashMarker` is null or no longer present in the stash list (e.g. a
141
- * concurrent process popped/dropped it), leaves the stash list untouched and
142
- * returns null.
143
- *
144
- * Throws on pop failure so callers can handle conflict cases the same way
145
- * they would with the prior `git stash pop` form. When throwing after a
146
- * targeted pop attempt, the error is annotated with the targeted stash ref.
147
- *
148
- * (Issue #4980 HIGH-6)
149
- */
150
- function popStashByRef(basePath: string, stashMarker: string | null): string | null {
151
- let popArg: string | null = null;
152
- if (stashMarker) {
153
- try {
154
- const list = execFileSync("git", ["stash", "list", "--format=%gd%x00%s"], {
155
- cwd: basePath,
156
- stdio: ["ignore", "pipe", "pipe"],
157
- encoding: "utf-8",
158
- }).trim().split("\n").filter(Boolean);
159
- for (const entry of list) {
160
- const [ref, subject] = entry.split("\0");
161
- if (ref && subject?.includes(stashMarker)) {
162
- popArg = ref;
163
- break;
164
- }
165
- }
166
- } catch (err) {
167
- logWarning("worktree", `stash list lookup failed; leaving stash untouched: ${err instanceof Error ? err.message : String(err)}`);
168
- }
169
- }
170
- if (!popArg) {
171
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic pop");
172
- return null;
173
- }
174
- try {
175
- execFileSync("git", ["stash", "pop", popArg], {
176
- cwd: basePath,
177
- stdio: ["ignore", "pipe", "pipe"],
178
- encoding: "utf-8",
179
- });
180
- } catch (err) {
181
- if (err && typeof err === "object") {
182
- (err as { stashRef?: string }).stashRef = popArg;
183
- }
184
- throw err;
185
- }
186
- return popArg;
187
- }
188
-
189
- /**
190
- * Extract a stash ref annotation injected by popStashByRef() when git stash
191
- * pop fails and we need to conditionally drop the exact stash entry later.
192
- */
193
- function stashRefFromError(err: unknown): string | null {
194
- if (!err || typeof err !== "object") return null;
195
- const stashRef = (err as { stashRef?: unknown }).stashRef;
196
- return typeof stashRef === "string" && stashRef.length > 0 ? stashRef : null;
197
- }
198
-
199
- function stashAlreadyExistsFilesFromError(err: unknown): string[] {
200
- if (!err || typeof err !== "object") return [];
201
- const stderr = (err as { stderr?: unknown }).stderr;
202
- const stderrText = typeof stderr === "string"
203
- ? stderr
204
- : stderr instanceof Uint8Array
205
- ? Buffer.from(stderr).toString("utf-8")
206
- : "";
207
- const message = err instanceof Error ? err.message : String(err);
208
- const text = `${stderrText}\n${message}`;
209
- const files = new Set<string>();
210
- for (const line of text.split("\n")) {
211
- const m = line.match(/^(.*?)\s+already exists, no checkout\s*$/i);
212
- if (!m) continue;
213
- const filePath = m[1]?.trim();
214
- if (filePath) files.add(filePath);
215
- }
216
- return [...files];
217
- }
218
-
219
- /**
220
- * Detect whether an on-disk file still contains unresolved merge conflict
221
- * markers from a failed stash-pop or merge attempt.
222
- *
223
- * Returns false when the file cannot be read.
224
- */
225
- function hasConflictMarkers(filePath: string): boolean {
226
- try {
227
- const content = readFileSync(filePath, "utf-8");
228
- return content.includes("<<<<<<<") && content.includes("=======") && content.includes(">>>>>>>");
229
- } catch {
230
- return false;
231
- }
232
- }
233
-
234
- function gsdJsonlFilesWithConflictMarkers(basePath: string): string[] {
235
- return nativeLsFiles(basePath, ".gsd/*.jsonl").filter((f) =>
236
- hasConflictMarkers(join(basePath, f)),
237
- );
238
- }
239
-
240
148
  /**
241
149
  * Check if two filesystem paths resolve to the same real location.
242
150
  * Returns false if either path cannot be resolved (e.g. doesn't exist).
@@ -524,49 +432,6 @@ export const isSafeToAutoResolve = (filePath: string): boolean =>
524
432
  filePath.startsWith(".gsd/") ||
525
433
  SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
526
434
 
527
- function removeMergeStateFiles(basePath: string, contextLabel: string): void {
528
- try {
529
- for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_MODE", "MERGE_HEAD", "AUTO_MERGE"]) {
530
- const rawPath = execFileSync("git", ["rev-parse", "--git-path", f], {
531
- cwd: basePath,
532
- stdio: ["ignore", "pipe", "pipe"],
533
- encoding: "utf-8",
534
- }).trim();
535
- const p = rawPath.length > 0
536
- ? (isAbsolute(rawPath) ? rawPath : resolve(basePath, rawPath))
537
- : join(resolveGitDir(basePath), f);
538
- if (existsSync(p)) unlinkSync(p);
539
- }
540
- } catch (err) {
541
- logError("worktree", `${contextLabel} merge state cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
542
- }
543
- }
544
-
545
- function cleanupConflictState(basePath: string): void {
546
- // Merge conflicts can leave unmerged index entries; merge-abort alone is not
547
- // enough for squash merges (MERGE_HEAD is never written). Reset the merge
548
- // index, then remove merge message files that native/libgit2 paths may have
549
- // created.
550
- try {
551
- nativeMergeAbort(basePath);
552
- } catch (err) {
553
- // MERGE_HEAD absent (squash merge path) — abort is a no-op, which is fine.
554
- debugLog("conflict-cleanup:merge-abort-skipped", {
555
- error: err instanceof Error ? err.message : String(err),
556
- });
557
- }
558
- try {
559
- execFileSync("git", ["reset", "--merge"], {
560
- cwd: basePath,
561
- stdio: ["ignore", "pipe", "pipe"],
562
- encoding: "utf-8",
563
- });
564
- } catch (err) {
565
- logError("worktree", `git reset --merge failed after merge conflict: ${err instanceof Error ? err.message : String(err)}`);
566
- }
567
- removeMergeStateFiles(basePath, "conflict");
568
- }
569
-
570
435
  // ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
571
436
 
572
437
  /**
@@ -655,21 +520,12 @@ export function checkResourcesStale(
655
520
  * Returns the corrected base path.
656
521
  */
657
522
  export function escapeStaleWorktree(base: string): string {
658
- // Direct layout: /.gsd/worktrees/
659
- const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
660
- let idx = base.indexOf(directMarker);
661
- if (idx === -1) {
662
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
663
- const symlinkRe = new RegExp(
664
- `\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`,
665
- );
666
- const match = base.match(symlinkRe);
667
- if (!match || match.index === undefined) return base;
668
- idx = match.index;
669
- }
523
+ const segment = findWorktreeSegment(base.replaceAll("\\", "/"));
524
+ if (!segment) return base;
670
525
 
671
- // base is inside .gsd/worktrees/<something> — extract the project root
672
- const projectRoot = base.slice(0, idx);
526
+ // base is inside .gsd/worktrees/<something> — extract the project root.
527
+ // Normalization is 1:1 on characters, so the segment index is valid in `base`.
528
+ const projectRoot = base.slice(0, segment.gsdIdx);
673
529
 
674
530
  // Guard: If the candidate project root's .gsd IS the user-level ~/.gsd,
675
531
  // the string-slice heuristic matched the wrong /.gsd/ boundary. This happens
@@ -1066,122 +922,6 @@ export function enterBranchModeForMilestone(
1066
922
  checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
1067
923
  }
1068
924
 
1069
- export function checkoutBranchWithStashGuard(
1070
- basePath: string,
1071
- branch: string,
1072
- reason: string,
1073
- ): void {
1074
- let stashMarker: string | null = null;
1075
- let stashed = false;
1076
-
1077
- const status = nativeWorkingTreeStatus(basePath).trim();
1078
- if (status.length > 0) {
1079
- stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
1080
- const stashListBefore = execFileSync("git", ["stash", "list"], {
1081
- cwd: basePath,
1082
- stdio: ["ignore", "pipe", "pipe"],
1083
- encoding: "utf-8",
1084
- });
1085
- execFileSync(
1086
- "git",
1087
- ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`],
1088
- {
1089
- cwd: basePath,
1090
- stdio: ["ignore", "pipe", "pipe"],
1091
- encoding: "utf-8",
1092
- },
1093
- );
1094
- const stashListAfter = execFileSync("git", ["stash", "list"], {
1095
- cwd: basePath,
1096
- stdio: ["ignore", "pipe", "pipe"],
1097
- encoding: "utf-8",
1098
- });
1099
- stashed = stashListAfter !== stashListBefore;
1100
- }
1101
-
1102
- // Checkout and stash-restore are split so we can distinguish two failure
1103
- // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
1104
- // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
1105
- // the working-tree changes remain in the stash list. We surface a distinct
1106
- // error in case (b) so callers don't assume the branch switch was rolled back.
1107
- try {
1108
- nativeCheckoutBranch(basePath, branch);
1109
- } catch (checkoutErr) {
1110
- if (stashed) {
1111
- try {
1112
- popStashByRef(basePath, stashMarker);
1113
- } catch (restoreErr) {
1114
- logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
1115
- }
1116
- }
1117
- throw checkoutErr;
1118
- }
1119
-
1120
- if (stashed) {
1121
- try {
1122
- popStashByRef(basePath, stashMarker);
1123
- } catch (popErr) {
1124
- const msg = popErr instanceof Error ? popErr.message : String(popErr);
1125
- const stderr = popErr && typeof popErr === "object"
1126
- ? (popErr as { stderr?: unknown }).stderr
1127
- : undefined;
1128
- const stderrText = typeof stderr === "string"
1129
- ? stderr
1130
- : stderr instanceof Uint8Array
1131
- ? Buffer.from(stderr).toString("utf-8")
1132
- : "";
1133
- const stashPopMessage = `${stderrText}\n${msg}`.trim();
1134
- const alreadyExists = stashAlreadyExistsFilesFromError(popErr);
1135
- const gsdAlreadyExists = alreadyExists.filter((f) => f.startsWith(".gsd/"));
1136
- const nonGsdAlreadyExists = alreadyExists.filter((f) => !f.startsWith(".gsd/"));
1137
- const isUntrackedRestoreFailure = stashPopMessage.includes("could not restore untracked files from stash");
1138
- const stashRefForDrop = stashRefFromError(popErr);
1139
- const nonGsdUnmerged = nativeConflictFiles(basePath).filter((f) => !f.startsWith(".gsd/"));
1140
- const gsdContentConflicts = isUntrackedRestoreFailure
1141
- ? gsdJsonlFilesWithConflictMarkers(basePath)
1142
- : [];
1143
- const gsdConflictFiles = [...new Set([...gsdAlreadyExists, ...gsdContentConflicts])];
1144
-
1145
- if (
1146
- isUntrackedRestoreFailure &&
1147
- gsdConflictFiles.length > 0 &&
1148
- nonGsdAlreadyExists.length === 0 &&
1149
- nonGsdUnmerged.length === 0
1150
- ) {
1151
- for (const f of gsdConflictFiles) {
1152
- execFileSync("git", ["checkout", "HEAD", "--", f], {
1153
- cwd: basePath,
1154
- stdio: ["ignore", "pipe", "pipe"],
1155
- encoding: "utf-8",
1156
- });
1157
- nativeAddPaths(basePath, [f]);
1158
- }
1159
-
1160
- if (stashRefForDrop) {
1161
- try {
1162
- execFileSync("git", ["stash", "drop", stashRefForDrop], {
1163
- cwd: basePath,
1164
- stdio: ["ignore", "pipe", "pipe"],
1165
- encoding: "utf-8",
1166
- });
1167
- } catch (err) { /* stash may already be consumed */
1168
- logWarning("worktree", `git stash drop failed: ${err instanceof Error ? err.message : String(err)}`);
1169
- }
1170
- } else {
1171
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic drop");
1172
- }
1173
- return;
1174
- }
1175
-
1176
- const wrapped = new Error(
1177
- `checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`,
1178
- );
1179
- if (stashRefForDrop) (wrapped as { stashRef?: string }).stashRef = stashRefForDrop;
1180
- throw wrapped;
1181
- }
1182
- }
1183
- }
1184
-
1185
925
  // ─── Public API ────────────────────────────────────────────────────────────
1186
926
 
1187
927
  /**
@@ -81,6 +81,7 @@ import {
81
81
  resolveAutoSupervisorConfig,
82
82
  loadEffectiveGSDPreferences,
83
83
  getIsolationMode,
84
+ resolveEffectiveUnitIsolationMode,
84
85
  } from "./preferences.js";
85
86
  import { playNotificationBell, sendDesktopNotification } from "./notifications.js";
86
87
  import type { GSDPreferences } from "./preferences.js";
@@ -634,22 +635,24 @@ export function shouldUseWorktreeIsolation(basePath?: string): boolean {
634
635
 
635
636
  type AutoIsolationMode = ReturnType<typeof getIsolationMode>;
636
637
 
637
- function resolveEffectiveUnitIsolationMode(
638
- configuredMode: AutoIsolationMode,
639
- isolationDegraded: boolean,
640
- ): AutoIsolationMode {
641
- return configuredMode === "worktree" && isolationDegraded ? "branch" : configuredMode;
642
- }
643
-
644
638
  export function _resolveEffectiveUnitIsolationModeForTest(
645
639
  configuredMode: AutoIsolationMode,
646
640
  isolationDegraded: boolean,
641
+ strandedRecoveryIsolationMode: "worktree" | "branch" | null = null,
647
642
  ): AutoIsolationMode {
648
- return resolveEffectiveUnitIsolationMode(configuredMode, isolationDegraded);
643
+ return resolveEffectiveUnitIsolationMode(
644
+ configuredMode,
645
+ isolationDegraded,
646
+ strandedRecoveryIsolationMode,
647
+ );
649
648
  }
650
649
 
651
650
  function getEffectiveUnitIsolationMode(basePath: string): AutoIsolationMode {
652
- return resolveEffectiveUnitIsolationMode(getIsolationMode(basePath), s.isolationDegraded);
651
+ return resolveEffectiveUnitIsolationMode(
652
+ getIsolationMode(basePath),
653
+ s.isolationDegraded,
654
+ s.strandedRecoveryIsolationMode,
655
+ );
653
656
  }
654
657
 
655
658
  /** Crash recovery prompt — set by startAuto, consumed by the main loop */
@@ -12,6 +12,7 @@ import { logWarning } from "../workflow-logger.js";
12
12
  import { openWorkflowDatabase } from "../db-workspace.js";
13
13
  import { getAutoWorktreePath } from "../auto-worktree.js";
14
14
  import { resolveWorktreeProjectRoot } from "../worktree-root.js";
15
+ import { worktreesDirs } from "../worktree-placement.js";
15
16
 
16
17
  export function safeWorkspaceCwd(): string {
17
18
  try {
@@ -46,13 +47,15 @@ export function resolveWorkflowToolBasePath(
46
47
  const worktree = getAutoWorktreePath(projectRoot, milestoneId);
47
48
  if (worktree) return worktree;
48
49
  } else {
49
- const worktreesDir = join(projectRoot, ".gsd", "worktrees");
50
- if (existsSync(worktreesDir)) {
50
+ const live: string[] = [];
51
+ for (const worktreesDir of worktreesDirs(projectRoot)) {
52
+ if (!existsSync(worktreesDir)) continue;
51
53
  try {
52
- const live = readdirSync(worktreesDir)
53
- .map((name) => join(worktreesDir, name))
54
- .filter((p) => existsSync(join(p, ".git")));
55
- if (live.length === 1) return live[0]!;
54
+ live.push(
55
+ ...readdirSync(worktreesDir)
56
+ .map((name) => join(worktreesDir, name))
57
+ .filter((p) => existsSync(join(p, ".git"))),
58
+ );
56
59
  } catch (err) {
57
60
  logWarning(
58
61
  "bootstrap",
@@ -60,6 +63,7 @@ export function resolveWorkflowToolBasePath(
60
63
  );
61
64
  }
62
65
  }
66
+ if (live.length === 1) return live[0]!;
63
67
  }
64
68
  return cwd;
65
69
  }
@@ -13,7 +13,7 @@ import type { GSDEcosystemBeforeAgentStartHandler } from "../ecosystem/gsd-exten
13
13
  import { updateSnapshot } from "../ecosystem/gsd-extension-api.js";
14
14
 
15
15
  import { buildMilestoneFileName, clearPathCache, milestonesDir, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
16
- import { applyAskUserQuestionsGateResult, canonicalToolName, clearDiscussionFlowState, formatPendingAskUserQuestionsGateMessage, isMilestoneDepthVerified, isQueuePhaseActive, markApprovalGateVerified, markDepthVerified, resetWriteGateState, shouldBlockContextWrite, shouldBlockPlanningUnit, shouldBlockQueueExecution, shouldBlockWorktreeWrite, isGateQuestionId, setPendingGate, clearPendingGate, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash, extractDepthVerificationMilestoneId } from "./write-gate.js";
16
+ import { applyAskUserQuestionsGateResult, canonicalToolName, clearDiscussionFlowState, formatPendingAskUserQuestionsGateMessage, isApprovalGateVerifiedInSnapshot, isMilestoneDepthVerified, isMilestoneDepthVerifiedInSnapshot, isQueuePhaseActive, loadWriteGateSnapshot, markApprovalGateVerified, markDepthVerified, refreshWriteGateStateFromDisk, resetWriteGateState, shouldBlockContextWrite, shouldBlockPlanningUnit, shouldBlockQueueExecution, shouldBlockWorktreeWrite, isGateQuestionId, setPendingGate, clearPendingGate, getPendingGate, shouldBlockPendingGate, shouldBlockPendingGateBash, extractDepthVerificationMilestoneId } from "./write-gate.js";
17
17
  import { resolveManifest } from "../unit-context-manifest.js";
18
18
  import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
19
19
  import { loadFile, saveFile, formatContinue } from "../files.js";
@@ -61,6 +61,7 @@ import { filterToolsForProvider } from "../model-router.js";
61
61
  import { mcpToolMatchesBaseName } from "../mcp-tool-name.js";
62
62
  import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
63
63
  import { supportsSourceObservationsForUnit } from "../source-observations.js";
64
+ import { clearPendingAutoStart } from "../pending-auto-start.js";
64
65
 
65
66
  let approvalQuestionAbortInFlight = false;
66
67
 
@@ -572,8 +573,14 @@ function isShellExecutionTool(canonicalName: string): boolean {
572
573
 
573
574
  function activateDeferredApprovalGate(basePath: string): void {
574
575
  if (deferredApprovalGate?.basePath !== basePath) return;
575
- setPendingGate(deferredApprovalGate.gateId, basePath);
576
+ const gateId = deferredApprovalGate.gateId;
576
577
  deferredApprovalGate = null;
578
+ refreshWriteGateStateFromDisk(basePath);
579
+ const snapshot = loadWriteGateSnapshot(basePath);
580
+ const milestoneId = extractDepthVerificationMilestoneId(gateId);
581
+ if (isApprovalGateVerifiedInSnapshot(snapshot, gateId)) return;
582
+ if (milestoneId && isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId)) return;
583
+ setPendingGate(gateId, basePath);
577
584
  }
578
585
 
579
586
  function extractGateQuestionId(input: unknown): string | undefined {
@@ -802,7 +809,7 @@ export function registerHooks(
802
809
  }
803
810
  });
804
811
 
805
- pi.on("session_switch", async (_event, ctx) => {
812
+ pi.on("session_switch", async (event, ctx) => {
806
813
  const basePath = contextBasePath(ctx);
807
814
  const preserveCloseoutSurface = isAutoCompletionStopInProgress();
808
815
  initSessionNotifications(ctx);
@@ -811,6 +818,13 @@ export function registerHooks(
811
818
  clearDeferredApprovalGate();
812
819
  await resetAskUserQuestionsTurnCache();
813
820
  clearDiscussionFlowState(basePath);
821
+ // /clear or /new destroys the conversation holding a discuss interview, so
822
+ // its pending discuss→auto handoff can never be answered — clear it. Resume
823
+ // restores the interview transcript, so the entry survives. Auto-mode's own
824
+ // newSession() calls are safe: the handoff consumes the entry on agent_end.
825
+ if (event.reason === "new") {
826
+ clearPendingAutoStart(basePath);
827
+ }
814
828
  await syncServiceTierStatus(ctx);
815
829
  await applyDisabledModelProviderPolicy(ctx);
816
830
  await applyCompactionThresholdOverride(ctx);
@@ -1419,6 +1433,21 @@ export function registerHooks(
1419
1433
  clearDeferredApprovalGate(basePath);
1420
1434
  }
1421
1435
  }
1436
+
1437
+ // Safety harness: record evidence here, not only in tool_call. External
1438
+ // engines (claude-code-cli) pre-execute tools, so the agent loop skips
1439
+ // beforeToolCall/tool_call for them — tool_execution_start is the only
1440
+ // event that fires for every tool call. recordToolCall dedupes by
1441
+ // toolCallId, so native tools (which hit both events) record once.
1442
+ safetyRecordToolCall(event.toolCallId, event.toolName, (event.args ?? {}) as Record<string, unknown>);
1443
+ const execDash = getAutoRuntimeSnapshot();
1444
+ if (execDash.basePath && execDash.currentUnit?.type === "execute-task") {
1445
+ const { milestone: xMid, slice: xSid, task: xTid } = parseUnitId(execDash.currentUnit.id);
1446
+ if (xMid && xSid && xTid) {
1447
+ saveEvidenceToDisk(execDash.basePath, xMid, xSid, xTid);
1448
+ }
1449
+ }
1450
+
1422
1451
  if (!isAutoActive()) return;
1423
1452
  markToolStart(event.toolCallId, event.toolName);
1424
1453
  });
@@ -10,6 +10,7 @@ import { getIsolationMode } from "../preferences.js";
10
10
  import { compileSubagentPermissionContract, type ToolsPolicy } from "../unit-context-manifest.js";
11
11
  import { logWarning } from "../workflow-logger.js";
12
12
  import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "../worktree-root.js";
13
+ import { worktreesDirs } from "../worktree-placement.js";
13
14
 
14
15
  /**
15
16
  * Regex matching milestone CONTEXT.md file names in both legacy M001
@@ -244,6 +245,23 @@ export function loadWriteGateSnapshot(basePath: string): WriteGateSnapshot {
244
245
  }
245
246
  }
246
247
 
248
+ /**
249
+ * Merge the persisted write-gate snapshot into the in-process Map entry.
250
+ * The workflow MCP server runs in a child process and records depth
251
+ * verification there; without this refresh the extension host keeps stale
252
+ * pending-gate memory and `activateDeferredApprovalGate` can re-arm a gate
253
+ * that the subprocess already cleared on disk.
254
+ */
255
+ export function refreshWriteGateStateFromDisk(basePath: string): void {
256
+ if (!shouldPersistWriteGateSnapshot()) return;
257
+ const snapshot = loadWriteGateSnapshot(basePath);
258
+ const state = getWriteGateState(basePath);
259
+ state.pendingGateId = snapshot.pendingGateId;
260
+ state.activeQueuePhase = snapshot.activeQueuePhase;
261
+ state.verifiedDepthMilestones = new Set(snapshot.verifiedDepthMilestones);
262
+ state.verifiedApprovalGates = new Set(snapshot.verifiedApprovalGates ?? []);
263
+ }
264
+
247
265
  export function isDepthVerified(basePath: string = process.cwd()): boolean {
248
266
  return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
249
267
  }
@@ -256,6 +274,7 @@ export function isMilestoneDepthVerified(
256
274
  basePath: string = process.cwd(),
257
275
  ): boolean {
258
276
  if (!milestoneId) return false;
277
+ refreshWriteGateStateFromDisk(basePath);
259
278
  return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
260
279
  }
261
280
 
@@ -357,6 +376,7 @@ export function clearPendingGate(basePath: string): void {
357
376
  * Get the currently pending gate, if any.
358
377
  */
359
378
  export function getPendingGate(basePath: string = process.cwd()): string | null {
379
+ refreshWriteGateStateFromDisk(basePath);
360
380
  return getWriteGateState(basePath).pendingGateId;
361
381
  }
362
382
 
@@ -1138,10 +1158,12 @@ export function shouldBlockWorktreeWrite(
1138
1158
  const realTarget = realpathOrResolve(absTarget);
1139
1159
  const realRoot = realpathOrResolve(projectRoot);
1140
1160
  const realGsd = realpathOrResolve(join(projectRoot, ".gsd"));
1141
- const realWorktreesDir = realpathOrResolve(join(projectRoot, ".gsd", "worktrees"));
1142
1161
 
1143
- // Allow writes inside the legitimate worktrees subtree.
1144
- if (isPathContained(realTarget, realWorktreesDir)) return { block: false };
1162
+ // Allow writes inside a legitimate worktrees subtree (canonical
1163
+ // .gsd-worktrees/ or legacy .gsd/worktrees/).
1164
+ for (const container of worktreesDirs(projectRoot)) {
1165
+ if (isPathContained(realTarget, realpathOrResolve(container))) return { block: false };
1166
+ }
1145
1167
 
1146
1168
  // Allow writes to .gsd/ planning artifacts, but reject siblings whose name
1147
1169
  // starts with "worktrees" (the worktrees-extra prefix trick — case 4).
@@ -9,9 +9,10 @@
9
9
  */
10
10
 
11
11
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
12
- import { join, resolve, sep } from "node:path";
12
+ import { join, resolve } from "node:path";
13
13
  import { randomUUID } from "node:crypto";
14
14
  import { gsdRoot } from "./paths.js";
15
+ import { findWorktreeSegment } from "./worktree-root.js";
15
16
 
16
17
  // ─── Types ────────────────────────────────────────────────────────────────────
17
18
 
@@ -60,20 +61,10 @@ const VALID_CLASSIFICATIONS: readonly string[] = [
60
61
  */
61
62
  export function resolveCapturesPath(basePath: string): string {
62
63
  const resolved = resolve(basePath);
63
- // Direct layout: /.gsd/worktrees/
64
- const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
65
- let idx = resolved.indexOf(worktreeMarker);
66
- if (idx === -1) {
67
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
68
- const symlinkRe = new RegExp(
69
- `\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`,
70
- );
71
- const match = resolved.match(symlinkRe);
72
- if (match && match.index !== undefined) idx = match.index;
73
- }
74
- if (idx !== -1) {
64
+ const segment = findWorktreeSegment(resolved.replaceAll("\\", "/"));
65
+ if (segment) {
75
66
  // basePath is inside a worktree — resolve to project root
76
- const projectRoot = resolved.slice(0, idx);
67
+ const projectRoot = resolved.slice(0, segment.gsdIdx);
77
68
  return join(projectRoot, ".gsd", CAPTURES_FILENAME);
78
69
  }
79
70
  return join(gsdRoot(basePath), CAPTURES_FILENAME);
@@ -10,6 +10,7 @@ import { runTurnGitAction, type TurnGitActionMode, type TurnGitActionResult } fr
10
10
  import { _getAdapter, upsertTurnGitTransaction } from "./gsd-db.js";
11
11
  import { probeGitConflictState } from "./git-conflict-state.js";
12
12
  import { parseUnitId } from "./unit-id.js";
13
+ import { worktreePathFor } from "./worktree-placement.js";
13
14
 
14
15
  export interface CloseoutFailureRecord {
15
16
  traceId: string;
@@ -156,7 +157,7 @@ export function resolveCloseoutRecoveryBasePath(projectRoot: string, record: Clo
156
157
  const parsed = parseUnitId(record.unitId);
157
158
  const milestoneId = parsed.milestone ?? (/^M\d+(?:-[a-z0-9]{6})?/.exec(record.unitId)?.[0] ?? "");
158
159
  if (milestoneId) {
159
- const worktreePath = existingRealPath(join(projectRoot, ".gsd", "worktrees", milestoneId));
160
+ const worktreePath = existingRealPath(worktreePathFor(projectRoot, milestoneId));
160
161
  if (worktreePath) return worktreePath;
161
162
  }
162
163