gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.3065f7bfa

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 (268) hide show
  1. package/README.md +1 -0
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +178 -108
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +20 -30
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
  10. package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
  11. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  12. package/dist/resources/extensions/gsd/auto-start.js +197 -6
  13. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  14. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  15. package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
  16. package/dist/resources/extensions/gsd/auto.js +79 -24
  17. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +42 -2
  18. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
  19. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
  20. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
  21. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
  22. package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
  23. package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
  24. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +238 -28
  25. package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
  26. package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
  27. package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
  28. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  29. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  30. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  31. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  32. package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
  33. package/dist/resources/extensions/gsd/git-service.js +36 -4
  34. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  35. package/dist/resources/extensions/gsd/guided-flow.js +66 -32
  36. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
  38. package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
  39. package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
  40. package/dist/resources/extensions/gsd/pre-execution-checks.js +22 -0
  41. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  42. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  43. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  45. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  47. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  48. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  49. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  50. package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
  51. package/dist/resources/extensions/gsd/worktree-resolver.js +68 -21
  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 +15 -15
  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 +15 -15
  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 +3 -3
  86. package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
  87. package/packages/native/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  89. package/packages/pi-agent-core/dist/agent-loop.js +4 -1
  90. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  91. package/packages/pi-agent-core/dist/agent.d.ts +5 -0
  92. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  93. package/packages/pi-agent-core/dist/agent.js +2 -0
  94. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  95. package/packages/pi-agent-core/dist/index.d.ts +1 -0
  96. package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
  97. package/packages/pi-agent-core/dist/index.js +2 -0
  98. package/packages/pi-agent-core/dist/index.js.map +1 -1
  99. package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
  100. package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
  101. package/packages/pi-agent-core/dist/token-audit.js +221 -0
  102. package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
  103. package/packages/pi-agent-core/dist/types.d.ts +9 -0
  104. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  105. package/packages/pi-agent-core/dist/types.js.map +1 -1
  106. package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
  107. package/packages/pi-agent-core/src/agent-loop.ts +4 -1
  108. package/packages/pi-agent-core/src/agent.ts +8 -0
  109. package/packages/pi-agent-core/src/index.ts +2 -0
  110. package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
  111. package/packages/pi-agent-core/src/token-audit.ts +287 -0
  112. package/packages/pi-agent-core/src/types.ts +14 -0
  113. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  114. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  115. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
  117. package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  119. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/core/agent-session.js +41 -7
  121. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
  126. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
  128. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
  130. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
  132. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
  135. package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
  137. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
  138. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
  139. package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
  140. package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
  141. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
  143. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
  145. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
  147. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
  149. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  150. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  151. package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
  152. package/packages/pi-coding-agent/src/core/agent-session.ts +45 -7
  153. package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
  154. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
  155. package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
  156. package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
  157. package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
  158. package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
  159. package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
  160. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
  161. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
  162. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  163. package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
  164. package/src/resources/extensions/gsd/auto/phases.ts +251 -172
  165. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  166. package/src/resources/extensions/gsd/auto/run-unit.ts +25 -30
  167. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  168. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
  169. package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
  170. package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
  171. package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
  172. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  173. package/src/resources/extensions/gsd/auto-start.ts +230 -9
  174. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  175. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  176. package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
  177. package/src/resources/extensions/gsd/auto.ts +87 -19
  178. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +49 -1
  179. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
  180. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
  181. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
  182. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
  183. package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
  184. package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
  185. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +283 -28
  186. package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
  187. package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
  188. package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
  189. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  190. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  191. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  192. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  193. package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
  194. package/src/resources/extensions/gsd/git-service.ts +46 -8
  195. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  196. package/src/resources/extensions/gsd/guided-flow.ts +84 -39
  197. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  198. package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
  199. package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
  200. package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
  201. package/src/resources/extensions/gsd/pre-execution-checks.ts +23 -0
  202. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  203. package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
  204. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  206. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  207. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  208. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
  209. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +417 -10
  210. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  211. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  212. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
  213. package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
  214. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +3 -1
  215. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
  216. package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
  217. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  218. package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
  219. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
  220. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  221. package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
  222. package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
  223. package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
  224. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +54 -0
  225. package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
  226. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +239 -1
  227. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
  228. package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
  229. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  230. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
  231. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
  232. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
  233. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
  234. package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
  235. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
  236. package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
  237. package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +45 -5
  238. package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
  239. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  240. package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
  241. package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
  242. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  243. package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
  244. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +106 -0
  245. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  246. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  247. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -0
  248. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  249. package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
  250. package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
  251. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
  252. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  253. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  254. package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
  255. package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
  256. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  257. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
  258. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
  259. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
  260. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +167 -4
  261. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  262. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  263. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  264. package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
  265. package/src/resources/extensions/gsd/worktree-resolver.ts +85 -19
  266. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
  267. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_buildManifest.js +0 -0
  268. /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_ssgManifest.js +0 -0
