@opengsd/gsd-pi 1.2.0-dev.9ad8ae33 → 1.2.0-dev.a6376d75

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 (264) hide show
  1. package/dist/cli-model-override.d.ts +15 -0
  2. package/dist/cli-model-override.js +21 -0
  3. package/dist/cli.js +1 -18
  4. package/dist/loader.js +6 -4
  5. package/dist/register-agent-bundles.d.ts +11 -2
  6. package/dist/register-agent-bundles.js +18 -4
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/ask-user-questions.js +3 -2
  9. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +447 -215
  10. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +33 -1
  11. package/dist/resources/extensions/gsd/auto/closeout.js +215 -0
  12. package/dist/resources/extensions/gsd/auto/dispatch-history.js +21 -6
  13. package/dist/resources/extensions/gsd/auto/dispatch.js +365 -0
  14. package/dist/resources/extensions/gsd/auto/finalize.js +347 -0
  15. package/dist/resources/extensions/gsd/auto/loop.js +4 -1
  16. package/dist/resources/extensions/gsd/auto/milestone-lease-reclaim.js +56 -0
  17. package/dist/resources/extensions/gsd/auto/orchestrator.js +85 -15
  18. package/dist/resources/extensions/gsd/auto/phase-helpers.js +146 -0
  19. package/dist/resources/extensions/gsd/auto/phases.js +17 -2372
  20. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +534 -0
  21. package/dist/resources/extensions/gsd/auto/unit-phase.js +694 -0
  22. package/dist/resources/extensions/gsd/auto/workflow-unit-dispatch.js +1 -1
  23. package/dist/resources/extensions/gsd/auto/worktree-safety-phase.js +125 -0
  24. package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
  25. package/dist/resources/extensions/gsd/auto.js +15 -1
  26. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +37 -7
  27. package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -2
  28. package/dist/resources/extensions/gsd/commands-workflow-templates.js +9 -2
  29. package/dist/resources/extensions/gsd/db/queries.js +30 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +256 -125
  31. package/dist/resources/extensions/gsd/guided-flow.js +88 -2
  32. package/dist/resources/extensions/gsd/health-widget.js +87 -28
  33. package/dist/resources/extensions/gsd/mcp-bridge.js +10 -0
  34. package/dist/resources/extensions/gsd/milestone-settlement.js +2 -2
  35. package/dist/resources/extensions/gsd/notifications.js +12 -7
  36. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -1
  38. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  39. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -1
  40. package/dist/resources/extensions/gsd/skill-activation.js +3 -6
  41. package/dist/resources/extensions/gsd/state.js +6 -2
  42. package/dist/resources/extensions/gsd/tool-surface-readiness.js +83 -31
  43. package/dist/resources/extensions/gsd/tools/complete-task.js +62 -0
  44. package/dist/resources/extensions/gsd/unit-context-composer.js +1 -1
  45. package/dist/resources/extensions/gsd/unit-registry.js +34 -4
  46. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +2 -0
  47. package/dist/resources/extensions/gsd/workflow-mcp-readiness-cache.js +105 -0
  48. package/dist/resources/extensions/gsd/worktree-safety.js +28 -26
  49. package/dist/resources/extensions/mcp-client/manager.js +6 -1
  50. package/dist/runtime-checks.d.ts +10 -0
  51. package/dist/runtime-checks.js +27 -0
  52. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  53. package/dist/web/standalone/.next/BUILD_ID +1 -1
  54. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  55. package/dist/web/standalone/.next/build-manifest.json +2 -2
  56. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  57. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.html +1 -1
  74. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  81. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  83. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  84. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  85. package/package.json +2 -2
  86. package/packages/cloud-mcp-gateway/package.json +2 -2
  87. package/packages/contracts/package.json +1 -1
  88. package/packages/daemon/package.json +4 -4
  89. package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
  90. package/packages/gsd-agent-core/dist/sdk.js +6 -4
  91. package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
  92. package/packages/gsd-agent-core/package.json +5 -5
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +8 -0
  98. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +50 -6
  100. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +2 -0
  102. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +34 -5
  104. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts +1 -0
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +12 -0
  108. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
  110. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +4 -0
  111. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
  112. package/packages/gsd-agent-modes/package.json +7 -7
  113. package/packages/mcp-server/README.md +12 -3
  114. package/packages/mcp-server/dist/cli-runner.d.ts +40 -0
  115. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -0
  116. package/packages/mcp-server/dist/cli-runner.js +137 -0
  117. package/packages/mcp-server/dist/cli-runner.js.map +1 -0
  118. package/packages/mcp-server/dist/cli.js +2 -58
  119. package/packages/mcp-server/dist/cli.js.map +1 -1
  120. package/packages/mcp-server/dist/pid-registry.d.ts +46 -0
  121. package/packages/mcp-server/dist/pid-registry.d.ts.map +1 -0
  122. package/packages/mcp-server/dist/pid-registry.js +452 -0
  123. package/packages/mcp-server/dist/pid-registry.js.map +1 -0
  124. package/packages/mcp-server/dist/probe-mode.d.ts +4 -0
  125. package/packages/mcp-server/dist/probe-mode.d.ts.map +1 -0
  126. package/packages/mcp-server/dist/probe-mode.js +10 -0
  127. package/packages/mcp-server/dist/probe-mode.js.map +1 -0
  128. package/packages/mcp-server/dist/stdio-watchdog.d.ts +8 -0
  129. package/packages/mcp-server/dist/stdio-watchdog.d.ts.map +1 -0
  130. package/packages/mcp-server/dist/stdio-watchdog.js +40 -0
  131. package/packages/mcp-server/dist/stdio-watchdog.js.map +1 -0
  132. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  133. package/packages/mcp-server/dist/workflow-tools.js +62 -43
  134. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  135. package/packages/mcp-server/package.json +5 -5
  136. package/packages/native/package.json +1 -1
  137. package/packages/pi-agent-core/dist/agent-loop.js +43 -2
  138. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  139. package/packages/pi-agent-core/package.json +1 -1
  140. package/packages/pi-ai/package.json +1 -1
  141. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  142. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  144. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  145. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/theme/theme.js +45 -17
  147. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  148. package/packages/pi-coding-agent/package.json +7 -7
  149. package/packages/pi-tui/dist/index.d.ts +1 -1
  150. package/packages/pi-tui/dist/index.d.ts.map +1 -1
  151. package/packages/pi-tui/dist/index.js +1 -1
  152. package/packages/pi-tui/dist/index.js.map +1 -1
  153. package/packages/pi-tui/dist/terminal-image.d.ts +33 -0
  154. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  155. package/packages/pi-tui/dist/terminal-image.js +54 -2
  156. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  157. package/packages/pi-tui/dist/tui.d.ts +8 -0
  158. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  159. package/packages/pi-tui/dist/tui.js +63 -18
  160. package/packages/pi-tui/dist/tui.js.map +1 -1
  161. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  162. package/packages/pi-tui/dist/utils.js +110 -36
  163. package/packages/pi-tui/dist/utils.js.map +1 -1
  164. package/packages/pi-tui/package.json +2 -2
  165. package/packages/rpc-client/package.json +2 -2
  166. package/pkg/dist/theme/theme.d.ts.map +1 -1
  167. package/pkg/dist/theme/theme.js +45 -17
  168. package/pkg/dist/theme/theme.js.map +1 -1
  169. package/pkg/package.json +1 -1
  170. package/src/resources/extensions/ask-user-questions.ts +7 -2
  171. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +531 -226
  172. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +672 -7
  173. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +38 -1
  174. package/src/resources/extensions/gsd/auto/closeout.ts +309 -0
  175. package/src/resources/extensions/gsd/auto/dispatch-history.ts +22 -6
  176. package/src/resources/extensions/gsd/auto/dispatch.ts +449 -0
  177. package/src/resources/extensions/gsd/auto/finalize.ts +445 -0
  178. package/src/resources/extensions/gsd/auto/loop.ts +4 -1
  179. package/src/resources/extensions/gsd/auto/milestone-lease-reclaim.ts +74 -0
  180. package/src/resources/extensions/gsd/auto/orchestrator.ts +95 -15
  181. package/src/resources/extensions/gsd/auto/phase-helpers.ts +199 -0
  182. package/src/resources/extensions/gsd/auto/phases.ts +58 -3061
  183. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +704 -0
  184. package/src/resources/extensions/gsd/auto/unit-phase.ts +910 -0
  185. package/src/resources/extensions/gsd/auto/workflow-unit-dispatch.ts +1 -1
  186. package/src/resources/extensions/gsd/auto/worktree-safety-phase.ts +149 -0
  187. package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
  188. package/src/resources/extensions/gsd/auto.ts +20 -1
  189. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +56 -6
  190. package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -2
  191. package/src/resources/extensions/gsd/commands-workflow-templates.ts +11 -4
  192. package/src/resources/extensions/gsd/db/queries.ts +29 -0
  193. package/src/resources/extensions/gsd/doctor-environment.ts +267 -142
  194. package/src/resources/extensions/gsd/guided-flow.ts +128 -2
  195. package/src/resources/extensions/gsd/health-widget.ts +91 -27
  196. package/src/resources/extensions/gsd/mcp-bridge.ts +39 -0
  197. package/src/resources/extensions/gsd/milestone-settlement.ts +2 -2
  198. package/src/resources/extensions/gsd/notifications.ts +13 -6
  199. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  200. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -1
  201. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  202. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -1
  203. package/src/resources/extensions/gsd/skill-activation.ts +3 -6
  204. package/src/resources/extensions/gsd/state.ts +7 -1
  205. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +1 -1
  206. package/src/resources/extensions/gsd/tests/auto-blocked-remediation-message.test.ts +1 -1
  207. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +206 -22
  208. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +76 -12
  209. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +1 -1
  210. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +77 -1
  211. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +2 -1
  212. package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +169 -1
  213. package/src/resources/extensions/gsd/tests/complete-task.test.ts +141 -5
  214. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -1
  215. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +36 -0
  216. package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +55 -0
  217. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +8 -0
  218. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +117 -91
  219. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +113 -0
  220. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +16 -0
  221. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +15 -0
  222. package/src/resources/extensions/gsd/tests/integration/doctor-environment-async.test.ts +104 -0
  223. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +18 -0
  224. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +47 -16
  225. package/src/resources/extensions/gsd/tests/mcp-readiness-preflight.test.ts +205 -0
  226. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +6 -5
  227. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +1 -1
  228. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +1 -1
  229. package/src/resources/extensions/gsd/tests/milestone-settlement.test.ts +92 -0
  230. package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +1 -1
  231. package/src/resources/extensions/gsd/tests/notifications.test.ts +64 -9
  232. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +2 -2
  233. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +5 -0
  234. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +1 -1
  235. package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +1 -1
  236. package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +3 -3
  237. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -2
  238. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +2 -4
  239. package/src/resources/extensions/gsd/tests/remote-notification-from-desktop.test.ts +31 -81
  240. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +7 -1
  241. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -17
  242. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +7 -3
  243. package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +1 -1
  244. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -2
  245. package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +184 -10
  246. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
  247. package/src/resources/extensions/gsd/tests/workflow-mcp-readiness-cache.test.ts +119 -0
  248. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +65 -2
  249. package/src/resources/extensions/gsd/tests/workflow-phase-contract-matrix.test.ts +332 -0
  250. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +92 -0
  251. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +1 -1
  252. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +1 -1
  253. package/src/resources/extensions/gsd/tests/worktree-safety-phase.test.ts +100 -0
  254. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +72 -0
  255. package/src/resources/extensions/gsd/tool-surface-readiness.ts +126 -19
  256. package/src/resources/extensions/gsd/tools/complete-task.ts +87 -0
  257. package/src/resources/extensions/gsd/unit-context-composer.ts +1 -1
  258. package/src/resources/extensions/gsd/unit-registry.ts +34 -4
  259. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -0
  260. package/src/resources/extensions/gsd/workflow-mcp-readiness-cache.ts +150 -0
  261. package/src/resources/extensions/gsd/worktree-safety.ts +41 -39
  262. package/src/resources/extensions/mcp-client/manager.ts +7 -1
  263. /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_buildManifest.js +0 -0
  264. /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_ssgManifest.js +0 -0
