gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.42bd34838

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 (241) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  5. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  6. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  8. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  9. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  10. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  11. package/dist/resources/extensions/gsd/auto.js +68 -4
  12. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  13. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  15. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  16. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  17. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  18. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  19. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  20. package/dist/resources/extensions/gsd/git-service.js +36 -4
  21. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  22. package/dist/resources/extensions/gsd/guided-flow.js +52 -10
  23. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  24. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  25. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  26. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  27. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  28. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  29. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  30. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  32. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  33. package/dist/resources/extensions/gsd/quick.js +34 -2
  34. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  35. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  36. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  37. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  38. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  39. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  40. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  41. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  42. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  43. package/dist/resources/extensions/gsd/unit-runtime.js +22 -0
  44. package/dist/resources/extensions/gsd/worktree-resolver.js +33 -17
  45. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  46. package/dist/web/standalone/.next/BUILD_ID +1 -1
  47. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  48. package/dist/web/standalone/.next/build-manifest.json +2 -2
  49. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  50. package/dist/web/standalone/.next/required-server-files.json +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.html +1 -1
  68. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  75. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  77. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  78. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  79. package/dist/web/standalone/server.js +1 -1
  80. package/package.json +3 -3
  81. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  82. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  83. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  84. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  85. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  86. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  87. package/packages/native/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  89. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  92. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  94. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  96. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  98. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  100. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  102. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  104. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  106. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  110. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  112. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  114. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  116. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  121. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  123. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  130. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  137. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  138. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  139. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  140. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  142. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  144. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  145. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  146. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  147. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  148. package/packages/pi-coding-agent/src/core/db-snapshot.test.ts +32 -0
  149. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  150. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  151. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  152. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  153. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  154. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  155. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  156. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  157. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  158. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  159. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  160. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  161. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  162. package/packages/pi-tui/dist/tui.js +18 -8
  163. package/packages/pi-tui/dist/tui.js.map +1 -1
  164. package/packages/pi-tui/src/tui.ts +20 -8
  165. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  166. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  167. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  168. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  169. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  170. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  171. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  172. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  173. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  174. package/src/resources/extensions/gsd/auto.ts +80 -3
  175. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  176. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  177. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  178. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  179. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  180. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  181. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  182. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  183. package/src/resources/extensions/gsd/git-service.ts +46 -8
  184. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  185. package/src/resources/extensions/gsd/guided-flow.ts +71 -10
  186. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  187. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  188. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  189. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  190. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  191. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  192. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  193. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  194. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  195. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  196. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  197. package/src/resources/extensions/gsd/quick.ts +37 -2
  198. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  199. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  200. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  201. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  202. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  203. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  204. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  205. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  206. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  207. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  208. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  209. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  210. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  211. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  212. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  213. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  214. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  215. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  216. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  217. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  218. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  219. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  220. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
  221. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  222. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  223. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  224. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  225. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  226. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +37 -0
  227. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  228. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +63 -1
  229. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  230. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  231. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  232. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  233. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  234. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  235. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  236. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  237. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  238. package/src/resources/extensions/gsd/unit-runtime.ts +25 -0
  239. package/src/resources/extensions/gsd/worktree-resolver.ts +36 -15
  240. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → tht0ltP8EIjsohPOA_WZf}/_buildManifest.js +0 -0
  241. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → tht0ltP8EIjsohPOA_WZf}/_ssgManifest.js +0 -0
@@ -5,9 +5,11 @@ import {
5
5
  clearUnitRuntimeRecord,
6
6
  formatExecuteTaskRecoveryStatus,
7
7
  inspectExecuteTaskDurability,
8
+ isInFlightRuntimePhase,
8
9
  readUnitRuntimeRecord,
9
10
  writeUnitRuntimeRecord,
10
11
  } from "../unit-runtime.ts";
12
+ import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
11
13
  import { clearPathCache } from '../paths.ts';
12
14
  import { test } from 'node:test';
13
15
  import assert from 'node:assert/strict';
@@ -22,6 +24,12 @@ writeFileSync(
22
24
  "utf-8",
23
25
  );
24
26
 
27
+ console.log("\n=== in-flight runtime phases ===");
28
+ {
29
+ assert.equal(isInFlightRuntimePhase("crashed"), true, "crashed records remain recoverable");
30
+ assert.equal(isInFlightRuntimePhase("finalized"), false, "finalized records are terminal");
31
+ }
32
+
25
33
  console.log("\n=== runtime record write/read/update ===");
