@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
@@ -8,25 +8,28 @@
8
8
  * manages create, enter, detect, and teardown for auto-mode worktrees.
9
9
  */
10
10
  import { existsSync, cpSync, readFileSync, readdirSync, mkdirSync, realpathSync, rmSync, unlinkSync, lstatSync as lstatSyncFn, } from "node:fs";
11
- import { dirname, isAbsolute, join, relative, resolve, sep as pathSep } from "node:path";
11
+ import { dirname, isAbsolute, join, relative } from "node:path";
12
12
  import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
13
13
  import { reconcileWorktreeDb, isDbAvailable, getMilestone, getMilestoneSlices, getSliceTasks, } from "./gsd-db.js";
14
14
  import { closeWorkflowDatabase, getWorkflowDatabasePath, openWorkflowDatabasePath, } from "./db-workspace.js";
15
15
  import { execFileSync } from "node:child_process";
16
16
  import { gsdRoot, resolveGsdPathContract } from "./paths.js";
17
- import { createWorktree, removeWorktree, resolveGitDir, worktreePath, isInsideWorktreesDir, } from "./worktree-manager.js";
17
+ import { createWorktree, removeWorktree, worktreePath, isInsideWorktreesDir, } from "./worktree-manager.js";
18
18
  import { detectWorktreeName, nudgeGitBranchCache, } from "./worktree.js";
19
- import { isGsdWorktreePath, normalizeWorktreePathForCompare, resolveWorktreeProjectRoot, } from "./worktree-root.js";
19
+ import { findWorktreeSegment, isGsdWorktreePath, normalizeWorktreePathForCompare, resolveWorktreeProjectRoot, } from "./worktree-root.js";
20
20
  import { autoResolveSafeConflictPaths } from "./git-conflict-resolve.js";
21
21
  import { MergeConflictError, readIntegrationBranch, resolveMilestoneIntegrationBranch, RUNTIME_EXCLUSION_PATHS } from "./git-service.js";
22
22
  import { buildPullRequestEvidence, createDraftPullRequestFromEvidence, } from "./pull-request-process.js";
23
23
  import { debugLog } from "./debug-logger.js";
24
24
  import { logWarning, logError } from "./workflow-logger.js";
25
+ import { checkoutBranchWithStashGuard, cleanupConflictState, gsdJsonlFilesWithConflictMarkers, hasConflictMarkers, popStashByRef, removeMergeStateFiles, stashAlreadyExistsFilesFromError, stashRefFromError, } from "./worktree-git-recovery.js";
26
+ // Re-export for existing callers/tests (auto-start.ts, checkout-branch-stash-guard.test.ts).
27
+ export { checkoutBranchWithStashGuard } from "./worktree-git-recovery.js";
25
28
  import { loadEffectiveGSDPreferences } from "./preferences.js";
26
29
  import { MILESTONE_ID_RE } from "./milestone-ids.js";
27
30
  import { runWorktreePostCreateHook } from "./worktree-post-create-hook.js";
28
31
  import { classifyProject } from "./detection.js";
29
- import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeMergeRegular, nativeConflictFiles, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchForceReset, nativeBranchExists, nativeDiffNumstat, nativeUpdateRef, nativeIsAncestor, nativeMergeAbort, nativeWorktreeList, nativeLsFiles, } from "./native-git-bridge.js";
32
+ import { nativeGetCurrentBranch, nativeDetectMainBranch, nativeWorkingTreeStatus, nativeAddAllWithExclusions, nativeCommit, nativeCheckoutBranch, nativeMergeSquash, nativeMergeRegular, nativeConflictFiles, nativeAddPaths, nativeRmForce, nativeBranchDelete, nativeBranchForceReset, nativeBranchExists, nativeDiffNumstat, nativeUpdateRef, nativeIsAncestor, nativeWorktreeList, nativeLsFiles, } from "./native-git-bridge.js";
30
33
  import { CLOSEOUT_CONSISTENCY_BLOCKED_REASON, } from "./closeout-consistency-gate.js";
31
34
  import { formatCloseoutProofBlock, proveMilestoneCloseout, } from "./milestone-closeout-proof.js";
32
35
  import { gsdHome } from "./gsd-home.js";
@@ -61,111 +64,6 @@ const ROOT_STATE_FILES = [
61
64
  // Back-sync (worktree → main) must NEVER overwrite the project root's copy
62
65
  // because the project root is authoritative for preferences (#2684).
63
66
  ];
