@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,6 +8,11 @@
8
8
  *
9
9
  * Fix: removeWorktree should query `git worktree list` to find the actual
10
10
  * registered path when the computed path doesn't match.
11
+ *
12
+ * New worktrees are created at the canonical `.gsd-worktrees/` sibling and
13
+ * never cross the symlink, so this scenario only applies to LEGACY worktrees
14
+ * (created under `.gsd/worktrees/` by older versions). The test creates the
15
+ * worktree at the legacy location directly to keep that coverage.
11
16
  */
12
17
  import { mkdtempSync, mkdirSync, rmSync, symlinkSync, unlinkSync, writeFileSync, existsSync, realpathSync } from "node:fs";
13
18
  import { join } from "node:path";
@@ -15,7 +20,6 @@ import { tmpdir } from "node:os";
15
20
  import { execSync } from "node:child_process";
16
21
 
17
22
  import {
18
- createWorktree,
19
23
  removeWorktree,
20
24
  listWorktrees,
21
25
  worktreePath,
@@ -64,13 +68,15 @@ test('worktree-symlink-removal removes the git-registered symlink target safely'
64
68
  run("git add .", base);
65
69
  run('git commit -m "init"', base);
66
70
 
67
- // Create a worktree git will resolve the symlink and register
68
- // the worktree at the external path
69
- const info = createWorktree(base, "M002", { branch: "milestone/M002" });
70
- assert.ok(info.exists, "worktree created");
71
+ // Create a LEGACY worktree through the symlinked .gsd path git resolves
72
+ // the symlink and registers the worktree at the external path. (Current
73
+ // createWorktree() uses the canonical .gsd-worktrees/ sibling instead.)
74
+ const legacyPath = join(base, ".gsd", "worktrees", "M002");
75
+ run(`git worktree add -b milestone/M002 "${legacyPath}"`, base);
76
+ assert.ok(existsSync(legacyPath), "worktree created");
71
77
 
72
78
  // Verify worktree was created at the resolved (external) path
73
- const realWtPath = realpathSync(info.path);
79
+ const realWtPath = realpathSync(legacyPath);
74
80
  assert.ok(
75
81
  realWtPath.startsWith(externalState),
76
82
  `worktree real path (${realWtPath}) is under external state dir`,
@@ -116,8 +116,8 @@ describe("worktree-teardown-safety", () => {
116
116
 
117
117
  const wtPathResult = worktreePath(tempDir, "anything");
118
118
  assertTrue(
119
- wtPathResult.startsWith(join(tempDir, ".gsd", "worktrees")),
120
- "worktreePath returns path under .gsd/worktrees/",
119
+ wtPathResult.startsWith(join(tempDir, ".gsd-worktrees")),
120
+ "worktreePath returns path under the canonical .gsd-worktrees/ container",
121
121
  );
122
122
  });
123
123
 
@@ -803,3 +803,45 @@ test('write-gate: resetWriteGateState persists through dangling .gsd symlink', (
803
803
  } catch { /* swallow */ }
804
804
  }
805
805
  });
806
+
807
+ // ─── Scenario 31: hydrate in-memory gate state from persisted snapshot (MCP subprocess) ──
808
+
809
+ test('write-gate: getPendingGate hydrates from disk when workflow MCP verified gate in child process', () => {
810
+ const base = join(tmpdir(), `gsd-write-gate-mcp-hydrate-${randomUUID()}`);
811
+ const stateFilePath = join(base, '.gsd', 'runtime', 'write-gate-state.json');
812
+ const originalEnv = process.env.GSD_PERSIST_WRITE_GATE_STATE;
813
+ const gateId = 'depth_verification_M005_confirm';
814
+
815
+ try {
816
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = '1';
817
+ mkdirSync(join(base, '.gsd', 'runtime'), { recursive: true });
818
+ clearDiscussionFlowState(base);
819
+ setPendingGate(gateId, base);
820
+
821
+ writeFileSync(stateFilePath, JSON.stringify({
822
+ verifiedDepthMilestones: ['M005'],
823
+ verifiedApprovalGates: [gateId],
824
+ activeQueuePhase: false,
825
+ pendingGateId: null,
826
+ }, null, 2), 'utf-8');
827
+
828
+ assert.strictEqual(getPendingGate(base), null, 'stale in-memory pending must refresh from disk');
829
+ assert.strictEqual(isMilestoneDepthVerified('M005', base), true, 'verified milestone must hydrate from disk');
830
+ assert.deepEqual(loadWriteGateSnapshot(base), {
831
+ verifiedDepthMilestones: ['M005'],
832
+ verifiedApprovalGates: [gateId],
833
+ activeQueuePhase: false,
834
+ pendingGateId: null,
835
+ });
836
+ } finally {
837
+ if (originalEnv === undefined) {
838
+ delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
839
+ } else {
840
+ process.env.GSD_PERSIST_WRITE_GATE_STATE = originalEnv;
841
+ }
842
+ clearDiscussionFlowState(base);
843
+ try {
844
+ rmSync(base, { recursive: true, force: true });
845
+ } catch { /* swallow */ }
846
+ }
847
+ });
@@ -15,20 +15,11 @@ import { existsSync, readFileSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
 
17
17
  import type { CompleteSliceParams } from "../types.js";
18
- import { isClosedStatus } from "../status-guards.js";
19
18
  import {
20
- transaction,
21
- insertMilestone,
22
- insertSlice,
23
- getSlice,
24
- getSliceTasks,
25
- getMilestone,
26
- updateSliceStatus,
19
+ completeSliceCascade,
27
20
  setSliceSummaryMd,
28
21
  saveGateResult,
29
22
  getPendingGatesForTurn,
30
- getMilestoneSlices,
31
- updateMilestoneStatus,
32
23
  } from "../gsd-db.js";
33
24
  import { getGatesForTurn } from "../gate-registry.js";
34
25
  import { gsdProjectionRoot, clearPathCache, resolveMilestoneFile } from "../paths.js";
@@ -371,60 +362,34 @@ export async function handleCompleteSlice(
371
362
  };
372
363
  }
373
364
 
374
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
365
+ // ── Atomic completion cascade (guards + writes in one transaction) ───────
375
366
  const completedAt = new Date().toISOString();
376
367
  let guardError: string | null = null;
377
368
  let existingSummaryMd = "";
378
369
  let duplicateComplete = false;
379
370
 
380
- transaction(() => {
381
- // State machine preconditions (inside txn for atomicity).
382
- // Milestone/slice not existing is OK — insertMilestone/insertSlice below will auto-create.
383
- // Only block if they exist and are closed.
384
- const milestone = getMilestone(params.milestoneId);
385
- if (milestone && isClosedStatus(milestone.status)) {
386
- guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
387
- return;
388
- }
389
-
390
- const slice = getSlice(params.milestoneId, params.sliceId);
391
- existingSummaryMd = slice?.full_summary_md?.trim() ?? "";
392
- if (slice && isClosedStatus(slice.status)) {
393
- duplicateComplete = true;
394
- return;
395
- }
396
-
397
- // Verify all tasks are complete
398
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
399
- if (tasks.length === 0) {
400
- guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
401
- return;
402
- }
403
-
404
- const incompleteTasks = tasks.filter(t => !isClosedStatus(t.status));
405
- if (incompleteTasks.length > 0) {
406
- const incompleteIds = incompleteTasks.map(t => `${t.id} (status: ${t.status})`).join(", ");
407
- guardError = `incomplete tasks: ${incompleteIds}`;
408
- return;
409
- }
410
-
411
- // All guards passed — perform writes. Preserve existing planning metadata:
412
- // completion should not overwrite title/risk/depends/demo/sequence.
413
- insertMilestone({ id: params.milestoneId, title: params.milestoneId });
414
- if (!slice) {
415
- insertSlice({ id: params.sliceId, milestoneId: params.milestoneId, title: params.sliceTitle || params.sliceId });
416
- }
417
- updateSliceStatus(params.milestoneId, params.sliceId, "complete", completedAt);
418
-
419
- const updatedSlices = getMilestoneSlices(params.milestoneId);
420
- if (
421
- milestone?.status === "planned" &&
422
- updatedSlices.length > 0 &&
423
- updatedSlices.every((s) => isClosedStatus(s.status))
424
- ) {
425
- updateMilestoneStatus(params.milestoneId, "active");
426
- }
371
+ const outcome = completeSliceCascade(params.milestoneId, params.sliceId, {
372
+ sliceTitle: params.sliceTitle,
373
+ completedAt,
427
374
  });
375
+ if (outcome.ok) {
376
+ existingSummaryMd = outcome.existingSummaryMd;
377
+ duplicateComplete = outcome.duplicate;
378
+ } else {
379
+ switch (outcome.reason) {
380
+ case "milestone-closed":
381
+ guardError = `cannot complete slice in a closed milestone: ${params.milestoneId} (status: ${outcome.status})`;
382
+ break;
383
+ case "no-tasks":
384
+ guardError = `no tasks found for slice ${params.sliceId} in milestone ${params.milestoneId}`;
385
+ break;
386
+ case "incomplete-tasks": {
387
+ const incompleteIds = outcome.incomplete.map((t) => `${t.id} (status: ${t.status})`).join(", ");
388
+ guardError = `incomplete tasks: ${incompleteIds}`;
389
+ break;
390
+ }
391
+ }
392
+ }
428
393
 
429
394
  if (duplicateComplete) {
430
395
  const staleSummaryPath = sliceSummaryPath(
@@ -11,6 +11,7 @@ import {
11
11
  import { realpathSync } from "node:fs";
12
12
  import path from "node:path";
13
13
  import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
14
+ import { findWorktreeSegment } from "../worktree-root.js";
14
15
  import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
15
16
 
16
17
  export interface ExecToolParams {
@@ -201,11 +202,10 @@ function normalizeScanPath(value: string): string {
201
202
 
202
203
  function parseWorktreeBase(baseDir: string): { originalRoot: string; worktreeRoot: string } | null {
203
204
  const normalizedBase = normalizeScanPath(baseDir);
204
- const marker = "/.gsd/worktrees/";
205
- const markerIndex = normalizedBase.indexOf(marker);
206
- if (markerIndex <= 0) return null;
205
+ const segment = findWorktreeSegment(normalizedBase);
206
+ if (!segment || segment.gsdIdx <= 0) return null;
207
207
  return {
208
- originalRoot: normalizedBase.slice(0, markerIndex),
208
+ originalRoot: normalizedBase.slice(0, segment.gsdIdx),
209
209
  worktreeRoot: normalizedBase,
210
210
  };
211
211
  }
@@ -297,7 +297,7 @@ function scriptReferencesOriginalRootFromWorktree(script: string, baseDir: strin
297
297
  const normalizedScript = script.replace(/\\/g, "/");
298
298
  return comparablePathVariants(parsed.originalRoot).some((originalRoot) => {
299
299
  const originalRootPattern = new RegExp(
300
- `${escapeRegExp(originalRoot)}(?=$|[\\s'"\\\`;)&|<>]|/(?!\\.gsd/worktrees(?:/|$)))`,
300
+ `${escapeRegExp(originalRoot)}(?=$|[\\s'"\\\`;)&|<>]|/(?!\\.gsd(?:-worktrees|/worktrees)(?:/|$)))`,
301
301
  );
302
302
  return originalRootPattern.test(normalizedScript);
303
303
  });
@@ -10,16 +10,11 @@
10
10
  */
11
11
 
12
12
  import {
13
- getMilestone,
14
13
  getMilestoneSlices,
15
14
  getSliceTasks,
16
- reopenMilestoneStatus,
17
- updateSliceStatus,
18
- updateTaskStatus,
19
- transaction,
15
+ reopenMilestoneCascade,
20
16
  } from "../gsd-db.js";
21
17
  import { invalidateStateCache } from "../state.js";
22
- import { isClosedStatus } from "../status-guards.js";
23
18
  import { renderAllProjections } from "../workflow-projections.js";
24
19
  import { writeManifest } from "../workflow-manifest.js";
25
20
  import { appendEvent } from "../workflow-events.js";
@@ -53,40 +48,18 @@ export async function handleReopenMilestone(
53
48
  return { error: "milestoneId is required and must be a non-empty string" };
54
49
  }
55
50
 
56
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
57
- let guardError: string | null = null;
58
- let slicesResetCount = 0;
59
- let tasksResetCount = 0;
60
-
61
- transaction(() => {
62
- const milestone = getMilestone(params.milestoneId);
63
- if (!milestone) {
64
- guardError = `milestone not found: ${params.milestoneId}`;
65
- return;
66
- }
67
- if (!isClosedStatus(milestone.status)) {
68
- guardError = `milestone ${params.milestoneId} is not closed (status: ${milestone.status}) — nothing to reopen`;
69
- return;
70
- }
71
-
72
- reopenMilestoneStatus(params.milestoneId);
73
-
74
- const slices = getMilestoneSlices(params.milestoneId);
75
- slicesResetCount = slices.length;
76
-
77
- for (const slice of slices) {
78
- updateSliceStatus(params.milestoneId, slice.id, "in_progress");
79
- const tasks = getSliceTasks(params.milestoneId, slice.id);
80
- tasksResetCount += tasks.length;
81
- for (const task of tasks) {
82
- updateTaskStatus(params.milestoneId, slice.id, task.id, "pending");
83
- }
51
+ // ── Atomic reopen cascade (guards + writes in one transaction) ───────────
52
+ const outcome = reopenMilestoneCascade(params.milestoneId);
53
+ if (!outcome.ok) {
54
+ switch (outcome.reason) {
55
+ case "milestone-not-found":
56
+ return { error: `milestone not found: ${params.milestoneId}` };
57
+ case "milestone-not-closed":
58
+ return { error: `milestone ${params.milestoneId} is not closed (status: ${outcome.status}) — nothing to reopen` };
84
59
  }
85
- });
86
-
87
- if (guardError) {
88
- return { error: guardError };
89
60
  }
61
+ const slicesResetCount = outcome.slicesReset;
62
+ const tasksResetCount = outcome.tasksReset;
90
63
 
91
64
  // ── Invalidate caches ────────────────────────────────────────────────────
92
65
  invalidateStateCache();
@@ -12,15 +12,10 @@
12
12
  // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
13
13
 
14
14
  import {
15
- getMilestone,
16
- getSlice,
17
15
  getSliceTasks,
18
- updateSliceStatus,
19
- updateTaskStatus,
20
- transaction,
16
+ reopenSliceCascade,
21
17
  } from "../gsd-db.js";
22
18
  import { invalidateStateCache } from "../state.js";
23
- import { isClosedStatus } from "../status-guards.js";
24
19
  import { renderAllProjections } from "../workflow-projections.js";
25
20
  import { writeManifest } from "../workflow-manifest.js";
26
21
  import { appendEvent } from "../workflow-events.js";
@@ -57,44 +52,21 @@ export async function handleReopenSlice(
57
52
  return { error: "milestoneId is required and must be a non-empty string" };
58
53
  }
59
54
 
60
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
61
- let guardError: string | null = null;
62
- let tasksResetCount = 0;
63
-
64
- transaction(() => {
65
- const milestone = getMilestone(params.milestoneId);
66
- if (!milestone) {
67
- guardError = `milestone not found: ${params.milestoneId}`;
68
- return;
69
- }
70
- if (isClosedStatus(milestone.status)) {
71
- guardError = `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${milestone.status})`;
72
- return;
73
- }
74
-
75
- const slice = getSlice(params.milestoneId, params.sliceId);
76
- if (!slice) {
77
- guardError = `slice not found: ${params.milestoneId}/${params.sliceId}`;
78
- return;
79
- }
80
- if (!isClosedStatus(slice.status)) {
81
- guardError = `slice ${params.sliceId} is not complete (status: ${slice.status}) — nothing to reopen`;
82
- return;
55
+ // ── Atomic reopen cascade (guards + writes in one transaction) ───────────
56
+ const outcome = reopenSliceCascade(params.milestoneId, params.sliceId);
57
+ if (!outcome.ok) {
58
+ switch (outcome.reason) {
59
+ case "milestone-not-found":
60
+ return { error: `milestone not found: ${params.milestoneId}` };
61
+ case "milestone-closed":
62
+ return { error: `cannot reopen slice in a closed milestone: ${params.milestoneId} (status: ${outcome.status})` };
63
+ case "slice-not-found":
64
+ return { error: `slice not found: ${params.milestoneId}/${params.sliceId}` };
65
+ case "slice-not-complete":
66
+ return { error: `slice ${params.sliceId} is not complete (status: ${outcome.status}) — nothing to reopen` };
83
67
  }
84
-
85
- // Fetch tasks inside txn so the list is consistent with the slice status check
86
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
87
- tasksResetCount = tasks.length;
88
-
89
- updateSliceStatus(params.milestoneId, params.sliceId, "in_progress");
90
- for (const task of tasks) {
91
- updateTaskStatus(params.milestoneId, params.sliceId, task.id, "pending");
92
- }
93
- });
94
-
95
- if (guardError) {
96
- return { error: guardError };
97
68
  }
69
+ const tasksResetCount = outcome.tasksReset;
98
70
 
99
71
  // ── Invalidate caches ────────────────────────────────────────────────────
100
72
  invalidateStateCache();
@@ -11,14 +11,9 @@
11
11
  */
12
12
 
13
13
  import {
14
- getSlice,
15
- getSliceTasks,
16
14
  isDbAvailable,
17
- transaction,
18
- updateSliceStatus,
19
- updateTaskStatus,
15
+ skipSliceCascade,
20
16
  } from "../gsd-db.js";
21
- import { isClosedStatus } from "../status-guards.js";
22
17
 
23
18
  /**
24
19
  * Input parameters for {@link handleSkipSlice}.
@@ -90,44 +85,23 @@ export function handleSkipSlice(params: SkipSliceParams): SkipSliceResult {
90
85
  throw new Error("handleSkipSlice: GSD database is not available");
91
86
  }
92
87
 
93
- // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ────
94
- let guardError: string | null = null;
95
- let guardCode: SkipSliceErrorCode | null = null;
96
- let wasAlreadySkipped = false;
97
- let tasksSkipped = 0;
98
-
99
- transaction(() => {
100
- const slice = getSlice(params.milestoneId, params.sliceId);
101
- if (!slice) {
102
- guardError = `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`;
103
- guardCode = "slice_not_found";
104
- return;
105
- }
106
- if (slice.status === "complete" || slice.status === "done") {
107
- guardError = `Slice ${params.sliceId} is already complete — cannot skip.`;
108
- guardCode = "already_complete";
109
- return;
110
- }
111
-
112
- wasAlreadySkipped = slice.status === "skipped";
113
- if (!wasAlreadySkipped) {
114
- updateSliceStatus(params.milestoneId, params.sliceId, "skipped");
88
+ // ── Atomic skip cascade (guards + writes in one transaction) ────────────
89
+ const outcome = skipSliceCascade(params.milestoneId, params.sliceId);
90
+ if (!outcome.ok) {
91
+ switch (outcome.reason) {
92
+ case "slice-not-found":
93
+ return {
94
+ ...base,
95
+ error: `Slice ${params.sliceId} not found in milestone ${params.milestoneId}`,
96
+ errorCode: "slice_not_found" as SkipSliceErrorCode,
97
+ };
98
+ case "slice-already-complete":
99
+ return {
100
+ ...base,
101
+ error: `Slice ${params.sliceId} is already complete cannot skip.`,
102
+ errorCode: "already_complete" as SkipSliceErrorCode,
103
+ };
115
104
  }
116
-
117
- // Cascade: mark every non-closed task as skipped so milestone completion
118
- // doesn't trip the deep-task guard (#4375). Closed tasks (complete/done/
119
- // skipped) are left untouched — we never downgrade.
120
- const tasks = getSliceTasks(params.milestoneId, params.sliceId);
121
- for (const task of tasks) {
122
- if (!isClosedStatus(task.status)) {
123
- updateTaskStatus(params.milestoneId, params.sliceId, task.id, "skipped");
124
- tasksSkipped++;
125
- }
126
- }
127
- });
128
-
129
- if (guardError) {
130
- return { ...base, error: guardError, errorCode: guardCode ?? undefined };
131
105
  }
132
- return { ...base, tasksSkipped, wasAlreadySkipped };
106
+ return { ...base, tasksSkipped: outcome.tasksSkipped, wasAlreadySkipped: outcome.wasAlreadySkipped };
133
107
  }
@@ -13,7 +13,7 @@ import { deriveState } from "./state.js";
13
13
  import { invalidateAllCaches } from "./cache.js";
14
14
  import { gsdRoot, resolveTasksDir, resolveSlicePath, resolveTaskFile, buildTaskFileName, buildSliceFileName } from "./paths.js";
15
15
  import { sendDesktopNotification } from "./notifications.js";
16
- import { getTask, getSlice, getSliceTasks, updateTaskStatus, updateSliceStatus } from "./gsd-db.js";
16
+ import { getTask, getSlice, getSliceTasks, updateTaskStatus, resetSliceCascade } from "./gsd-db.js";
17
17
  import { renderPlanCheckboxes, renderRoadmapCheckboxes } from "./markdown-renderer.js";
18
18
 
19
19
  /**
@@ -330,12 +330,16 @@ export async function handleResetSlice(
330
330
  return;
331
331
  }
332
332
 
333
- // Reset all tasks
334
- let tasksReset = 0;
333
+ // Reset all task statuses to "pending" and the slice to "active" in one
334
+ // atomic commit (DB is source of truth). Previously a per-task updateTaskStatus
335
+ // loop + a separate updateSliceStatus, which could leave a partial reset if
336
+ // interrupted mid-loop.
337
+ resetSliceCascade(mid, sid);
338
+
339
+ // Delete task summary files — projection cleanup, separate from the DB reset.
340
+ const tasksReset = tasks.length;
335
341
  let summariesDeleted = 0;
336
342
  for (const t of tasks) {
337
- updateTaskStatus(mid, sid, t.id, "pending");
338
- tasksReset++;
339
343
  const summaryPath = resolveTaskFile(basePath, mid, sid, t.id, "SUMMARY");
340
344
  if (summaryPath && existsSync(summaryPath)) {
341
345
  unlinkSync(summaryPath);
@@ -343,9 +347,6 @@ export async function handleResetSlice(
343
347
  }
344
348
  }
345
349
 
346
- // Reset slice status
347
- updateSliceStatus(mid, sid, "active");
348
-
349
350
  // Delete slice summary and UAT files
350
351
  let sliceFilesDeleted = 0;
351
352
  const slicePath = resolveSlicePath(basePath, mid, sid);