@@ -10,10 +10,13 @@ import {
10
10
  _resetPendingResolve,
11
11
  _hasPendingResolveForTest,
12
12
  _setActiveSession,
13
+ _setSessionSwitchInFlight,
14
+ _consumePendingSwitchCancellation,
13
15
  isSessionSwitchInFlight,
14
16
  } from "../auto/resolve.js";
15
17
  import { runUnit } from "../auto/run-unit.js";
16
18
  import { autoLoop } from "../auto/loop.js";
19
+ import { runDispatch } from "../auto/phases.js";
17
20
  import { detectStuck } from "../auto/detect-stuck.js";
18
21
  import type { UnitResult, AgentEndEvent } from "../auto/types.js";
19
22
  import type { LoopDeps } from "../auto/loop-deps.js";
@@ -64,7 +67,7 @@ function makeMockSession(opts?: {
64
67
  verbose: false,
65
68
  basePath: process.cwd(),
66
69
  cmdCtx: {
67
- newSession: (options?: { abortSignal?: AbortSignal }) => {
70
+ newSession: (options?: { abortSignal?: AbortSignal; workspaceRoot?: string }) => {
68
71
  opts?.onNewSessionStart?.(session);
69
72
  if (opts?.newSessionThrows) {
70
73
  return Promise.reject(new Error(opts.newSessionThrows));
@@ -76,7 +79,7 @@ function makeMockSession(opts?: {
76
79
  setTimeout(() => {
77
80
  // Simulate AgentSession.newSession() checking abortSignal after
78
81
  // its internal async work (abort()) completes — this is where the
79
- // real code captures process.cwd() and rebuilds the tool runtime.
82
+ // real code selects a workspace root and rebuilds the tool runtime.
80
83
  // If the signal is aborted, the real code discards the session.
81
84
  opts?.onSignalCheck?.(options?.abortSignal?.aborted ?? false);
82
85
  opts?.onNewSessionSettle?.(session);
@@ -206,6 +209,28 @@ test("runUnit returns cancelled when session creation fails", async () => {
206
209
  assert.equal(pi.calls.length, 0);
207
210
  });
208
211
 
212
+ test("runUnit clears queued switch cancellation when session creation fails", async () => {
213
+ _resetPendingResolve();
214
+
215
+ const ctx = makeMockCtx();
216
+ const pi = makeMockPi();
217
+ const s = makeMockSession({
218
+ newSessionThrows: "connection refused",
219
+ onNewSessionStart: () => {
220
+ resolveAgentEndCancelled({
221
+ message: "Claude Code process aborted by user",
222
+ category: "aborted",
223
+ isTransient: false,
224
+ });
225
+ },
226
+ });
227
+
228
+ const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
229
+
230
+ assert.equal(result.status, "cancelled");
231
+ assert.equal(_consumePendingSwitchCancellation(), null);
232
+ });
233
+
209
234
  test("runUnit returns cancelled when session creation times out", async () => {
210
235
  _resetPendingResolve();
211
236
 
@@ -221,6 +246,34 @@ test("runUnit returns cancelled when session creation times out", async () => {
221
246
  assert.equal(pi.calls.length, 0);
222
247
  });
223
248
 
249
+ test("runUnit consumes a cancellation queued during session switch before dispatch", async () => {
250
+ _resetPendingResolve();
251
+
252
+ const ctx = makeMockCtx();
253
+ const pi = makeMockPi();
254
+ let cancellationQueued = false;
255
+ const s = makeMockSession({
256
+ newSessionDelayMs: 10,
257
+ onNewSessionStart: () => {
258
+ setTimeout(() => {
259
+ cancellationQueued = !resolveAgentEndCancelled({
260
+ message: "Claude Code process aborted by user",
261
+ category: "aborted",
262
+ isTransient: false,
263
+ });
264
+ }, 0);
265
+ },
266
+ });
267
+
268
+ const result = await runUnit(ctx, pi, s, "plan-slice", "M009/S01", "prompt");
269
+
270
+ assert.equal(cancellationQueued, true);
271
+ assert.equal(result.status, "cancelled");
272
+ assert.equal(result.errorContext?.category, "aborted");
273
+ assert.equal(result.errorContext?.message, "Claude Code process aborted by user");
274
+ assert.equal(pi.calls.length, 0, "queued switch cancellation must prevent prompt dispatch");
275
+ });
276
+
224
277
  test("runUnit keeps the session-switch guard across a late newSession settlement", async () => {
225
278
  _resetPendingResolve();
226
279
  mock.timers.enable();
@@ -496,10 +549,9 @@ test("runUnit proceeds when isProviderRequestReady throws (defensive) (#4555)",
496
549
  assert.equal(pi.calls.length, 0);
497
550
  });
498
551
 
499
- test("late-resolving newSession() after timeout receives aborted signal so tool runtime is not configured with root cwd (#3731)", async () => {
500
- // When newSession() times out in runUnit(), auto-mode restores cwd to project
501
- // root. If newSession() later resolves, it must NOT use process.cwd() to
502
- // configure the tool runtime (which would give it root cwd, not worktree cwd).
552
+ test("late-resolving newSession() after timeout receives aborted signal so tool runtime is not configured with stale workspace root (#3731)", async () => {
553
+ // When newSession() times out in runUnit(), a late resolution must not
554
+ // configure the tool runtime against a stale workspace root.
503
555
  //
504
556
  // The fix: runUnit creates an AbortController, aborts it on timeout, and passes
505
557
  // the signal to newSession(). AgentSession.newSession() checks the signal after
@@ -514,8 +566,8 @@ test("late-resolving newSession() after timeout receives aborted signal so tool
514
566
 
515
567
  // newSession mock simulates AgentSession.newSession() behavior:
516
568
  // after an internal delay (representing await this.abort()), it checks the
517
- // abortSignal that's where the real code would capture process.cwd() and
518
- // call _buildRuntime. If aborted, the real code must discard the session.
569
+ // abortSignal before selecting the workspace root and calling _buildRuntime.
570
+ // If aborted, the real code must discard the session.
519
571
  const s = makeMockSession({
520
572
  newSessionDelayMs: 200_000, // longer than NEW_SESSION_TIMEOUT_MS (120s)
521
573
  onSignalCheck: (aborted) => {
@@ -548,7 +600,7 @@ test("late-resolving newSession() after timeout receives aborted signal so tool
548
600
  abortedWhenLateSessionSettled,
549
601
  true,
550
602
  "runUnit must pass an aborted AbortSignal to newSession() when it resolves after the session-creation timeout (#3731). " +
551
- "Without this, AgentSession.newSession() captures root process.cwd() and rebuilds the tool runtime with wrong cwd.",
603
+ "Without this, AgentSession.newSession() can rebuild the tool runtime with a stale workspace root.",
552
604
  );
553
605
  } finally {
554
606
  mock.timers.reset();
@@ -637,7 +689,11 @@ function makeMockDeps(
637
689
  resolveMilestoneFile: () => null,
638
690
  reconcileMergeState: () => "clean",
639
691
  preflightCleanRoot: () => ({ stashPushed: false, summary: "" }),
640
- postflightPopStash: () => {},
692
+ postflightPopStash: () => ({
693
+ restored: true,
694
+ needsManualRecovery: false,
695
+ message: "restored",
696
+ }),
641
697
  getLedger: () => null,
642
698
  getProjectTotals: () => ({ cost: 0 }),
643
699
  formatCost: (c: number) => `$${c.toFixed(2)}`,
@@ -805,6 +861,145 @@ test("autoLoop exits on terminal complete state", async (t) => {
805
861
  );
806
862
  });
807
863
 
864
+ test("autoLoop stops before success notification when postflight stash restore needs recovery", async () => {
865
+ _resetPendingResolve();
866
+
867
+ const notifications: Array<{ msg: string; level: string }> = [];
868
+ const ctx = makeMockCtx();
869
+ ctx.ui.setStatus = () => {};
870
+ ctx.ui.notify = (msg: string, level: string) => {
871
+ notifications.push({ msg, level });
872
+ };
873
+ const pi = makeMockPi();
874
+ const s = makeLoopSession();
875
+ let stopReason = "";
876
+
877
+ const deps = makeMockDeps({
878
+ deriveState: async () => {
879
+ deps.callLog.push("deriveState");
880
+ return {
881
+ phase: "complete",
882
+ activeMilestone: { id: "M001", title: "Test", status: "complete" },
883
+ activeSlice: null,
884
+ activeTask: null,
885
+ registry: [{ id: "M001", status: "complete" }],
886
+ blockers: [],
887
+ } as any;
888
+ },
889
+ preflightCleanRoot: () => ({
890
+ stashPushed: true,
891
+ stashMarker: "gsd-preflight-stash:M001:test",
892
+ summary: "stashed",
893
+ }),
894
+ postflightPopStash: () => ({
895
+ restored: false,
896
+ needsManualRecovery: true,
897
+ message: "git stash pop stash@{0} failed after merge of milestone M001",
898
+ stashRef: "stash@{0}",
899
+ }),
900
+ sendDesktopNotification: () => {
901
+ deps.callLog.push("sendDesktopNotification");
902
+ },
903
+ logCmuxEvent: () => {
904
+ deps.callLog.push("logCmuxEvent");
905
+ },
906
+ stopAuto: async (_ctx, _pi, reason) => {
907
+ deps.callLog.push("stopAuto");
908
+ stopReason = reason ?? "";
909
+ },
910
+ });
911
+
912
+ await autoLoop(ctx, pi, s, deps);
913
+
914
+ assert.equal(stopReason, "Post-merge stash restore failed for milestone M001");
915
+ assert.ok(
916
+ notifications.some(
917
+ (n) => n.level === "error" && n.msg.includes("Post-merge stash restore failed for milestone M001"),
918
+ ),
919
+ "failed postflight restore must be surfaced as an error",
920
+ );
921
+ assert.ok(
922
+ !deps.callLog.includes("sendDesktopNotification"),
923
+ "must not emit milestone success desktop notification after stash restore failure",
924
+ );
925
+ assert.ok(
926
+ !deps.callLog.includes("logCmuxEvent"),
927
+ "must not emit milestone success cmux event after stash restore failure",
928
+ );
929
+ });
930
+
931
+ test("autoLoop marks transition merge complete before postflight recovery stop", async () => {
932
+ _resetPendingResolve();
933
+
934
+ const ctx = makeMockCtx();
935
+ ctx.ui.setStatus = () => {};
936
+ ctx.ui.notify = () => {};
937
+ const pi = makeMockPi();
938
+ const s = makeLoopSession();
939
+ let mergeCalls = 0;
940
+ let stopReason = "";
941
+
942
+ const deps = makeMockDeps({
943
+ deriveState: async () => {
944
+ deps.callLog.push("deriveState");
945
+ return {
946
+ phase: "executing",
947
+ activeMilestone: { id: "M002", title: "Next", status: "active" },
948
+ activeSlice: null,
949
+ activeTask: null,
950
+ registry: [
951
+ { id: "M001", title: "Done", status: "complete" },
952
+ { id: "M002", title: "Next", status: "active" },
953
+ ],
954
+ blockers: [],
955
+ } as any;
956
+ },
957
+ preflightCleanRoot: () => ({
958
+ stashPushed: true,
959
+ stashMarker: "gsd-preflight-stash:M001:test",
960
+ summary: "stashed",
961
+ }),
962
+ postflightPopStash: () => ({
963
+ restored: false,
964
+ needsManualRecovery: true,
965
+ message: "git stash pop stash@{0} failed after merge of milestone M001",
966
+ stashRef: "stash@{0}",
967
+ }),
968
+ resolver: {
969
+ get workPath() {
970
+ return "/tmp/project";
971
+ },
972
+ get projectRoot() {
973
+ return "/tmp/project";
974
+ },
975
+ get lockPath() {
976
+ return "/tmp/project";
977
+ },
978
+ enterMilestone: () => {
979
+ assert.fail("must not enter the next milestone after postflight recovery fails");
980
+ },
981
+ exitMilestone: () => {},
982
+ mergeAndExit: () => {
983
+ mergeCalls += 1;
984
+ },
985
+ mergeAndEnterNext: () => {},
986
+ } as any,
987
+ stopAuto: async (_ctx, _pi, reason) => {
988
+ deps.callLog.push("stopAuto");
989
+ stopReason = reason ?? "";
990
+ if (!s.milestoneMergedInPhases) {
991
+ deps.resolver.mergeAndExit("M001", ctx.ui);
992
+ }
993
+ },
994
+ });
995
+
996
+ await autoLoop(ctx, pi, s, deps);
997
+
998
+ assert.equal(stopReason, "Post-merge stash restore failed for milestone M001");
999
+ assert.equal(s.milestoneMergedInPhases, true);
1000
+ assert.equal(mergeCalls, 1, "postflight recovery stop must not re-run an already completed transition merge");
1001
+ });
1002
+
808
1003
  test("autoLoop pauses when provider readiness cancels before dispatch", async () => {
809
1004
  _resetPendingResolve();
810
1005
 
@@ -2089,6 +2284,25 @@ test("resolveAgentEndCancelled without args produces no errorContext field", asy
2089
2284
  assert.equal(resolved.errorContext, undefined, "errorContext must not be present when no args passed");
2090
2285
  });
2091
2286
 
2287
+ test("resolveAgentEndCancelled queues cancellation that arrives during session switch", () => {
2288
+ _resetPendingResolve();
2289
+
2290
+ _setSessionSwitchInFlight(true);
2291
+ const resolved = resolveAgentEndCancelled({
2292
+ message: "Claude Code process aborted by user",
2293
+ category: "aborted",
2294
+ isTransient: false,
2295
+ });
2296
+
2297
+ assert.equal(resolved, false);
2298
+ const pending = _consumePendingSwitchCancellation();
2299
+ assert.ok(pending?.errorContext, "queued cancellation should preserve errorContext");
2300
+ assert.equal(pending.errorContext.category, "aborted");
2301
+ assert.equal(pending.errorContext.message, "Claude Code process aborted by user");
2302
+ assert.equal(_consumePendingSwitchCancellation(), null);
2303
+ _resetPendingResolve();
2304
+ });
2305
+
2092
2306
  // ─── #1571: artifact verification retry ──────────────────────────────────────
2093
2307
 
2094
2308
  test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
@@ -2509,6 +2723,199 @@ test("autoLoop stops when worktree has no .git for execute-task (#1833)", async
2509
2723
  );
2510
2724
  });
2511
2725
 
2726
+ test("dispatch health check wins before stuck detection for execute-task without .git", async () => {
2727
+ _resetPendingResolve();
2728
+
2729
+ const ctx = makeMockCtx();
2730
+ const pi = makeMockPi();
2731
+ const notifications: string[] = [];
2732
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
2733
+
2734
+ const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
2735
+ const deps = makeMockDeps({
2736
+ existsSync: (p: string) => !p.endsWith(".git"),
2737
+ });
2738
+ const result = await runDispatch(
2739
+ {
2740
+ ctx,
2741
+ pi,
2742
+ s,
2743
+ deps,
2744
+ prefs: undefined,
2745
+ iteration: 1,
2746
+ flowId: "test-flow",
2747
+ nextSeq: () => 1,
2748
+ },
2749
+ {
2750
+ state: {
2751
+ phase: "executing",
2752
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
2753
+ activeSlice: { id: "S01", title: "Slice 1" },
2754
+ activeTask: { id: "T01" },
2755
+ registry: [{ id: "M001", status: "active" }],
2756
+ blockers: [],
2757
+ } as any,
2758
+ mid: "M001",
2759
+ midTitle: "Test",
2760
+ },
2761
+ {
2762
+ recentUnits: [
2763
+ { key: "execute-task/M001/S01/T01" },
2764
+ { key: "execute-task/M001/S01/T01" },
2765
+ ],
2766
+ stuckRecoveryAttempts: 1,
2767
+ consecutiveFinalizeTimeouts: 0,
2768
+ },
2769
+ );
2770
+
2771
+ assert.equal(result.action, "break");
2772
+ assert.equal(result.reason, "worktree-invalid");
2773
+ assert.ok(deps.callLog.includes("stopAuto"), "should stop through worktree health check");
2774
+ assert.ok(
2775
+ notifications.some((n) => n.includes("Worktree health check failed") && n.includes("no .git")),
2776
+ "should notify about missing .git",
2777
+ );
2778
+ assert.ok(
2779
+ !notifications.some((n) => n.includes("Stuck on execute-task")),
2780
+ "stuck-loop message must not mask the worktree health failure",
2781
+ );
2782
+ });
2783
+
2784
+ test("pre-dispatch skip resolves before dispatch health and stuck accounting", async () => {
2785
+ _resetPendingResolve();
2786
+
2787
+ const ctx = makeMockCtx();
2788
+ const pi = makeMockPi();
2789
+ const notifications: string[] = [];
2790
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
2791
+
2792
+ const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
2793
+ const deps = makeMockDeps({
2794
+ existsSync: (p: string) => !p.endsWith(".git"),
2795
+ runPreDispatchHooks: () => ({ firedHooks: ["skip-execute"], action: "skip" }),
2796
+ });
2797
+ const loopState = {
2798
+ recentUnits: [
2799
+ { key: "execute-task/M001/S01/T01" },
2800
+ { key: "execute-task/M001/S01/T01" },
2801
+ ],
2802
+ stuckRecoveryAttempts: 1,
2803
+ consecutiveFinalizeTimeouts: 0,
2804
+ };
2805
+
2806
+ const result = await runDispatch(
2807
+ {
2808
+ ctx,
2809
+ pi,
2810
+ s,
2811
+ deps,
2812
+ prefs: undefined,
2813
+ iteration: 1,
2814
+ flowId: "test-flow",
2815
+ nextSeq: () => 1,
2816
+ },
2817
+ {
2818
+ state: {
2819
+ phase: "executing",
2820
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
2821
+ activeSlice: { id: "S01", title: "Slice 1" },
2822
+ activeTask: { id: "T01" },
2823
+ registry: [{ id: "M001", status: "active" }],
2824
+ blockers: [],
2825
+ } as any,
2826
+ mid: "M001",
2827
+ midTitle: "Test",
2828
+ },
2829
+ loopState,
2830
+ );
2831
+
2832
+ assert.equal(result.action, "continue");
2833
+ assert.ok(!deps.callLog.includes("stopAuto"), "skip hook should not stop on worktree health");
2834
+ assert.equal(loopState.recentUnits.length, 2, "skip hook should not update stuck accounting");
2835
+ assert.ok(
2836
+ notifications.some((n) => n.includes("Skipping execute-task M001/S01/T01")),
2837
+ "should notify about the skip hook",
2838
+ );
2839
+ assert.ok(
2840
+ !notifications.some((n) => n.includes("Worktree health check failed") || n.includes("Stuck on execute-task")),
2841
+ "health and stuck notifications must not run before skip hook resolution",
2842
+ );
2843
+ });
2844
+
2845
+ test("pre-dispatch replace resolves final unit before dispatch health and stuck accounting", async () => {
2846
+ _resetPendingResolve();
2847
+
2848
+ const ctx = makeMockCtx();
2849
+ const pi = makeMockPi();
2850
+ const notifications: string[] = [];
2851
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
2852
+
2853
+ const s = makeLoopSession({ basePath: "/tmp/broken-worktree" });
2854
+ const deps = makeMockDeps({
2855
+ existsSync: (p: string) => !p.endsWith(".git"),
2856
+ runPreDispatchHooks: () => ({
2857
+ firedHooks: ["review"],
2858
+ action: "replace",
2859
+ unitType: "hook/review",
2860
+ prompt: "review before executing",
2861
+ model: "review-model",
2862
+ }),
2863
+ });
2864
+ const loopState = {
2865
+ recentUnits: [
2866
+ { key: "execute-task/M001/S01/T01" },
2867
+ { key: "execute-task/M001/S01/T01" },
2868
+ ],
2869
+ stuckRecoveryAttempts: 1,
2870
+ consecutiveFinalizeTimeouts: 0,
2871
+ };
2872
+
2873
+ const result = await runDispatch(
2874
+ {
2875
+ ctx,
2876
+ pi,
2877
+ s,
2878
+ deps,
2879
+ prefs: undefined,
2880
+ iteration: 1,
2881
+ flowId: "test-flow",
2882
+ nextSeq: () => 1,
2883
+ },
2884
+ {
2885
+ state: {
2886
+ phase: "executing",
2887
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
2888
+ activeSlice: { id: "S01", title: "Slice 1" },
2889
+ activeTask: { id: "T01" },
2890
+ registry: [{ id: "M001", status: "active" }],
2891
+ blockers: [],
2892
+ } as any,
2893
+ mid: "M001",
2894
+ midTitle: "Test",
2895
+ },
2896
+ loopState,
2897
+ );
2898
+
2899
+ assert.equal(result.action, "next");
2900
+ assert.equal(result.data?.unitType, "hook/review");
2901
+ assert.equal(result.data?.finalPrompt, "review before executing");
2902
+ assert.equal(result.data?.hookModelOverride, "review-model");
2903
+ assert.ok(!deps.callLog.includes("stopAuto"), "replace hook should not stop on execute-task health");
2904
+ assert.deepEqual(
2905
+ loopState.recentUnits.map((u) => u.key),
2906
+ [
2907
+ "execute-task/M001/S01/T01",
2908
+ "execute-task/M001/S01/T01",
2909
+ "hook/review/M001/S01/T01",
2910
+ ],
2911
+ "stuck accounting should record the final replaced unit",
2912
+ );
2913
+ assert.ok(
2914
+ !notifications.some((n) => n.includes("Worktree health check failed") || n.includes("Stuck on execute-task")),
2915
+ "health and stuck notifications must use the final replaced unit",
2916
+ );
2917
+ });
2918
+
2512
2919
  test("autoLoop warns but proceeds for greenfield project (no project files) (#1833)", async () => {
2513
2920
  _resetPendingResolve();
2514
2921
 
@@ -1,17 +1,20 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
3
6
 
4
7
  import { runFinalize } from "../auto/phases.ts";
5
8
  import { AutoSession } from "../auto/session.ts";
9
+ import { readUnitRuntimeRecord, writeUnitRuntimeRecord } from "../unit-runtime.ts";
6
10
 
7
- test("runFinalize clears currentUnit after successful finalize", async () => {
8
- const s = new AutoSession();
9
- s.basePath = "/tmp/gsd-finalize-current-unit";
10
- s.currentUnit = {
11
- type: "execute-task",
12
- id: "M001/S01/T01",
13
- startedAt: Date.now(),
14
- };
11
+ async function runSuccessfulFinalize(s: AutoSession) {
12
+ const unit = s.currentUnit;
13
+ assert.ok(unit, "test setup must provide currentUnit");
14
+
15
+ writeUnitRuntimeRecord(s.basePath, unit.type, unit.id, unit.startedAt, {
16
+ phase: "dispatched",
17
+ });
15
18
 
16
19
  const deps = {
17
20
  clearUnitTimeout() {},
@@ -26,7 +29,7 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
26
29
  postUnitPostVerification: async () => "continue",
27
30
  };
28
31
 
29
- const result = await runFinalize(
32
+ return runFinalize(
30
33
  {
31
34
  ctx: { ui: { notify() {} } },
32
35
  pi: {},
@@ -38,8 +41,8 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
38
41
  nextSeq: () => 1,
39
42
  } as any,
40
43
  {
41
- unitType: "execute-task",
42
- unitId: "M001/S01/T01",
44
+ unitType: unit.type,
45
+ unitId: unit.id,
43
46
  prompt: "",
44
47
  finalPrompt: "",
45
48
  pauseAfterUatDispatch: false,
@@ -55,7 +58,47 @@ test("runFinalize clears currentUnit after successful finalize", async () => {
55
58
  consecutiveFinalizeTimeouts: 0,
56
59
  },
57
60
  );
61
+ }
62
+
63
+ test("runFinalize clears currentUnit after successful finalize", async () => {
64
+ const base = mkdtempSync(join(tmpdir(), "gsd-finalize-current-unit-"));
65
+ const s = new AutoSession();
66
+ s.basePath = base;
67
+ s.currentUnit = {
68
+ type: "execute-task",
69
+ id: "M001/S01/T01",
70
+ startedAt: Date.now(),
71
+ };
72
+
73
+ try {
74
+ const result = await runSuccessfulFinalize(s);
75
+
76
+ assert.equal(result.action, "next");
77
+ assert.equal(s.currentUnit, null);
78
+ } finally {
79
+ rmSync(base, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test("runFinalize marks unit runtime finalized after successful finalize", async () => {
84
+ const base = mkdtempSync(join(tmpdir(), "gsd-finalize-runtime-"));
85
+ const s = new AutoSession();
86
+ const startedAt = Date.now();
87
+ s.basePath = base;
88
+ s.currentUnit = {
89
+ type: "complete-milestone",
90
+ id: "M001",
91
+ startedAt,
92
+ };
93
+
94
+ try {
95
+ const result = await runSuccessfulFinalize(s);
96
+ const runtime = readUnitRuntimeRecord(base, "complete-milestone", "M001");
58
97
 
59
- assert.equal(result.action, "next");
60
- assert.equal(s.currentUnit, null);
98
+ assert.equal(result.action, "next");
99
+ assert.equal(runtime?.phase, "finalized");
100
+ assert.equal(runtime?.lastProgressKind, "finalize-success");
101
+ } finally {
102
+ rmSync(base, { recursive: true, force: true });
103
+ }
61
104
  });
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import { randomUUID } from "node:crypto";
7
7
 
8
- import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder } from "../auto-recovery.ts";
8
+ import { verifyExpectedArtifact, hasImplementationArtifacts, resolveExpectedArtifactPath, diagnoseExpectedArtifact, buildLoopRemediationSteps, writeBlockerPlaceholder, refreshRecoveryDbForArtifact } from "../auto-recovery.ts";
9
9
  import { resolveMilestoneFile } from "../paths.ts";
10
10
  import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertGateRow, insertTask, getMilestoneCommitAttributionShas } from "../gsd-db.ts";
11
11
  import { clearParseCache } from "../files.ts";
@@ -175,6 +175,19 @@ test("resolveExpectedArtifactPath returns correct path for all slice-level types
175
175
  }
176
176
  });
177
177
 
178
+ test("refreshRecoveryDbForArtifact treats missing execute-task DB rows as fatal mismatches", () => {
179
+ makeTmpProject();
180
+
181
+ const result = refreshRecoveryDbForArtifact("execute-task", "M001/S01/T01");
182
+
183
+ assert.deepEqual(result, {
184
+ ok: false,
185
+ fatal: true,
186
+ reason: "execute-task-artifact-db-missing",
187
+ message: "Stuck recovery found execute-task M001/S01/T01 artifacts, but no matching DB task row exists after refresh.",
188
+ });
189
+ });
190
+
178
191
  // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
179
192
 
180
193
  test("diagnoseExpectedArtifact returns description for known types", () => {