64
- /**
65
- * Pop a stash entry by tracking the unique marker embedded in its message so
66
- * concurrent stash operations against the same project root cannot cause us to
67
- * pop the wrong entry.
68
- *
69
- * If `stashMarker` is null or no longer present in the stash list (e.g. a
70
- * concurrent process popped/dropped it), leaves the stash list untouched and
71
- * returns null.
72
- *
73
- * Throws on pop failure so callers can handle conflict cases the same way
74
- * they would with the prior `git stash pop` form. When throwing after a
75
- * targeted pop attempt, the error is annotated with the targeted stash ref.
76
- *
77
- * (Issue #4980 HIGH-6)
78
- */
79
- function popStashByRef(basePath, stashMarker) {
80
- let popArg = null;
81
- if (stashMarker) {
82
- try {
83
- const list = execFileSync("git", ["stash", "list", "--format=%gd%x00%s"], {
84
- cwd: basePath,
85
- stdio: ["ignore", "pipe", "pipe"],
86
- encoding: "utf-8",
87
- }).trim().split("\n").filter(Boolean);
88
- for (const entry of list) {
89
- const [ref, subject] = entry.split("\0");
90
- if (ref && subject?.includes(stashMarker)) {
91
- popArg = ref;
92
- break;
93
- }
94
- }
95
- }
96
- catch (err) {
97
- logWarning("worktree", `stash list lookup failed; leaving stash untouched: ${err instanceof Error ? err.message : String(err)}`);
98
- }
99
- }
100
- if (!popArg) {
101
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic pop");
102
- return null;
103
- }
104
- try {
105
- execFileSync("git", ["stash", "pop", popArg], {
106
- cwd: basePath,
107
- stdio: ["ignore", "pipe", "pipe"],
108
- encoding: "utf-8",
109
- });
110
- }
111
- catch (err) {
112
- if (err && typeof err === "object") {
113
- err.stashRef = popArg;
114
- }
115
- throw err;
116
- }
117
- return popArg;
118
- }
119
- /**
120
- * Extract a stash ref annotation injected by popStashByRef() when git stash
121
- * pop fails and we need to conditionally drop the exact stash entry later.
122
- */
123
- function stashRefFromError(err) {
124
- if (!err || typeof err !== "object")
125
- return null;
126
- const stashRef = err.stashRef;
127
- return typeof stashRef === "string" && stashRef.length > 0 ? stashRef : null;
128
- }
129
- function stashAlreadyExistsFilesFromError(err) {
130
- if (!err || typeof err !== "object")
131
- return [];
132
- const stderr = err.stderr;
133
- const stderrText = typeof stderr === "string"
134
- ? stderr
135
- : stderr instanceof Uint8Array
136
- ? Buffer.from(stderr).toString("utf-8")
137
- : "";
138
- const message = err instanceof Error ? err.message : String(err);
139
- const text = `${stderrText}\n${message}`;
140
- const files = new Set();
141
- for (const line of text.split("\n")) {
142
- const m = line.match(/^(.*?)\s+already exists, no checkout\s*$/i);
143
- if (!m)
144
- continue;
145
- const filePath = m[1]?.trim();
146
- if (filePath)
147
- files.add(filePath);
148
- }
149
- return [...files];
150
- }
151
- /**
152
- * Detect whether an on-disk file still contains unresolved merge conflict
153
- * markers from a failed stash-pop or merge attempt.
154
- *
155
- * Returns false when the file cannot be read.
156
- */
157
- function hasConflictMarkers(filePath) {
158
- try {
159
- const content = readFileSync(filePath, "utf-8");
160
- return content.includes("<<<<<<<") && content.includes("=======") && content.includes(">>>>>>>");
161
- }
162
- catch {
163
- return false;
164
- }
165
- }
166
- function gsdJsonlFilesWithConflictMarkers(basePath) {
167
- return nativeLsFiles(basePath, ".gsd/*.jsonl").filter((f) => hasConflictMarkers(join(basePath, f)));
168
- }
169
67
  /**
170
68
  * Check if two filesystem paths resolve to the same real location.
171
69
  * Returns false if either path cannot be resolved (e.g. doesn't exist).
@@ -421,51 +319,6 @@ export const SAFE_AUTO_RESOLVE_PATTERNS = [
421
319
  * Covers `.gsd/` state files and common build artifacts. */
422
320
  export const isSafeToAutoResolve = (filePath) => filePath.startsWith(".gsd/") ||