26
34
  {
27
35
  const first = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "dispatched" });
@@ -65,6 +73,35 @@ console.log("\n=== runtime record cleanup ===");
65
73
  assert.deepStrictEqual(loaded, null, "record removed");
66
74
  }
67
75
 
76
+ console.log("\n=== execute-task durability trusts closed DB task status ===");
77
+ {
78
+ const dbBase = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-db-test-"));
79
+ mkdirSync(join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "tasks"), { recursive: true });
80
+ try {
81
+ openDatabase(join(dbBase, ".gsd", "gsd.db"));
82
+ insertMilestone({ id: "M300", title: "DB Milestone", status: "active" });
83
+ insertSlice({ id: "S01", milestoneId: "M300", title: "DB Slice", status: "in_progress" });
84
+ insertTask({ id: "T01", milestoneId: "M300", sliceId: "S01", title: "DB Task", status: "complete" });
85
+ writeFileSync(
86
+ join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "S01-PLAN.md"),
87
+ "# S01\n\n## Tasks\n\n- [ ] **T01: DB Task** `est:10m`\n",
88
+ "utf-8",
89
+ );
90
+ writeFileSync(join(dbBase, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: DB task\n", "utf-8");
91
+
92
+ const status = await inspectExecuteTaskDurability(dbBase, "M300/S01/T01");
93
+ assert.ok(status !== null, "db-complete: status exists");
94
+ assert.equal(status!.dbComplete, true, "db-complete: closed DB status is captured");
95
+ assert.equal(status!.summaryExists, false, "db-complete: summary can still be missing");
96
+ assert.equal(status!.taskChecked, false, "db-complete: checkbox can still be unchecked");
97
+ assert.equal(status!.nextActionAdvanced, false, "db-complete: next action can still point at task");
98
+ assert.equal(formatExecuteTaskRecoveryStatus(status!), "DB task status is closed");
99
+ } finally {
100
+ closeDatabase();
101
+ rmSync(dbBase, { recursive: true, force: true });
102
+ }
103
+ }
104
+
68
105
  console.log("\n=== hook unit type sanitization (slash in unitType) ===");