@@ -3,6 +3,7 @@
3
3
 
4
4
  import test, { mock } from "node:test";
5
5
  import assert from "node:assert/strict";
6
+ import { execSync } from "node:child_process";
6
7
  import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
8
  import { tmpdir } from "node:os";
8
9
  import { join } from "node:path";
@@ -24,7 +25,9 @@ import { runUnit, shouldDeferUnitFailsafeTimeout } from "../auto/run-unit.js";
24
25
  import { scheduleAutoWakeup, _resetAutoWakeupsForTest } from "../auto/schedule-wakeup.js";
25
26
  import { writeUnitRuntimeRecord, readUnitRuntimeRecord } from "../unit-runtime.js";
26
27
  import { autoLoop as rawAutoLoop } from "../auto/loop.js";
27
- import { runPreDispatch, runDispatch, runUnitPhase } from "../auto/phases.js";
28
+ import { runPreDispatch } from "../auto/pre-dispatch.js";
29
+ import { runDispatch } from "../auto/dispatch.js";
30
+ import { runUnitPhase, resetSessionTimeoutState } from "../auto/unit-phase.js";
28
31
  import { detectStuck } from "../auto/detect-stuck.js";
29
32
  import type { UnitResult, AgentEndEvent, LoopState } from "../auto/types.js";