423
321
  SAFE_AUTO_RESOLVE_PATTERNS.some((re) => re.test(filePath));
424
- function removeMergeStateFiles(basePath, contextLabel) {
425
- try {
426
- for (const f of ["SQUASH_MSG", "MERGE_MSG", "MERGE_MODE", "MERGE_HEAD", "AUTO_MERGE"]) {
427
- const rawPath = execFileSync("git", ["rev-parse", "--git-path", f], {
428
- cwd: basePath,
429
- stdio: ["ignore", "pipe", "pipe"],
430
- encoding: "utf-8",
431
- }).trim();
432
- const p = rawPath.length > 0
433
- ? (isAbsolute(rawPath) ? rawPath : resolve(basePath, rawPath))
434
- : join(resolveGitDir(basePath), f);
435
- if (existsSync(p))
436
- unlinkSync(p);
437
- }
438
- }
439
- catch (err) {
440
- logError("worktree", `${contextLabel} merge state cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
441
- }
442
- }
443
- function cleanupConflictState(basePath) {
444
- // Merge conflicts can leave unmerged index entries; merge-abort alone is not
445
- // enough for squash merges (MERGE_HEAD is never written). Reset the merge
446
- // index, then remove merge message files that native/libgit2 paths may have
447
- // created.
448
- try {
449
- nativeMergeAbort(basePath);
450
- }
451
- catch (err) {
452
- // MERGE_HEAD absent (squash merge path) — abort is a no-op, which is fine.
453
- debugLog("conflict-cleanup:merge-abort-skipped", {
454
- error: err instanceof Error ? err.message : String(err),
455
- });
456
- }
457
- try {
458
- execFileSync("git", ["reset", "--merge"], {
459
- cwd: basePath,
460
- stdio: ["ignore", "pipe", "pipe"],
461
- encoding: "utf-8",
462
- });
463
- }
464
- catch (err) {
465
- logError("worktree", `git reset --merge failed after merge conflict: ${err instanceof Error ? err.message : String(err)}`);
466
- }
467
- removeMergeStateFiles(basePath, "conflict");
468
- }
469
322
  // ─── Dispatch-Level Sync (project root ↔ worktree) ──────────────────────────
470
323
  /**
471
324
  * Sync milestone artifacts from project root INTO worktree before deriveState.
@@ -539,19 +392,12 @@ export function checkResourcesStale(versionOnStart) {
539
392
  * Returns the corrected base path.
540
393
  */
541
394
  export function escapeStaleWorktree(base) {
542
- // Direct layout: /.gsd/worktrees/
543
- const directMarker = `${pathSep}.gsd${pathSep}worktrees${pathSep}`;
544
- let idx = base.indexOf(directMarker);
545
- if (idx === -1) {
546
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
547
- const symlinkRe = new RegExp(`\\${pathSep}\\.gsd\\${pathSep}projects\\${pathSep}[a-f0-9]+\\${pathSep}worktrees\\${pathSep}`);
548
- const match = base.match(symlinkRe);
549
- if (!match || match.index === undefined)
550
- return base;
551
- idx = match.index;
552
- }
553
- // base is inside .gsd/worktrees/<something> — extract the project root
554
- const projectRoot = base.slice(0, idx);
395
+ const segment = findWorktreeSegment(base.replaceAll("\\", "/"));
396
+ if (!segment)
397
+ return base;
398
+ // base is inside .gsd/worktrees/<something> — extract the project root.
399
+ // Normalization is 1:1 on characters, so the segment index is valid in `base`.
400
+ const projectRoot = base.slice(0, segment.gsdIdx);
555
401
  // Guard: If the candidate project root's .gsd IS the user-level ~/.gsd,
556
402
  // the string-slice heuristic matched the wrong /.gsd/ boundary. This happens
557
403
  // when .gsd is a symlink into ~/.gsd/projects/<hash> and process.cwd()
@@ -908,109 +754,6 @@ export function enterBranchModeForMilestone(basePath, milestoneId) {
908
754
  }
909
755
  checkoutBranchWithStashGuard(basePath, branch, `enter-branch-mode:${milestoneId}`);
910
756
  }
911
- export function checkoutBranchWithStashGuard(basePath, branch, reason) {
912
- let stashMarker = null;
913
- let stashed = false;
914
- const status = nativeWorkingTreeStatus(basePath).trim();
915
- if (status.length > 0) {
916
- stashMarker = `gsd-checkout-stash:${reason}:${process.pid}:${Date.now()}:${process.hrtime.bigint().toString(36)}`;
917
- const stashListBefore = execFileSync("git", ["stash", "list"], {
918
- cwd: basePath,
919
- stdio: ["ignore", "pipe", "pipe"],
920
- encoding: "utf-8",
921
- });
922
- execFileSync("git", ["stash", "push", "--include-untracked", "-m", `gsd: checkout stash [${stashMarker}]`], {
923
- cwd: basePath,
924
- stdio: ["ignore", "pipe", "pipe"],
925
- encoding: "utf-8",
926
- });
927
- const stashListAfter = execFileSync("git", ["stash", "list"], {
928
- cwd: basePath,
929
- stdio: ["ignore", "pipe", "pipe"],
930
- encoding: "utf-8",
931
- });
932
- stashed = stashListAfter !== stashListBefore;
933
- }
934
- // Checkout and stash-restore are split so we can distinguish two failure
935
- // modes: (a) checkout failed → HEAD did not move, restore stash and rethrow;
936
- // (b) checkout succeeded but stash pop failed → HEAD moved to `branch` but
937
- // the working-tree changes remain in the stash list. We surface a distinct
938
- // error in case (b) so callers don't assume the branch switch was rolled back.
939
- try {
940
- nativeCheckoutBranch(basePath, branch);
941
- }
942
- catch (checkoutErr) {
943
- if (stashed) {
944
- try {
945
- popStashByRef(basePath, stashMarker);
946
- }
947
- catch (restoreErr) {
948
- logWarning("worktree", `git stash pop failed during checkout restore: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
949
- }
950
- }
951
- throw checkoutErr;
952
- }
953
- if (stashed) {
954
- try {
955
- popStashByRef(basePath, stashMarker);
956
- }
957
- catch (popErr) {
958
- const msg = popErr instanceof Error ? popErr.message : String(popErr);
959
- const stderr = popErr && typeof popErr === "object"
960
- ? popErr.stderr
961
- : undefined;
962
- const stderrText = typeof stderr === "string"
963
- ? stderr
964
- : stderr instanceof Uint8Array
965
- ? Buffer.from(stderr).toString("utf-8")
966
- : "";
967
- const stashPopMessage = `${stderrText}\n${msg}`.trim();
968
- const alreadyExists = stashAlreadyExistsFilesFromError(popErr);
969
- const gsdAlreadyExists = alreadyExists.filter((f) => f.startsWith(".gsd/"));
970
- const nonGsdAlreadyExists = alreadyExists.filter((f) => !f.startsWith(".gsd/"));
971
- const isUntrackedRestoreFailure = stashPopMessage.includes("could not restore untracked files from stash");
972
- const stashRefForDrop = stashRefFromError(popErr);
973
- const nonGsdUnmerged = nativeConflictFiles(basePath).filter((f) => !f.startsWith(".gsd/"));
974
- const gsdContentConflicts = isUntrackedRestoreFailure
975
- ? gsdJsonlFilesWithConflictMarkers(basePath)
976
- : [];
977
- const gsdConflictFiles = [...new Set([...gsdAlreadyExists, ...gsdContentConflicts])];
978
- if (isUntrackedRestoreFailure &&
979
- gsdConflictFiles.length > 0 &&
980
- nonGsdAlreadyExists.length === 0 &&
981
- nonGsdUnmerged.length === 0) {
982
- for (const f of gsdConflictFiles) {
983
- execFileSync("git", ["checkout", "HEAD", "--", f], {
984
- cwd: basePath,
985
- stdio: ["ignore", "pipe", "pipe"],
986
- encoding: "utf-8",
987
- });
988
- nativeAddPaths(basePath, [f]);
989
- }
990
- if (stashRefForDrop) {
991
- try {
992
- execFileSync("git", ["stash", "drop", stashRefForDrop], {
993
- cwd: basePath,
994
- stdio: ["ignore", "pipe", "pipe"],
995
- encoding: "utf-8",
996
- });
997
- }
998
- catch (err) { /* stash may already be consumed */
999
- logWarning("worktree", `git stash drop failed: ${err instanceof Error ? err.message : String(err)}`);
1000
- }
1001
- }
1002
- else {
1003
- logWarning("worktree", "recorded stash entry could not be resolved; skipping automatic drop");
1004
- }
1005
- return;
1006
- }
1007
- const wrapped = new Error(`checkout to '${branch}' succeeded but stash restore failed; working tree changes remain in the stash list. Original error: ${msg}`);
1008
- if (stashRefForDrop)
1009
- wrapped.stashRef = stashRefForDrop;
1010
- throw wrapped;
1011
- }
1012
- }
1013
- }
1014
757
  // ─── Public API ────────────────────────────────────────────────────────────