69
106
  {
70
107
  // Hook units have unitType like "hook/code-review" with a slash
@@ -165,6 +165,9 @@ test("executeMilestoneStatus returns milestone metadata and slice counts", async
165
165
  assert.equal(parsed.sliceCount, 1);
166
166
  assert.equal(parsed.slices[0].id, "S01");
167
167
  assert.equal(parsed.slices[0].taskCounts.pending, 1);
168
+ assert.equal(result.details.status, "active");
169
+ assert.equal(result.details.title, "Milestone One");
170
+ assert.deepEqual(result.details.slices, parsed.slices);
168
171
  } finally {
169
172
  closeDatabase();
170
173
  cleanup(base);
@@ -1,3 +1,5 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: WorktreeResolver unit and regression tests.
1
3
  import test from "node:test";
2
4
  import assert from "node:assert/strict";
3
5
  import { mkdtempSync, rmSync, mkdirSync, realpathSync } from "node:fs";
@@ -9,6 +11,17 @@ import {
9
11
  type NotifyCtx,
10
12
  } from "../worktree-resolver.js";
11
13
  import { AutoSession } from "../auto/session.js";
14
+ import {
15
+ closeDatabase,
16
+ insertMilestone,
17
+ openDatabase,
18
+ } from "../gsd-db.js";
19
+ import { registerAutoWorker } from "../db/auto-workers.js";
20
+ import {
21
+ claimMilestoneLease,
22
+ getMilestoneLease,
23
+ releaseMilestoneLease,
24
+ } from "../db/milestone-leases.js";
12
25
 
13
26
  // ─── Helpers ─────────────────────────────────────────────────────────────────
14
27
 
@@ -19,11 +32,12 @@ interface CallLog {
19
32
  }
20
33
 
21
34
  function makeSession(
22
- overrides?: Partial<{ basePath: string; originalBasePath: string }>,
35
+ overrides?: Partial<AutoSession>,
23
36
  ): AutoSession {
24
37
  const s = new AutoSession();
25
38
  s.basePath = overrides?.basePath ?? "/project";
26
39
  s.originalBasePath = overrides?.originalBasePath ?? "/project";
40
+ Object.assign(s, overrides);
27
41
  return s;
28
42
  }
29
43
 
@@ -182,6 +196,17 @@ function findCalls(calls: CallLog[], fn: string): CallLog[] {
182
196
  return calls.filter((c) => c.fn === fn);
183
197
  }
184
198
 
199
+ function makeDbBase(): string {
200
+ const base = mkdtempSync(join(tmpdir(), "gsd-worktree-resolver-"));
201
+ mkdirSync(join(base, ".gsd"), { recursive: true });
202
+ return base;
203
+ }
204
+
205
+ function cleanupDbBase(base: string): void {
206
+ try { closeDatabase(); } catch { /* noop */ }
207
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
208
+ }
209
+
185
210
  // ─── Getter Tests ────────────────────────────────────────────────────────────
186
211
 
187
212
  test("workPath returns s.basePath", () => {
@@ -371,6 +396,43 @@ test("enterMilestone does not create double-nested worktree when originalBasePat
371
396
  );
372
397
  });
373
398
 
399
+ test("enterMilestone reacquires a released same-milestone lease before worktree entry", (t) => {
400
+ const base = makeDbBase();
401
+ t.after(() => cleanupDbBase(base));
402
+ openDatabase(join(base, ".gsd", "gsd.db"));
403
+ insertMilestone({ id: "M001", title: "Test milestone", status: "active" });
404
+
405
+ const workerId = registerAutoWorker({ projectRootRealpath: base });
406
+ const originalClaim = claimMilestoneLease(workerId, "M001");
407
+ assert.equal(originalClaim.ok, true);
408
+ if (!originalClaim.ok) throw new Error("expected test lease claim");
409
+ assert.equal(releaseMilestoneLease(workerId, "M001", originalClaim.token), true);
410
+
411
+ const s = makeSession({
412
+ basePath: base,
413
+ originalBasePath: base,
414
+ workerId,
415
+ currentMilestoneId: "M001",
416
+ milestoneLeaseToken: originalClaim.token,
417
+ });
418
+ const deps = makeDeps({
419
+ createAutoWorktree: (basePath: string, milestoneId: string) => join(basePath, ".gsd", "worktrees", milestoneId),
420
+ });
421
+ const ctx = makeNotifyCtx();
422
+ const resolver = new WorktreeResolver(s, deps);
423
+
424
+ resolver.enterMilestone("M001", ctx);
425
+
426
+ const row = getMilestoneLease("M001");
427
+ assert.ok(row);
428
+ assert.equal(row.worker_id, workerId);
429
+ assert.equal(row.status, "held");
430
+ assert.equal(row.fencing_token, originalClaim.token + 1);
431
+ assert.equal(s.milestoneLeaseToken, originalClaim.token + 1);
432
+ assert.equal(s.basePath, join(base, ".gsd", "worktrees", "M001"));
433
+ assert.equal(ctx.messages.some((m) => m.level === "error"), false);
434
+ });
435
+
374
436
  // ─── enterMilestone Tests (branch mode) ──────────────────────────────────────
375
437
 
376
438
  test("enterMilestone in branch mode calls enterBranchModeForMilestone and rebuilds GitService", () => {
@@ -0,0 +1,25 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Shared Context Mode tool result helpers.
3
+
4
+ export interface ToolExecutionResult {
5
+ content: Array<{ type: "text"; text: string }>;
6
+ details: Record<string, unknown>;
7
+ isError?: boolean;
8
+ }
9
+
10
+ export type ContextModeToolName = "gsd_exec" | "gsd_exec_search" | "gsd_resume";
11
+
12
+ export function contextModeDisabledResult(operation: ContextModeToolName): ToolExecutionResult {
13
+ return {
14
+ content: [
15
+ {
16
+ type: "text",
17
+ text:
18
+ `${operation} is disabled by \`context_mode.enabled: false\` in preferences. ` +
19
+ "Remove that override or set it to true to re-enable Context Mode tools.",
20
+ },
21
+ ],
22
+ details: { operation, error: "context_mode_disabled" },
23
+ isError: true,
24
+ };
25
+ }
@@ -4,6 +4,8 @@
4
4
  // re-discover past runs without re-executing. Read-only; no DB writes.
5
5
 
6
6
  import { searchExecHistory, type ExecSearchOptions } from "../exec-history.js";
7
+ import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
8
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
7
9
 
8
10
  export interface ExecSearchToolParams {
9
11
  query?: string;
@@ -12,16 +14,14 @@ export interface ExecSearchToolParams {
12
14
  limit?: number;
13
15
  }
14
16
 
15
- export interface ToolExecutionResult {
16
- content: Array<{ type: "text"; text: string }>;
17
- details: Record<string, unknown>;
18
- isError?: boolean;
19
- }
20
-
21
17
  export function executeExecSearch(
22
18
  params: ExecSearchToolParams,
23
- opts: { baseDir: string },
19
+ opts: { baseDir: string; preferences?: { context_mode?: ContextModeConfig } | null },
24
20
  ): ToolExecutionResult {
21
+ if (!isContextModeEnabled(opts.preferences)) {
22
+ return contextModeDisabledResult("gsd_exec_search");
23
+ }
24
+
25
25
  const searchOpts: ExecSearchOptions = {
26
26
  query: typeof params.query === "string" ? params.query : undefined,
27
27
  runtime: params.runtime,
@@ -12,6 +12,7 @@ import {
12
12
  type ExecSandboxResult,
13
13
  } from "../exec-sandbox.js";
14
14
  import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
15
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
15
16
 
16
17
  export interface ExecToolParams {
17
18
  runtime: ExecSandboxRequest["runtime"];
@@ -20,17 +21,12 @@ export interface ExecToolParams {
20
21
  timeout_ms?: number;
21
22
  }
22
23
 
23
- export interface ToolExecutionResult {
24
- content: Array<{ type: "text"; text: string }>;
25
- details: Record<string, unknown>;
26
- isError?: boolean;
27
- }
28
-
29
24
  export interface ExecToolDeps {
30
25
  baseDir: string;
31
26
  preferences: { context_mode?: ContextModeConfig } | null;
32
27
  /** Optional override for testing. */
33
28
  run?: (req: ExecSandboxRequest, opts: ExecSandboxOptions) => Promise<ExecSandboxResult>;
29
+ env?: NodeJS.ProcessEnv;
34
30
  now?: () => Date;
35
31
  generateId?: () => string;
36
32
  }
@@ -77,21 +73,6 @@ function isEnabled(prefs: ExecToolDeps["preferences"]): boolean {
77
73
  return isContextModeEnabled(prefs);
78
74
  }
79
75
 
80
- function disabledResult(): ToolExecutionResult {
81
- return {
82
- content: [
83
- {
84
- type: "text",
85
- text:
86
- "gsd_exec is disabled by `context_mode.enabled: false` in preferences. Remove that " +
87
- "override (or set it to true) to re-enable sandboxed tool-output execution.",
88
- },
89
- ],
90
- details: { operation: "gsd_exec", error: "context_mode_disabled" },
91
- isError: true,
92
- };
93
- }
94
-
95
76
  function paramError(message: string): ToolExecutionResult {
96
77
  return {
97
78
  content: [{ type: "text", text: `Error: ${message}` }],
@@ -104,7 +85,7 @@ export async function executeGsdExec(
104
85
  params: ExecToolParams,
105
86
  deps: ExecToolDeps,
106
87
  ): Promise<ToolExecutionResult> {
107
- if (!isEnabled(deps.preferences)) return disabledResult();
88
+ if (!isEnabled(deps.preferences)) return contextModeDisabledResult("gsd_exec");
108
89
 
109
90
  const runtime = params.runtime;
110
91
  if (runtime !== "bash" && runtime !== "node" && runtime !== "python") {
@@ -121,7 +102,7 @@ export async function executeGsdExec(
121
102
  const opts = buildExecOptions(
122
103
  deps.baseDir,
123
104
  deps.preferences?.context_mode,
124
- { now: deps.now, generateId: deps.generateId },
105
+ { env: deps.env, now: deps.now, generateId: deps.generateId },
125
106
  );
126
107
  const run = deps.run ?? runExecSandbox;
127
108
 
@@ -308,6 +308,7 @@ function includeSupersededMemories(rankedActive: Memory[]): Memory[] {
308
308
  scope: (row["scope"] as string) ?? "project",
309
309
  tags,
310
310
  structured_fields: structuredFields,
311
+ last_hit_at: (row["last_hit_at"] as string | null) ?? null,
311
312
  };
312
313
  });
313
314
  } catch {
@@ -18,6 +18,7 @@ import { renderAllProjections } from "../workflow-projections.js";
18
18
  import { writeManifest } from "../workflow-manifest.js";
19
19
  import { appendEvent } from "../workflow-events.js";
20
20
  import { logWarning } from "../workflow-logger.js";
21
+ import { validatePlanningPathScope } from "../planning-path-scope.js";
21
22
 
22
23
  export interface PlanSliceTaskInput {
23
24
  taskId: string;
@@ -141,6 +142,18 @@ export async function handlePlanSlice(
141
142
  return { error: `validation failed: ${(err as Error).message}` };
142
143
  }
143
144
 
145
+ const pathScopeError = validatePlanningPathScope(
146
+ basePath,
147
+ params.tasks.flatMap((task, index) => [
148
+ { field: `tasks[${index}].files`, values: task.files },
149
+ { field: `tasks[${index}].inputs`, values: task.inputs },
150
+ { field: `tasks[${index}].expectedOutput`, values: task.expectedOutput },
151
+ ]),
152
+ );
153
+ if (pathScopeError) {
154
+ return { error: `validation failed: ${pathScopeError}` };
155
+ }
156
+
144
157
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
145
158
  // Guards must be inside the transaction so the state they check cannot
146
159
  // change between the read and the write (#2723).
@@ -8,6 +8,7 @@ import { renderAllProjections } from "../workflow-projections.js";
8
8
  import { writeManifest } from "../workflow-manifest.js";
9
9
  import { appendEvent } from "../workflow-events.js";
10
10
  import { logWarning } from "../workflow-logger.js";
11
+ import { validatePlanningPathScope } from "../planning-path-scope.js";
11
12
 
12
13
  export interface PlanTaskParams {
13
14
  milestoneId: string;
@@ -66,6 +67,15 @@ export async function handlePlanTask(
66
67
  return { error: `validation failed: ${(err as Error).message}` };
67
68
  }
68
69
 
70
+ const pathScopeError = validatePlanningPathScope(basePath, [
71
+ { field: "files", values: params.files },
72
+ { field: "inputs", values: params.inputs },
73
+ { field: "expectedOutput", values: params.expectedOutput },
74
+ ]);
75
+ if (pathScopeError) {
76
+ return { error: `validation failed: ${pathScopeError}` };
77
+ }
78
+
69
79
  // ── Guards + DB writes inside a single transaction (prevents TOCTOU) ───
70
80
  // Guards must be inside the transaction so the state they check cannot
71
81
  // change between the read and the write (#2723).
@@ -3,22 +3,22 @@
3
3
  // re-deriving project memory state.
4
4
 
5
5
  import { readCompactionSnapshot } from "../compaction-snapshot.js";
6
+ import { isContextModeEnabled, type ContextModeConfig } from "../preferences-types.js";
7
+ import { contextModeDisabledResult, type ToolExecutionResult } from "./context-mode-tool-result.js";
6
8
 
7
9
  export interface ResumeToolParams {
8
10
  /** Ignored — reserved for future variant (e.g. dated snapshots). */
9
11
  _variant?: string;
10
12
  }
11
13
 
12
- export interface ToolExecutionResult {
13
- content: Array<{ type: "text"; text: string }>;
14
- details: Record<string, unknown>;
15
- isError?: boolean;
16
- }
17
-
18
14
  export function executeResume(
19
15
  _params: ResumeToolParams,
20
- opts: { baseDir: string },
16
+ opts: { baseDir: string; preferences?: { context_mode?: ContextModeConfig } | null },
21
17
  ): ToolExecutionResult {
18
+ if (!isContextModeEnabled(opts.preferences)) {
19
+ return contextModeDisabledResult("gsd_resume");
20
+ }
21
+
22
22
  const snapshot = readCompactionSnapshot(opts.baseDir);
23
23
  if (snapshot == null) {
24
24
  return {
@@ -813,7 +813,7 @@ export async function executeMilestoneStatus(
813
813
 
814
814
  return {
815
815
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
816
- details: { operation: "milestone_status", milestoneId: milestone.id, sliceCount: slices.length },
816
+ details: { operation: "milestone_status", ...result },
817
817
  };
818
818
  });
819
819
  } catch (err) {
@@ -112,8 +112,22 @@ const CONTEXT_MODE_LANE_LABELS: Record<Exclude<ContextModePolicy, "none">, strin
112
112
  docs: "documentation",
113
113
  };
114
114
 
115
- const CONTEXT_MODE_GUIDANCE =
116
- "Use `gsd_exec` for noisy commands, `gsd_exec_search` before reruns, and `gsd_resume` after compaction or resume.";
115
+ const CONTEXT_MODE_GUIDANCE_BY_LANE: Record<Exclude<ContextModePolicy, "none">, string> = {
116
+ interview:
117
+ "Use `gsd_resume` to restore prior discussion, `gsd_exec` for noisy discovery, and `gsd_exec_search` before repeating scans.",
118
+ research:
119
+ "Use `gsd_exec` for noisy research scans, `gsd_exec_search` before reruns, and `gsd_resume` to restore prior findings.",
120
+ planning:
121
+ "Use `gsd_resume` for planning continuity, `gsd_exec` for noisy checks, and `gsd_exec_search` before rerunning diagnostics.",
122
+ execution:
123
+ "Use `gsd_exec` for builds, tests, and diagnostics, `gsd_exec_search` before reruns, and `gsd_resume` after compaction or resume.",
124
+ verification:
125
+ "Use `gsd_exec` for verification commands, `gsd_exec_search` to reuse prior evidence, and `gsd_resume` after compaction or resume.",
126
+ orchestration:
127
+ "Use `gsd_resume` before resuming orchestration, `gsd_exec_search` to reuse prior runs, and `gsd_exec` for noisy coordination checks.",
128
+ docs:
129
+ "Use `gsd_resume` for prior context, `gsd_exec_search` for saved evidence, and `gsd_exec` for noisy doc validation commands.",
130
+ };
117
131
 
118
132
  /**
119
133
  * Render the Context Mode instruction lane for a unit type. Unknown unit
@@ -129,15 +143,16 @@ export function composeContextModeInstructions(
129
143
  if (!manifest || manifest.contextMode === "none") return "";
130
144
 
131
145
  const lane = CONTEXT_MODE_LANE_LABELS[manifest.contextMode];
146
+ const guidance = CONTEXT_MODE_GUIDANCE_BY_LANE[manifest.contextMode];
132
147
  if (opts.renderMode === "nested") {
133
- return `Context Mode (${lane} lane): ${CONTEXT_MODE_GUIDANCE}`;
148
+ return `Context Mode (${lane} lane): ${guidance}`;
134
149
  }
135
150
 
136
151
  return [
137
152
  "## Context Mode",
138
153
  "",
139
154
  `Lane: **${lane} lane**.`,
140
- CONTEXT_MODE_GUIDANCE,
155
+ guidance,
141
156
  ].join("\n");
142
157
  }
143
158
 
@@ -10,6 +10,8 @@ import {
10
10
  } from "./paths.js";
11
11
  import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
12
12
  import { parseUnitId } from "./unit-id.js";
13
+ import { getTask, isDbAvailable, refreshOpenDatabaseFromDisk } from "./gsd-db.js";
14
+ import { isClosedStatus } from "./status-guards.js";
13
15
 
14
16
  // Per-record advisory lock — prevents read-modify-write races between
15
17
  // concurrent writers updating disjoint fields of the same runtime record.
@@ -67,17 +69,32 @@ export type UnitRuntimePhase =
67
69
  | "wrapup-warning-sent"
68
70
  | "timeout"
69
71
  | "finalize-timeout"
72
+ | "crashed"
70
73
  | "recovered"
71
74
  | "finalized"
72
75
  | "paused"
73
76
  | "skipped";
74
77
 
78
+ export const IN_FLIGHT_RUNTIME_PHASES: ReadonlySet<UnitRuntimePhase> = new Set([
79
+ "dispatched",
80
+ "wrapup-warning-sent",
81
+ "timeout",
82
+ "finalize-timeout",
83
+ "crashed",
84
+ "paused",
85
+ ]);
86
+
87
+ export function isInFlightRuntimePhase(phase: UnitRuntimePhase): boolean {
88
+ return IN_FLIGHT_RUNTIME_PHASES.has(phase);
89
+ }
90
+
75
91
  export interface ExecuteTaskRecoveryStatus {
76
92
  planPath: string;
77
93
  summaryPath: string;
78
94
  summaryExists: boolean;
79
95
  taskChecked: boolean;
80
96
  nextActionAdvanced: boolean;
97
+ dbComplete: boolean;
81
98
  mustHaveCount: number;
82
99
  mustHavesMentionedInSummary: number;
83
100
  }
@@ -199,6 +216,12 @@ export async function inspectExecuteTaskDurability(
199
216
  const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
200
217
  const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent);
201
218
  const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test(stateContent);
219
+ let dbComplete = false;
220
+ if (isDbAvailable()) {
221
+ refreshOpenDatabaseFromDisk();
222
+ const task = getTask(mid, sid, tid);
223
+ dbComplete = !!task && isClosedStatus(task.status);
224
+ }
202
225
 
203
226
  // Must-have coverage: load task plan and count mentions in summary
204
227
  let mustHaveCount = 0;
@@ -225,12 +248,14 @@ export async function inspectExecuteTaskDurability(
225
248
  summaryExists,
226
249
  taskChecked,
227
250
  nextActionAdvanced,
251
+ dbComplete,
228
252
  mustHaveCount,
229
253
  mustHavesMentionedInSummary,
230
254
  };
231
255
  }
232
256
 
233
257
  export function formatExecuteTaskRecoveryStatus(status: ExecuteTaskRecoveryStatus): string {
258
+ if (status.dbComplete) return "DB task status is closed";
234
259
  const missing = [] as string[];
235
260
  if (!status.summaryExists) missing.push(`summary missing (${status.summaryPath})`);
236
261
  if (!status.taskChecked) missing.push(`task checkbox unchecked in ${status.planPath}`);
@@ -25,7 +25,7 @@ import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js
25
25
  import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
26
26
  import { loadEffectiveGSDPreferences } from "./preferences.js";
27
27
  import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
28
- import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
28
+ import { claimMilestoneLease, refreshMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
29
29
 
30
30
  // ─── Path Comparison Helper ────────────────────────────────────────────────
31
31
  /**
@@ -207,23 +207,44 @@ export class WorktreeResolver {
207
207
  // milestone (re-entry within the same session).
208
208
  if (this.s.workerId) {
209
209
  if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
210
- // Already held — no-op, the heartbeat in loop.ts refreshes TTL.
211
- } else {
212
- // If we held a different milestone, release it first so other
213
- // workers don't have to wait for TTL.
214
- if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
215
- try {
216
- releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
217
- } catch (err) {
218
- debugLog("WorktreeResolver", {
219
- action: "enterMilestone",
220
- milestoneId,
221
- releasePriorLeaseError: err instanceof Error ? err.message : String(err),
222
- });
223
- }
210
+ const refreshed = refreshMilestoneLease(
211
+ this.s.workerId,
212
+ milestoneId,
213
+ this.s.milestoneLeaseToken,
214
+ );
215
+ if (refreshed) {
216
+ debugLog("WorktreeResolver", {
217
+ action: "enterMilestone",
218
+ milestoneId,
219
+ leaseRefreshed: true,
220
+ fencingToken: this.s.milestoneLeaseToken,
221
+ });
222
+ } else {
223
+ debugLog("WorktreeResolver", {
224
+ action: "enterMilestone",
225
+ milestoneId,
226
+ staleLeaseToken: this.s.milestoneLeaseToken,
227
+ });
224
228
  this.s.milestoneLeaseToken = null;
225
229
  }
230
+ }
231
+
232
+ // If we held a different milestone, release it first so other
233
+ // workers don't have to wait for TTL.
234
+ if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
235
+ try {
236
+ releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
237
+ } catch (err) {
238
+ debugLog("WorktreeResolver", {
239
+ action: "enterMilestone",
240
+ milestoneId,
241
+ releasePriorLeaseError: err instanceof Error ? err.message : String(err),
242
+ });
243
+ }
244
+ this.s.milestoneLeaseToken = null;
245
+ }
226
246
 
247
+ if (this.s.milestoneLeaseToken === null) {
227
248
  try {
228
249
  const claim = claimMilestoneLease(this.s.workerId, milestoneId);
229
250
  if (claim.ok) {