30
33
  import type { LoopDeps } from "../auto/loop-deps.js";
@@ -531,7 +534,7 @@ test("runUnit failsafe defers cancellation while timeout recovery is making fres
531
534
  const ctx = makeMockCtx();
532
535
  const pi = makeMockPi();
533
536
  const s = makeMockSession();
534
- s.basePath = mkdtempSync(join(tmpdir(), "gsd-rununit-recovery-"));
537
+ s.basePath = makeLoopTestBase("gsd-rununit-recovery-");
535
538
  s.currentUnit = { type: "task", id: "T01", startedAt: 1234 };
536
539
 
537
540
  const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
@@ -1366,12 +1369,19 @@ function makeMockDeps(
1366
1369
  * runUnit mock (dispatch counters, milestone state, etc.).
1367
1370
  */
1368
1371
  function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
1372
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-auto-loop-"));
1373
+ // Plan 001 enforces worktree safety for all isolation modes. Loop-mechanics
1374
+ // tests run with getIsolationMode: () => "none", so the project root itself
1375
+ // must be a valid git working tree for source-writing Units to dispatch.
1376
+ execSync("git init --initial-branch=main", { cwd: basePath, stdio: "ignore" });
1377
+ execSync("git config user.email test@test.com", { cwd: basePath, stdio: "ignore" });
1378
+ execSync("git config user.name Test", { cwd: basePath, stdio: "ignore" });
1369
1379
  return {
1370
1380
  active: true,
1371
1381
  verbose: false,
1372
1382
  stepMode: false,
1373
1383
  paused: false,
1374
- basePath: mkdtempSync(join(tmpdir(), "gsd-auto-loop-")),
1384
+ basePath,
1375
1385
  originalBasePath: "",
1376
1386
  currentMilestoneId: "M001",
1377
1387
  currentUnit: null,
@@ -1419,6 +1429,15 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
1419
1429
  } as any;
1420
1430
  }
1421
1431
 
1432
+ /** Create a temp project root suitable for loop-mechanics tests. */
1433
+ function makeLoopTestBase(prefix: string): string {
1434
+ const base = mkdtempSync(join(tmpdir(), prefix));
1435
+ execSync("git init --initial-branch=main", { cwd: base, stdio: "ignore" });
1436
+ execSync("git config user.email test@test.com", { cwd: base, stdio: "ignore" });
1437
+ execSync("git config user.name Test", { cwd: base, stdio: "ignore" });
1438
+ return base;
1439
+ }
1440
+
1422
1441
  test("autoLoop exits when s.active is set to false", async (t) => {
1423
1442
  _resetPendingResolve();
1424
1443
 
@@ -1506,7 +1525,7 @@ test("autoLoop preserves stuck recovery counter when dispatch recovery continues
1506
1525
 
1507
1526
  const ctx = makeMockCtx();
1508
1527
  const pi = makeMockPi();
1509
- const basePath = realpathSync(mkdtempSync(join(tmpdir(), "gsd-stuck-counter-reset-")));
1528
+ const basePath = realpathSync(makeLoopTestBase("gsd-stuck-counter-reset-"));
1510
1529
  mkdirSync(join(basePath, ".gsd"), { recursive: true });
1511
1530
  mkdirSync(join(basePath, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
1512
1531
  writeFileSync(
@@ -1581,7 +1600,7 @@ test("autoLoop skips provider dispatch when execute-task is already complete in
1581
1600
  ctx.ui.setStatus = () => {};
1582
1601
  ctx.ui.setWidget = () => {};
1583
1602
  const pi = makeMockPi();
1584
- const basePath = realpathSync(mkdtempSync(join(tmpdir(), "gsd-already-complete-dispatch-")));
1603
+ const basePath = realpathSync(makeLoopTestBase("gsd-already-complete-dispatch-"));
1585
1604
  mkdirSync(join(basePath, ".gsd"), { recursive: true });
1586
1605
 
1587
1606
  try {
@@ -3992,7 +4011,7 @@ test("resolveAgentEndCancelled with errorContext passes it through to resolved p
3992
4011
  test("runUnitPhase pauses transient aborted cancellations instead of hard-stopping", async (t) => {
3993
4012
  _resetPendingResolve();
3994
4013
 
3995
- const basePath = mkdtempSync(join(tmpdir(), "gsd-aborted-cancel-"));
4014
+ const basePath = makeLoopTestBase("gsd-aborted-cancel-");
3996
4015
  t.after(() => {
3997
4016
  rmSync(basePath, { recursive: true, force: true });
3998
4017
  });
@@ -4064,10 +4083,157 @@ test("runUnitPhase pauses transient aborted cancellations instead of hard-stoppi
4064
4083
  assert.equal(deps.callLog.includes("stopAuto"), false);
4065
4084
  });
4066
4085
 
4086
+ test("resetSessionTimeoutState gives a new auto session a fresh session-creation timeout budget", async (t) => {
4087
+ _resetPendingResolve();
4088
+
4089
+ // runUnitPhase schedules an auto-resume setTimeout on transient session
4090
+ // timeouts. Capture and clear those timers so the test process can exit
4091
+ // promptly while still exercising the real production path.
4092
+ const originalSetTimeout = globalThis.setTimeout;
4093
+ const timerHandles: ReturnType<typeof originalSetTimeout>[] = [];
4094
+ globalThis.setTimeout = ((callback: any, delay?: number, ...args: any[]) => {
4095
+ const handle = originalSetTimeout(callback, delay ?? 0, ...args);
4096
+ timerHandles.push(handle);
4097
+ return handle;
4098
+ }) as any;
4099
+
4100
+ const basePath = makeLoopTestBase("gsd-session-timeout-reset-");
4101
+ execSync("git init", { cwd: basePath });
4102
+ execSync('git -c user.email=test@test.com -c user.name=Test commit --allow-empty -m init', { cwd: basePath });
4103
+
4104
+ t.after(() => {
4105
+ for (const handle of timerHandles) clearTimeout(handle);
4106
+ globalThis.setTimeout = originalSetTimeout;
4107
+ rmSync(basePath, { recursive: true, force: true });
4108
+ });
4109
+
4110
+ const ctx = {
4111
+ ...makeMockCtx(),
4112
+ ui: {
4113
+ notify: () => {},
4114
+ setStatus: () => {},
4115
+ setWorkingMessage: () => {},
4116
+ },
4117
+ sessionManager: {
4118
+ getEntries: () => [],
4119
+ },
4120
+ modelRegistry: {
4121
+ getProviderAuthMode: () => undefined,
4122
+ isProviderRequestReady: () => true,
4123
+ },
4124
+ } as any;
4125
+ const notifications: Array<{ message: string; level?: string }> = [];
4126
+ ctx.ui.notify = (message: string, level?: string) => {
4127
+ notifications.push({ message, level });
4128
+ };
4129
+ const pi = makeMockPi();
4130
+ const s = makeLoopSession({
4131
+ basePath,
4132
+ canonicalProjectRoot: basePath,
4133
+ originalBasePath: basePath,
4134
+ });
4135
+ const deps = makeMockDeps();
4136
+
4137
+ async function runTimeoutUnit(iteration: number): Promise<string | undefined> {
4138
+ notifications.length = 0;
4139
+ const callsBefore = pi.calls.length;
4140
+ let seq = 0;
4141
+ const phasePromise = runUnitPhase(
4142
+ { ctx, pi, s, deps, prefs: undefined, iteration, flowId: `flow-${iteration}`, nextSeq: () => ++seq },
4143
+ {
4144
+ unitType: "plan-slice",
4145
+ unitId: "M001/S01",
4146
+ prompt: "plan the slice",
4147
+ finalPrompt: "plan the slice",
4148
+ pauseAfterUatDispatch: false,
4149
+ state: {
4150
+ phase: "planning",
4151
+ activeMilestone: { id: "M001", title: "Milestone" },
4152
+ activeSlice: { id: "S01", title: "Slice" },
4153
+ activeTask: null,
4154
+ registry: [{ id: "M001", title: "Milestone", status: "active" }],
4155
+ recentDecisions: [],
4156
+ blockers: [],
4157
+ nextAction: "",
4158
+ progress: { milestones: { done: 0, total: 1 } },
4159
+ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
4160
+ } as any,
4161
+ mid: "M001",
4162
+ midTitle: "Milestone",
4163
+ isRetry: false,
4164
+ previousTier: undefined,
4165
+ },
4166
+ makeLoopState(),
4167
+ );
4168
+
4169
+ // Wait until runUnit has dispatched the prompt, then resolve the unit
4170
+ // as a session-creation timeout. This avoids the 120s real timeout and
4171
+ // the mock-timer interaction that runUnitPhase's pre-flight setup makes
4172
+ // fragile.
4173
+ await new Promise<void>((resolve) => {
4174
+ const check = () => {
4175
+ if (pi.calls.length > callsBefore) return resolve();
4176
+ setTimeout(check, 5);
4177
+ };
4178
+ check();
4179
+ });
4180
+ resolveAgentEndCancelled({
4181
+ message: "Session creation timed out",
4182
+ category: "timeout",
4183
+ isTransient: true,
4184
+ });
4185
+
4186
+ const result = await phasePromise;
4187
+ return (result as any).reason;
4188
+ }
4189
+
4190
+ // Start from a known state in case a previous test left the counter raised.
4191
+ resetSessionTimeoutState();
4192
+
4193
+ // Exhaust the per-process timeout budget in the first "session".
4194
+ for (let i = 1; i <= 4; i++) {
4195
+ const reason = await runTimeoutUnit(i);
4196
+ assert.equal(reason, "session-timeout");
4197
+ }
4198
+ const lastBudgetNotification = notifications.find((n) =>
4199
+ n.message.includes("Session creation timed out")
4200
+ );
4201
+ assert.ok(lastBudgetNotification, "expected a session-creation timeout notification");
4202
+ assert.match(
4203
+ lastBudgetNotification.message,
4204
+ /Pausing for manual review/,
4205
+ "fourth consecutive timeout should exhaust the auto-resume budget",
4206
+ );
4207
+
4208
+ // Simulate a new auto-mode session starting. autoLoop() must reset the
4209
+ // module-level counter so the next timeout is treated as the first in the
4210
+ // new session rather than inheriting the exhausted budget.
4211
+ const freshSession = makeLoopSession({
4212
+ basePath,
4213
+ canonicalProjectRoot: basePath,
4214
+ originalBasePath: basePath,
4215
+ active: false,
4216
+ });
4217
+ freshSession.orchestration = createLoopTestOrchestration(ctx, pi, freshSession, deps);
4218
+ await rawAutoLoop(ctx, pi, freshSession, deps);
4219
+
4220
+ const reasonAfterReset = await runTimeoutUnit(5);
4221
+ assert.equal(reasonAfterReset, "session-timeout");
4222
+ const notificationAfterReset = notifications.find((n) =>
4223
+ n.message.includes("Auto-resuming")
4224
+ );
4225
+ assert.ok(notificationAfterReset, "expected an auto-resume notification after reset");
4226
+ assert.match(
4227
+ notificationAfterReset.message,
4228
+ /Auto-resuming/,
4229
+ "after autoLoop entry the timeout budget should be fresh so the first timeout auto-resumes",
4230
+ );
4231
+ });
4232
+
4067
4233
  test("runUnitPhase treats setup-race cancellations as pause-induced when session is already paused", async (t) => {
4068
4234
  _resetPendingResolve();
4069
4235
 
4070
- const basePath = mkdtempSync(join(tmpdir(), "gsd-paused-setup-race-"));
4236
+ const basePath = makeLoopTestBase("gsd-paused-setup-race-");
4071
4237
  t.after(() => {
4072
4238
  rmSync(basePath, { recursive: true, force: true });
4073
4239
  });
@@ -4142,7 +4308,7 @@ test("runUnitPhase treats setup-race cancellations as pause-induced when session
4142
4308
  test("runUnitPhase remembers aborted milestone closeout for same-unit resume", async (t) => {
4143
4309
  _resetPendingResolve();
4144
4310
 
4145
- const basePath = mkdtempSync(join(tmpdir(), "gsd-aborted-closeout-"));
4311
+ const basePath = makeLoopTestBase("gsd-aborted-closeout-");
4146
4312
  t.after(() => {
4147
4313
  rmSync(basePath, { recursive: true, force: true });
4148
4314
  });
@@ -4222,7 +4388,7 @@ test("runUnitPhase remembers aborted milestone closeout for same-unit resume", a
4222
4388
  test("runUnitPhase schedules default auto-resume for transient provider cancellations", async (t) => {
4223
4389
  _resetPendingResolve();
4224
4390
 
4225
- const basePath = mkdtempSync(join(tmpdir(), "gsd-provider-resume-"));
4391
+ const basePath = makeLoopTestBase("gsd-provider-resume-");
4226
4392
  t.after(() => {
4227
4393
  _resetPendingResolve();
4228
4394
  rmSync(basePath, { recursive: true, force: true });
@@ -4319,7 +4485,7 @@ test("runUnitPhase schedules default auto-resume for transient provider cancella
4319
4485
  test("runUnitPhase pauses ghost completions before closeout and finalize side effects", async (t) => {
4320
4486
  _resetPendingResolve();
4321
4487
 
4322
- const basePath = mkdtempSync(join(tmpdir(), "gsd-ghost-completion-"));
4488
+ const basePath = makeLoopTestBase("gsd-ghost-completion-");
4323
4489
  t.after(() => {
4324
4490
  _resetPendingResolve();
4325
4491
  rmSync(basePath, { recursive: true, force: true });
@@ -4421,7 +4587,7 @@ test("runUnitPhase pauses ghost completions before closeout and finalize side ef
4421
4587
  test("runUnitPhase records failed routing outcome when expected artifact is missing", async (t) => {
4422
4588
  _resetPendingResolve();
4423
4589
 
4424
- const basePath = mkdtempSync(join(tmpdir(), "gsd-routing-artifact-missing-"));
4590
+ const basePath = makeLoopTestBase("gsd-routing-artifact-missing-");
4425
4591
  t.after(() => {
4426
4592
  _resetPendingResolve();
4427
4593
  rmSync(basePath, { recursive: true, force: true });
@@ -4504,7 +4670,7 @@ test("runUnitPhase records failed routing outcome when expected artifact is miss
4504
4670
  test("runUnitPhase execute-task retry prompt instructs gsd_task_complete instead of manual summary writes", async (t) => {
4505
4671
  _resetPendingResolve();
4506
4672
 
4507
- const basePath = mkdtempSync(join(tmpdir(), "gsd-execute-task-retry-prompt-"));
4673
+ const basePath = makeLoopTestBase("gsd-execute-task-retry-prompt-");
4508
4674
  t.after(() => {
4509
4675
  _resetPendingResolve();
4510
4676
  rmSync(basePath, { recursive: true, force: true });
@@ -4585,7 +4751,7 @@ test("runUnitPhase execute-task retry prompt instructs gsd_task_complete instead
4585
4751
  test("runUnitPhase non-execute-task retry prompt keeps generic required-file guidance", async (t) => {
4586
4752
  _resetPendingResolve();
4587
4753
 
4588
- const basePath = mkdtempSync(join(tmpdir(), "gsd-non-execute-retry-prompt-"));
4754
+ const basePath = makeLoopTestBase("gsd-non-execute-retry-prompt-");
4589
4755
  t.after(() => {
4590
4756
  _resetPendingResolve();
4591
4757
  rmSync(basePath, { recursive: true, force: true });
@@ -4933,7 +5099,7 @@ test("autoLoop rejects execute-task with 0 tool calls as hallucinated (#1833)",
4933
5099
  test("runUnitPhase retries 0-tool units with ordinary network-related assistant text", async (t) => {
4934
5100
  _resetPendingResolve();
4935
5101
 
4936
- const basePath = mkdtempSync(join(tmpdir(), "gsd-zero-tool-network-text-"));
5102
+ const basePath = makeLoopTestBase("gsd-zero-tool-network-text-");
4937
5103
  t.after(() => {
4938
5104
  rmSync(basePath, { recursive: true, force: true });
4939
5105
  });
@@ -5028,7 +5194,7 @@ test("runUnitPhase retries 0-tool units with ordinary network-related assistant
5028
5194
  test("runUnitPhase pauses auto-mode when zero-tool-call retry is exhausted", async (t) => {
5029
5195
  _resetPendingResolve();
5030
5196
 
5031
- const basePath = mkdtempSync(join(tmpdir(), "gsd-zero-tool-exhausted-"));
5197
+ const basePath = makeLoopTestBase("gsd-zero-tool-exhausted-");
5032
5198
  t.after(() => {
5033
5199
  rmSync(basePath, { recursive: true, force: true });
5034
5200
  });
@@ -5313,7 +5479,7 @@ test("autoLoop rejects complete-slice with 0 tool calls as context-exhausted (#2
5313
5479
  test("autoLoop pauses on zero-tool-call rate-limit assistant messages instead of immediate retry", async (t) => {
5314
5480
  _resetPendingResolve();
5315
5481
 
5316
- const basePath = mkdtempSync(join(tmpdir(), "gsd-zero-tool-rate-limit-"));
5482
+ const basePath = makeLoopTestBase("gsd-zero-tool-rate-limit-");
5317
5483
  t.after(() => {
5318
5484
  _resetPendingResolve();
5319
5485
  rmSync(basePath, { recursive: true, force: true });
@@ -5529,6 +5695,15 @@ test("dispatch Worktree Safety honors degraded branch fallback instead of demand
5529
5695
  // branch in the project root. The safety gate must validate against that
5530
5696
  // effective branch mode, not the configured worktree mode.
5531
5697
  const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-degraded-"));
5698
+ execSync("git init --initial-branch=main", { cwd: projectRoot, stdio: "ignore" });
5699
+ execSync("git config user.email test@test.com", { cwd: projectRoot, stdio: "ignore" });
5700
+ execSync("git config user.name Test", { cwd: projectRoot, stdio: "ignore" });
5701
+ // The lifecycle fallback checks out the milestone branch in the project
5702
+ // root, so the safety gate's branch verification expects that branch here
5703
+ // too. expectedBranch comes from deps.autoWorktreeBranch (mocked to
5704
+ // "auto/M001"), so the fixture repo must be on that same branch.
5705
+ execSync("git commit --allow-empty -m init", { cwd: projectRoot, stdio: "ignore" });
5706
+ execSync("git checkout -b auto/M001", { cwd: projectRoot, stdio: "ignore" });
5532
5707
  t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
5533
5708
 
5534
5709
  const s = makeLoopSession({
@@ -5591,6 +5766,15 @@ test("dispatch Worktree Safety honors stranded branch recovery instead of demand
5591
5766
  // NOT degraded — the adoption is intentional. The safety gate must validate
5592
5767
  // against the effective branch mode, not the configured worktree mode.
5593
5768
  const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-stranded-"));
5769
+ execSync("git init --initial-branch=main", { cwd: projectRoot, stdio: "ignore" });
5770
+ execSync("git config user.email test@test.com", { cwd: projectRoot, stdio: "ignore" });
5771
+ execSync("git config user.name Test", { cwd: projectRoot, stdio: "ignore" });
5772
+ // Stranded recovery adopts the milestone branch in the project root, so the
5773
+ // safety gate's branch verification expects that branch here too.
5774
+ // expectedBranch comes from deps.autoWorktreeBranch (mocked to "auto/M001"),
5775
+ // so the fixture repo must be on that same branch.
5776
+ execSync("git commit --allow-empty -m init", { cwd: projectRoot, stdio: "ignore" });
5777
+ execSync("git checkout -b auto/M001", { cwd: projectRoot, stdio: "ignore" });
5594
5778
  t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
5595
5779
 
5596
5780
  const s = makeLoopSession({
@@ -5648,7 +5832,7 @@ test("runDispatch runs stuck detection while artifact verification retry is pend
5648
5832
  const notifications: string[] = [];
5649
5833
  ctx.ui.notify = (msg: string) => { notifications.push(msg); };
5650
5834
 
5651
- const basePath = mkdtempSync(join(tmpdir(), "gsd-5719-retry-stuck-"));
5835
+ const basePath = makeLoopTestBase("gsd-5719-retry-stuck-");
5652
5836
  t.after(() => rmSync(basePath, { recursive: true, force: true }));
5653
5837
 
5654
5838
  const s = makeLoopSession({
@@ -5716,7 +5900,7 @@ test("runDispatch falls back to main when dispatch guard cannot read main branch
5716
5900
 
5717
5901
  const ctx = makeMockCtx();
5718
5902
  const pi = makeMockPi();
5719
- const basePath = mkdtempSync(join(tmpdir(), "gsd-5530-main-branch-fallback-"));
5903
+ const basePath = makeLoopTestBase("gsd-5530-main-branch-fallback-");
5720
5904
  t.after(() => rmSync(basePath, { recursive: true, force: true }));
5721
5905
 
5722
5906
  let guardBranch: string | null = null;
@@ -6243,7 +6427,7 @@ test("pre-dispatch replace resolves final unit before dispatch health and stuck
6243
6427
  );
6244
6428
  });
6245
6429
 
6246
- test("autoLoop warns but proceeds for greenfield project (no project files) (#1833)", async () => {
6430
+ test("autoLoop warns but proceeds for greenfield project (no project files) (#1833)", async (t) => {
6247
6431
  _resetPendingResolve();
6248
6432
 
6249
6433
  const ctx = makeMockCtx();
@@ -6252,7 +6436,9 @@ test("autoLoop warns but proceeds for greenfield project (no project files) (#18
6252
6436
  const pi = makeMockPi();
6253
6437
 
6254
6438
  const notifications: string[] = [];
6255
- const s = makeLoopSession({ basePath: "/tmp/empty-worktree" });
6439
+ const basePath = makeLoopTestBase("gsd-greenfield-");
6440
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
6441
+ const s = makeLoopSession({ basePath });
6256
6442
 
6257
6443
  ctx.ui.notify = (msg: string) => {
6258
6444
  notifications.push(msg);
@@ -6275,8 +6461,6 @@ test("autoLoop warns but proceeds for greenfield project (no project files) (#18
6275
6461
  blockers: [],
6276
6462
  } as any;
6277
6463
  },
6278
- // Has .git but no package.json or src/
6279
- existsSync: (p: string) => p.endsWith(".git"),
6280
6464
  });
6281
6465
 
6282
6466
  await autoLoop(ctx, pi, s, deps);
@@ -42,7 +42,7 @@ import {
42
42
  } from "../gsd-db.js";
43
43
  import { AutoSession } from "../auto/session.js";
44
44
  import { registerAutoWorker } from "../db/auto-workers.js";
45
- import { claimMilestoneLease } from "../db/milestone-leases.js";
45
+ import { claimMilestoneLease, getMilestoneLease, releaseMilestoneLease } from "../db/milestone-leases.js";
46
46
  import { recordDispatchClaim, markFailed } from "../db/unit-dispatches.js";
47
47
  import { normalizeRealPath } from "../paths.js";
48
48
  import { acquireSessionLock, releaseSessionLock } from "../session-lock.js";
@@ -308,6 +308,71 @@ test("advance() sets active unit and is reflected in status", async (t) => {
308
308
  });
309
309
  });
310
310
 
311
+ test("advance() reclaims a released milestone lease before isolated source dispatch", async (t) => {
312
+ const f = makeFixture();
313
+ t.after(() => f.cleanup());
314
+
315
+ writeFileSync(
316
+ join(f.base, ".gsd", "PREFERENCES.md"),
317
+ "---\ngit:\n isolation: branch\n---\n",
318
+ );
319
+ execFileSync("git", ["checkout", "-b", "milestone/M001"], { cwd: f.base, stdio: "ignore" });
320
+
321
+ const priorWorkerId = registerAutoWorker({ projectRootRealpath: f.base });
322
+ const priorLease = claimMilestoneLease(priorWorkerId, "M001");
323
+ assert.equal(priorLease.ok, true);
324
+ if (!priorLease.ok) return;
325
+ assert.equal(releaseMilestoneLease(priorWorkerId, "M001", priorLease.token), true);
326
+
327
+ const resumedWorkerId = registerAutoWorker({ projectRootRealpath: f.base });
328
+ f.session.workerId = resumedWorkerId;
329
+ f.session.currentMilestoneId = null;
330
+ f.session.milestoneLeaseToken = null;
331
+
332
+ const result = await f.orchestrator.advance();
333
+
334
+ assert.equal(result.kind, "advanced", JSON.stringify(result));
335
+ assert.equal(f.session.currentMilestoneId, "M001");
336
+ assert.equal(f.session.milestoneLeaseToken, priorLease.token + 1);
337
+ const lease = getMilestoneLease("M001");
338
+ assert.equal(lease?.worker_id, resumedWorkerId);
339
+ assert.equal(lease?.status, "held");
340
+ assert.ok(f.journalNames().includes("advance"));
341
+ assert.ok(!f.journalNames().includes("advance-blocked"));
342
+ });
343
+
344
+ test("advance() claims the active milestone lease even when session still holds a prior milestone token", async (t) => {
345
+ const f = makeFixture();
346
+ t.after(() => f.cleanup());
347
+
348
+ writeFileSync(
349
+ join(f.base, ".gsd", "PREFERENCES.md"),
350
+ "---\ngit:\n isolation: branch\n---\n",
351
+ );
352
+ execFileSync("git", ["checkout", "-b", "milestone/M001"], { cwd: f.base, stdio: "ignore" });
353
+
354
+ insertMilestone({ id: "M000", title: "Prior", status: "complete" });
355
+ const workerId = registerAutoWorker({ projectRootRealpath: f.base });
356
+ const staleLease = claimMilestoneLease(workerId, "M000");
357
+ assert.equal(staleLease.ok, true);
358
+ if (!staleLease.ok) return;
359
+
360
+ f.session.workerId = workerId;
361
+ f.session.currentMilestoneId = "M000";
362
+ f.session.milestoneLeaseToken = staleLease.token;
363
+
364
+ const result = await f.orchestrator.advance();
365
+
366
+ assert.equal(result.kind, "advanced", JSON.stringify(result));
367
+ assert.equal(f.session.currentMilestoneId, "M001");
368
+ const activeLease = getMilestoneLease("M001");
369
+ assert.equal(activeLease?.worker_id, workerId);
370
+ assert.equal(activeLease?.status, "held");
371
+ assert.equal(f.session.milestoneLeaseToken, activeLease?.fencing_token);
372
+ assert.ok(f.journalNames().includes("advance"));
373
+ assert.ok(!f.journalNames().includes("advance-blocked"));
374
+ });
375
+
311
376
  test("advance() blocks source dispatch when an earlier slice is incomplete", async (t) => {
312
377
  const f = makeFixture({
313
378
  dispatch: () => ({
@@ -425,7 +490,7 @@ test("advance() reports completion when complete state has no next unit", async
425
490
  assert.equal(f.orchestrator.getStatus().phase, "stopped");
426
491
  });
427
492
 
428
- test("advance() blocks all-complete stop when completed milestone is still unmerged in a worktree", async (t) => {
493
+ test("advance() merges a completed milestone worktree before all-complete stop", async (t) => {
429
494
  const f = makeFixture({ complete: true, noTask: true });
430
495
  t.after(() => f.cleanup());
431
496
 
@@ -466,17 +531,16 @@ test("advance() blocks all-complete stop when completed milestone is still unmer
466
531
 
467
532
  const result = await f.orchestrator.advance();
468
533
 
469
- assert.equal(result.kind, "blocked");
470
- if (result.kind !== "blocked") return;
471
- assert.equal(result.action, "pause");
472
- assert.equal(result.terminalOutcome?.code, "settlement-blocked");
473
- assert.match(result.reason, /worktree branch has not been merged to main/);
474
- assert.doesNotMatch(result.reason, /quality gate Q3 is still pending/);
475
- assert.equal(f.orchestrator.getStatus().phase, "paused");
476
- assert.equal(f.session.milestoneSettlement?.ok, false);
534
+ assert.equal(result.kind, "stopped");
535
+ if (result.kind !== "stopped") return;
536
+ assert.equal(result.reason, "All milestones complete");
537
+ assert.equal(result.terminalOutcome?.code, "all-complete");
538
+ assert.equal(f.orchestrator.getStatus().phase, "stopped");
539
+ assert.equal(f.session.milestoneMergedInPhases, true);
540
+ assert.deepEqual(f.session.milestoneSettlement, { ok: true, reason: "settled" });
477
541
  const names = f.journalNames();
478
- assert.ok(names.includes("advance-blocked"));
479
- assert.ok(!names.includes("advance-stopped"));
542
+ assert.ok(names.includes("advance-stopped"));
543
+ assert.ok(!names.includes("advance-blocked"));
480
544
  });
481
545
 
482
546
  test("advance() stopped clears previous activeUnit and resets idempotent lock", async (t) => {
@@ -13,7 +13,7 @@ import { join } from "node:path";
13
13
  import { capturePauseAutoUnitIdentity, pauseAuto, isAutoActive } from "../auto.ts";
14
14
  import { _resetPendingResolve, _setCurrentResolve } from "../auto/resolve.ts";
15
15
  import { autoSession } from "../auto-runtime-state.ts";
16
- import { _isPauseOriginCancelledResult } from "../auto/phases.ts";
16
+ import { _isPauseOriginCancelledResult } from "../auto/phase-helpers.ts";
17
17
 
18
18
  test("pauseAuto sets s.active = false synchronously before first await (blocks concurrent re-entry)", async () => {
19
19
  const base = mkdtempSync(join(tmpdir(), "gsd-double-pause-guard-"));
@@ -7,10 +7,17 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "nod
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
 
10
- import { cleanupAfterLoopExit, pauseAuto, rerootCommandSession, stopAuto } from "../auto.ts";
10
+ import {
11
+ anchorProcessCwdForAutoResume,
12
+ cleanupAfterLoopExit,
13
+ pauseAuto,
14
+ rerootCommandSession,
15
+ stopAuto,
16
+ } from "../auto.ts";
11
17
  import { autoSession } from "../auto-runtime-state.ts";
12
18
  import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
13
19
  import { getAutoWorker, registerAutoWorker } from "../db/auto-workers.ts";
20
+ import { claimMilestoneLease, getMilestoneLease } from "../db/milestone-leases.ts";
14
21
  import { readPausedSessionMetadata } from "../interrupted-session.ts";
15
22
  import { WorktreeLifecycle } from "../worktree-lifecycle.ts";
16
23
 
@@ -107,6 +114,25 @@ test("cleanupAfterLoopExit preserves paused worktree session and visible failure
107
114
  }
108
115
  });
109
116
 
117
+ test("anchorProcessCwdForAutoResume recovers when current cwd was deleted", () => {
118
+ const base = mkdtempSync(join(tmpdir(), "gsd-resume-cwd-anchor-"));
119
+ const deletedCwd = join(base, ".gsd-worktrees", "M002");
120
+ const previousCwd = process.cwd();
121
+
122
+ mkdirSync(deletedCwd, { recursive: true });
123
+
124
+ try {
125
+ process.chdir(deletedCwd);
126
+ rmSync(deletedCwd, { recursive: true, force: true });
127
+
128
+ assert.equal(anchorProcessCwdForAutoResume(base), true);
129
+ assert.equal(realpathSync(process.cwd()), realpathSync(base));
130
+ } finally {
131
+ process.chdir(previousCwd);
132
+ rmSync(base, { recursive: true, force: true });
133
+ }
134
+ });
135
+
110
136
  test("cleanupAfterLoopExit clears status and progress widget without replacing outcome surface", async () => {
111
137
  const statusCalls: unknown[] = [];
112
138
  const widgetCalls: unknown[] = [];
@@ -276,6 +302,56 @@ test("pauseAuto marks active worker as stopping and clears workerId", async () =
276
302
  }
277
303
  });
278
304
 
305
+ test("pauseAuto preserves worker lease across transient provider auto-resume pauses", async () => {
306
+ const base = mkdtempSync(join(tmpdir(), "gsd-pause-provider-lease-"));
307
+ const previousCwd = process.cwd();
308
+ const dbPath = join(base, ".gsd", "gsd.db");
309
+ mkdirSync(join(base, ".gsd"), { recursive: true });
310
+
311
+ autoSession.reset();
312
+ autoSession.active = true;
313
+ autoSession.basePath = base;
314
+ autoSession.originalBasePath = base;
315
+ autoSession.currentMilestoneId = "M001";
316
+
317
+ try {
318
+ openDatabase(dbPath);
319
+ insertMilestone({ id: "M001", title: "Milestone 1", status: "active" });
320
+ const workerId = registerAutoWorker({ projectRootRealpath: base });
321
+ const lease = claimMilestoneLease(workerId, "M001");
322
+ assert.equal(lease.ok, true);
323
+ if (!lease.ok) return;
324
+
325
+ autoSession.workerId = workerId;
326
+ autoSession.milestoneLeaseToken = lease.token;
327
+ process.chdir(base);
328
+
329
+ await pauseAuto(undefined, undefined, {
330
+ message: "Provider error: socket closed",
331
+ category: "provider",
332
+ isTransient: true,
333
+ });
334
+
335
+ assert.equal(autoSession.paused, true);
336
+ assert.equal(autoSession.workerId, workerId);
337
+ assert.equal(autoSession.milestoneLeaseToken, lease.token);
338
+ assert.equal(getAutoWorker(workerId)?.status, "active");
339
+ const row = getMilestoneLease("M001");
340
+ assert.equal(row?.worker_id, workerId);
341
+ assert.equal(row?.status, "held");
342
+ assert.equal(row?.fencing_token, lease.token);
343
+ } finally {
344
+ autoSession.reset();
345
+ try {
346
+ closeDatabase();
347
+ } catch {
348
+ /* noop */
349
+ }
350
+ process.chdir(previousCwd);
351
+ rmSync(base, { recursive: true, force: true });
352
+ }
353
+ });
354
+
279
355
  test("pauseAuto records the expected worktree path when paused from project root", async () => {
280
356
  const base = mkdtempSync(join(tmpdir(), "gsd-pause-worktree-path-"));
281
357
  const previousCwd = process.cwd();
@@ -7,7 +7,8 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { tmpdir } from "node:os";
9
9
 
10
- import { resolveDispatchRecoveryAttempts, runFinalize } from "../auto/phases.ts";
10
+ import { resolveDispatchRecoveryAttempts } from "../auto/unit-phase.ts";
11
+ import { runFinalize } from "../auto/finalize.ts";
11
12
  import { AutoSession } from "../auto/session.ts";
12
13
  import { readUnitRuntimeRecord, writeUnitRuntimeRecord } from "../unit-runtime.ts";
13
14
  import { captureRootDirtySnapshot } from "../root-write-leak-guard.ts";