1015
758
  /**
1016
759
  * Create a new auto-worktree for a milestone, chdir into it, and store
@@ -25,7 +25,7 @@ import { clearActivityLogState } from "./activity-log.js";
25
25
  import { synthesizeCrashRecovery, getDeepDiagnostic, readActiveMilestoneId, } from "./session-forensics.js";
26
26
  import { writeLock, clearLock, clearStaleWorkerLock, readCrashLock, isLockProcessAlive, formatCrashInfo, emitCrashRecoveredUnitEnd, emitOpenUnitEndForUnit, } from "./crash-recovery.js";
27
27
  import { acquireSessionLock, getSessionLockStatus, releaseSessionLock, updateSessionLock, } from "./session-lock.js";
28
- import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, } from "./preferences.js";
28
+ import { resolveAutoSupervisorConfig, loadEffectiveGSDPreferences, getIsolationMode, resolveEffectiveUnitIsolationMode, } from "./preferences.js";
29
29
  import { playNotificationBell, sendDesktopNotification } from "./notifications.js";
30
30
  import { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction, resolveCompactionThresholdPercent, shouldRerootStepSessionForContext, } from "./auto-budget.js";
31
31
  import { markToolStart as _markToolStart, markToolEnd as _markToolEnd, getOldestInFlightToolAgeMs as _getOldestInFlightToolAgeMs, clearInFlightTools, isToolInvocationError, isQueuedUserMessageSkip, isDeterministicPolicyError, } from "./auto-tool-tracking.js";
@@ -346,14 +346,11 @@ export function startAutoDetached(ctx, pi, base, verboseMode, options) {
346
346
  export function shouldUseWorktreeIsolation(basePath) {
347
347
  return getIsolationMode(basePath) === "worktree";
348
348
  }
349
- function resolveEffectiveUnitIsolationMode(configuredMode, isolationDegraded) {
350
- return configuredMode === "worktree" && isolationDegraded ? "branch" : configuredMode;
351
- }
352
- export function _resolveEffectiveUnitIsolationModeForTest(configuredMode, isolationDegraded) {
353
- return resolveEffectiveUnitIsolationMode(configuredMode, isolationDegraded);
349
+ export function _resolveEffectiveUnitIsolationModeForTest(configuredMode, isolationDegraded, strandedRecoveryIsolationMode = null) {
350
+ return resolveEffectiveUnitIsolationMode(configuredMode, isolationDegraded, strandedRecoveryIsolationMode);
354
351
  }
355
352
  function getEffectiveUnitIsolationMode(basePath) {
356
- return resolveEffectiveUnitIsolationMode(getIsolationMode(basePath), s.isolationDegraded);
353
+ return resolveEffectiveUnitIsolationMode(getIsolationMode(basePath), s.isolationDegraded, s.strandedRecoveryIsolationMode);
357
354
  }
358
355
  /** Crash recovery prompt — set by startAuto, consumed by the main loop */
359
356
  /** Pending verification retry — set when gate fails with retries remaining, consumed by autoLoop */
@@ -9,6 +9,7 @@ import { logWarning } from "../workflow-logger.js";
9
9
  import { openWorkflowDatabase } from "../db-workspace.js";
10
10
  import { getAutoWorktreePath } from "../auto-worktree.js";
11
11
  import { resolveWorktreeProjectRoot } from "../worktree-root.js";
12
+ import { worktreesDirs } from "../worktree-placement.js";
12
13
  export function safeWorkspaceCwd() {
13
14
  try {
14
15
  return process.cwd();
@@ -42,19 +43,21 @@ export function resolveWorkflowToolBasePath(ctx, scope) {
42
43
  return worktree;
43
44
  }
44
45
  else {
45
- const worktreesDir = join(projectRoot, ".gsd", "worktrees");
46
- if (existsSync(worktreesDir)) {
46
+ const live = [];
47
+ for (const worktreesDir of worktreesDirs(projectRoot)) {
48
+ if (!existsSync(worktreesDir))
49
+ continue;
47
50
  try {
48
- const live = readdirSync(worktreesDir)
51
+ live.push(...readdirSync(worktreesDir)
49
52
  .map((name) => join(worktreesDir, name))
50
- .filter((p) => existsSync(join(p, ".git")));
51
- if (live.length === 1)
52
- return live[0];
53
+ .filter((p) => existsSync(join(p, ".git"))));
53
54
  }
54
55
  catch (err) {
55
56
  logWarning("bootstrap", `resolveWorkflowToolBasePath: failed to scan worktrees: ${err instanceof Error ? err.message : String(err)}`);
56
57
  }
57
58
  }
59
+ if (live.length === 1)
60
+ return live[0];
58
61
  }
59
62
  return cwd;
60
63
  }
@@ -7,7 +7,7 @@ import { isToolCallEventType } from "@gsd/pi-coding-agent";
7
7
  import { ALWAYS_PRESERVED_SHIM_TOOL_NAMES } from "@gsd/pi-ai";
8
8
  import { updateSnapshot } from "../ecosystem/gsd-extension-api.js";
9
9
  import { buildMilestoneFileName, clearPathCache, milestonesDir, resolveMilestonePath, resolveSliceFile, resolveSlicePath } from "../paths.js";
10
- 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";
10
+ 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";
11
11
  import { resolveManifest } from "../unit-context-manifest.js";
12
12
  import { isBlockedStateFile, isBashWriteToStateFile, BLOCKED_WRITE_ERROR } from "../write-intercept.js";
13
13
  import { loadFile, saveFile, formatContinue } from "../files.js";
@@ -35,6 +35,7 @@ import { filterToolsForProvider } from "../model-router.js";
35
35
  import { mcpToolMatchesBaseName } from "../mcp-tool-name.js";
36
36
  import { RUN_UAT_READ_ONLY_TOOL_NAMES, RUN_UAT_WORKFLOW_TOOL_NAMES } from "../tool-presentation-plan.js";
37
37
  import { supportsSourceObservationsForUnit } from "../source-observations.js";
38
+ import { clearPendingAutoStart } from "../pending-auto-start.js";
38
39
  let approvalQuestionAbortInFlight = false;
39
40
  async function loadWelcomeScreenModule() {
40
41
  const candidates = [];
@@ -444,8 +445,16 @@ function isShellExecutionTool(canonicalName) {
444
445
  function activateDeferredApprovalGate(basePath) {
445
446
  if (deferredApprovalGate?.basePath !== basePath)
446
447
  return;
447
- setPendingGate(deferredApprovalGate.gateId, basePath);
448
+ const gateId = deferredApprovalGate.gateId;
448
449
  deferredApprovalGate = null;
450
+ refreshWriteGateStateFromDisk(basePath);
451
+ const snapshot = loadWriteGateSnapshot(basePath);
452
+ const milestoneId = extractDepthVerificationMilestoneId(gateId);
453
+ if (isApprovalGateVerifiedInSnapshot(snapshot, gateId))
454
+ return;
455
+ if (milestoneId && isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId))
456
+ return;
457
+ setPendingGate(gateId, basePath);
449
458
  }
450
459
  function extractGateQuestionId(input) {
451
460
  const questions = input?.questions ?? [];
@@ -626,7 +635,7 @@ export function registerHooks(pi, ecosystemHandlers) {
626
635
  catch { /* non-fatal */ }
627
636
  }
628
637
  });
629
- pi.on("session_switch", async (_event, ctx) => {
638
+ pi.on("session_switch", async (event, ctx) => {
630
639
  const basePath = contextBasePath(ctx);
631
640
  const preserveCloseoutSurface = isAutoCompletionStopInProgress();
632
641
  initSessionNotifications(ctx);
@@ -635,6 +644,13 @@ export function registerHooks(pi, ecosystemHandlers) {
635
644
  clearDeferredApprovalGate();
636
645
  await resetAskUserQuestionsTurnCache();
637
646
  clearDiscussionFlowState(basePath);
647
+ // /clear or /new destroys the conversation holding a discuss interview, so
648
+ // its pending discuss→auto handoff can never be answered — clear it. Resume
649
+ // restores the interview transcript, so the entry survives. Auto-mode's own
650
+ // newSession() calls are safe: the handoff consumes the entry on agent_end.
651
+ if (event.reason === "new") {
652
+ clearPendingAutoStart(basePath);
653
+ }
638
654
  await syncServiceTierStatus(ctx);
639
655
  await applyDisabledModelProviderPolicy(ctx);
640
656
  await applyCompactionThresholdOverride(ctx);
@@ -1150,6 +1166,19 @@ export function registerHooks(pi, ecosystemHandlers) {
1150
1166
  clearDeferredApprovalGate(basePath);
1151
1167
  }
1152
1168
  }
1169
+ // Safety harness: record evidence here, not only in tool_call. External
1170
+ // engines (claude-code-cli) pre-execute tools, so the agent loop skips
1171
+ // beforeToolCall/tool_call for them — tool_execution_start is the only
1172
+ // event that fires for every tool call. recordToolCall dedupes by
1173
+ // toolCallId, so native tools (which hit both events) record once.
1174
+ safetyRecordToolCall(event.toolCallId, event.toolName, (event.args ?? {}));
1175
+ const execDash = getAutoRuntimeSnapshot();
1176
+ if (execDash.basePath && execDash.currentUnit?.type === "execute-task") {
1177
+ const { milestone: xMid, slice: xSid, task: xTid } = parseUnitId(execDash.currentUnit.id);
1178
+ if (xMid && xSid && xTid) {
1179
+ saveEvidenceToDisk(execDash.basePath, xMid, xSid, xTid);
1180
+ }
1181
+ }
1153
1182
  if (!isAutoActive())
1154
1183
  return;
1155
1184
  markToolStart(event.toolCallId, event.toolName);
@@ -8,6 +8,7 @@ import { getIsolationMode } from "../preferences.js";
8
8
  import { compileSubagentPermissionContract } from "../unit-context-manifest.js";
9
9
  import { logWarning } from "../workflow-logger.js";
10
10
  import { isGsdWorktreePath, resolveWorktreeProjectRoot } from "../worktree-root.js";
11
+ import { worktreesDirs } from "../worktree-placement.js";
11
12
  /**
12
13
  * Regex matching milestone CONTEXT.md file names in both legacy M001
13
14
  * and unique M001-abc123 formats. Exported so regex-hardening tests
@@ -214,6 +215,23 @@ export function loadWriteGateSnapshot(basePath) {
214
215
  return currentWriteGateSnapshot(basePath);
215
216
  }
216
217
  }
218
+ /**
219
+ * Merge the persisted write-gate snapshot into the in-process Map entry.
220
+ * The workflow MCP server runs in a child process and records depth
221
+ * verification there; without this refresh the extension host keeps stale
222
+ * pending-gate memory and `activateDeferredApprovalGate` can re-arm a gate
223
+ * that the subprocess already cleared on disk.
224
+ */
225
+ export function refreshWriteGateStateFromDisk(basePath) {
226
+ if (!shouldPersistWriteGateSnapshot())
227
+ return;
228
+ const snapshot = loadWriteGateSnapshot(basePath);
229
+ const state = getWriteGateState(basePath);
230
+ state.pendingGateId = snapshot.pendingGateId;
231
+ state.activeQueuePhase = snapshot.activeQueuePhase;
232
+ state.verifiedDepthMilestones = new Set(snapshot.verifiedDepthMilestones);
233
+ state.verifiedApprovalGates = new Set(snapshot.verifiedApprovalGates ?? []);
234
+ }
217
235
  export function isDepthVerified(basePath = process.cwd()) {
218
236
  return getWriteGateState(basePath).verifiedDepthMilestones.size > 0;
219
237
  }
@@ -223,6 +241,7 @@ export function isDepthVerified(basePath = process.cwd()) {
223
241
  export function isMilestoneDepthVerified(milestoneId, basePath = process.cwd()) {
224
242
  if (!milestoneId)
225
243
  return false;
244
+ refreshWriteGateStateFromDisk(basePath);
226
245
  return getWriteGateState(basePath).verifiedDepthMilestones.has(milestoneId);
227
246
  }
228
247
  export function isMilestoneDepthVerifiedInSnapshot(snapshot, milestoneId) {
@@ -309,6 +328,7 @@ export function clearPendingGate(basePath) {
309
328
  * Get the currently pending gate, if any.
310
329
  */
311
330
  export function getPendingGate(basePath = process.cwd()) {
331
+ refreshWriteGateStateFromDisk(basePath);
312
332
  return getWriteGateState(basePath).pendingGateId;
313
333
  }
314
334
  /**
@@ -919,10 +939,12 @@ export function shouldBlockWorktreeWrite(toolName, targetPath, effectiveBasePath
919
939
  const realTarget = realpathOrResolve(absTarget);
920
940
  const realRoot = realpathOrResolve(projectRoot);
921
941
  const realGsd = realpathOrResolve(join(projectRoot, ".gsd"));
922
- const realWorktreesDir = realpathOrResolve(join(projectRoot, ".gsd", "worktrees"));
923
- // Allow writes inside the legitimate worktrees subtree.
924
- if (isPathContained(realTarget, realWorktreesDir))
925
- return { block: false };
942
+ // Allow writes inside a legitimate worktrees subtree (canonical
943
+ // .gsd-worktrees/ or legacy .gsd/worktrees/).
944
+ for (const container of worktreesDirs(projectRoot)) {
945
+ if (isPathContained(realTarget, realpathOrResolve(container)))
946
+ return { block: false };
947
+ }
926
948
  // Allow writes to .gsd/ planning artifacts, but reject siblings whose name
927
949
  // starts with "worktrees" (the worktrees-extra prefix trick — case 4).
928
950
  if (isPathContained(realTarget, realGsd)) {
@@ -8,9 +8,10 @@
8
8
  * `.gsd/CAPTURES.md`, not the worktree's local `.gsd/`.
9
9
  */
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
- import { join, resolve, sep } from "node:path";
11
+ import { join, resolve } from "node:path";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { gsdRoot } from "./paths.js";
14
+ import { findWorktreeSegment } from "./worktree-root.js";
14
15
  // ─── Constants ────────────────────────────────────────────────────────────────
15
16
  const CAPTURES_FILENAME = "CAPTURES.md";
16
17
  const VALID_CLASSIFICATIONS = [
@@ -30,19 +31,10 @@ const VALID_CLASSIFICATIONS = [
30
31
  */
31
32
  export function resolveCapturesPath(basePath) {
32
33
  const resolved = resolve(basePath);
33
- // Direct layout: /.gsd/worktrees/
34
- const worktreeMarker = `${sep}.gsd${sep}worktrees${sep}`;
35
- let idx = resolved.indexOf(worktreeMarker);
36
- if (idx === -1) {
37
- // Symlink-resolved layout: /.gsd/projects/<hash>/worktrees/
38
- const symlinkRe = new RegExp(`\\${sep}\\.gsd\\${sep}projects\\${sep}[a-f0-9]+\\${sep}worktrees\\${sep}`);
39
- const match = resolved.match(symlinkRe);
40
- if (match && match.index !== undefined)
41
- idx = match.index;
42
- }
43
- if (idx !== -1) {
34
+ const segment = findWorktreeSegment(resolved.replaceAll("\\", "/"));
35
+ if (segment) {
44
36
  // basePath is inside a worktree — resolve to project root
45
- const projectRoot = resolved.slice(0, idx);
37
+ const projectRoot = resolved.slice(0, segment.gsdIdx);
46
38
  return join(projectRoot, ".gsd", CAPTURES_FILENAME);
47
39
  }
48
40
  return join(gsdRoot(basePath), CAPTURES_FILENAME);
@@ -2,12 +2,13 @@
2
2
  // File Purpose: Closeout git failure discovery, retry, and manual resolution helpers.
3
3
  import { execFileSync } from "node:child_process";
4
4
  import { existsSync, realpathSync } from "node:fs";
5
- import { isAbsolute, join, resolve } from "node:path";
5
+ import { isAbsolute, resolve } from "node:path";
6
6
  import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
7
7
  import { runTurnGitAction } from "./git-service.js";
8
8
  import { _getAdapter, upsertTurnGitTransaction } from "./gsd-db.js";
9
9
  import { probeGitConflictState } from "./git-conflict-state.js";
10
10
  import { parseUnitId } from "./unit-id.js";
11
+ import { worktreePathFor } from "./worktree-placement.js";
11
12
  function parseMetadata(value) {
12
13
  if (typeof value !== "string" || !value.trim())
13
14
  return {};
@@ -121,7 +122,7 @@ export function resolveCloseoutRecoveryBasePath(projectRoot, record) {
121
122
  const parsed = parseUnitId(record.unitId);
122
123
  const milestoneId = parsed.milestone ?? (/^M\d+(?:-[a-z0-9]{6})?/.exec(record.unitId)?.[0] ?? "");
123
124
  if (milestoneId) {
124
- const worktreePath = existingRealPath(join(projectRoot, ".gsd", "worktrees", milestoneId));
125
+ const worktreePath = existingRealPath(worktreePathFor(projectRoot, milestoneId));
125
126
  if (worktreePath)
126
127
  return worktreePath;
127
128
  }