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
@@ -23,7 +23,7 @@ import type { CmuxLogLevel } from "../../shared/cmux-events.js";
23
23
  import type { JournalEntry } from "../journal.js";
24
24
  import type { MergeReconcileResult } from "../auto-recovery.js";
25
25
  import type { UokTurnObserver } from "../uok/contracts.js";
26
- import type { PreflightResult } from "../clean-root-preflight.js";
26
+ import type { PostflightResult, PreflightResult } from "../clean-root-preflight.js";
27
27
 
28
28
  type PauseAutoFn = (
29
29
  ctx?: ExtensionContext,
@@ -141,7 +141,7 @@ export interface LoopDeps {
141
141
  milestoneId: string,
142
142
  stashMarker: string | undefined,
143
143
  notify: (message: string, level: "info" | "warning" | "error") => void,
144
- ) => void;
144
+ ) => PostflightResult;
145
145
 
146
146
  // Budget/context/secrets
147
147
  getLedger: () => unknown;
@@ -50,13 +50,14 @@ import {
50
50
  } from "../workflow-logger.js";
51
51
  import { gsdRoot } from "../paths.js";
52
52
  import { atomicWriteSync } from "../atomic-write.js";
53
- import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
53
+ import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
54
54
  import { writeUnitRuntimeRecord } from "../unit-runtime.js";
55
55
  import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
56
56
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
57
57
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
58
- import { isDbAvailable, getMilestoneSlices, refreshOpenDatabaseFromDisk } from "../gsd-db.js";
58
+ import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
59
59
  import type { MinimalModelRegistry } from "../context-budget.js";
60
+ import type { PostflightResult, PreflightResult } from "../clean-root-preflight.js";
60
61
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
61
62
  import { resolveUokFlags } from "../uok/flags.js";
62
63
  import { UokGateRunner } from "../uok/gate-runner.js";
@@ -76,10 +77,15 @@ function isSamePathLocal(a: string, b: string): boolean {
76
77
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
77
78
  }
78
79
 
79
- function refreshPlanSliceRecoveryDbIfNeeded(unitType: string): boolean {
80
- if (unitType !== "plan-slice") return true;
81
- if (!isDbAvailable()) return true;
82
- return refreshOpenDatabaseFromDisk();
80
+ export function shouldDegradeEmptyWorktreeToProjectRoot(
81
+ worktreeClassification: ReturnType<typeof classifyProject>,
82
+ projectRootClassification: ReturnType<typeof classifyProject>,
83
+ ): boolean {
84
+ return (
85
+ worktreeClassification.kind === "greenfield" &&
86
+ projectRootClassification.kind !== "greenfield" &&
87
+ projectRootClassification.kind !== "invalid-repo"
88
+ );
83
89
  }
84
90
 
85
91
  // ─── Session timeout auto-resume state ────────────────────────────────────────
@@ -215,6 +221,117 @@ async function closeoutAndStop(
215
221
  await deps.stopAuto(ctx, pi, reason);
216
222
  }
217
223
 
224
+ async function stopOnPostflightRecoveryNeeded(
225
+ ic: IterationContext,
226
+ result: PostflightResult,
227
+ milestoneId: string,
228
+ ): Promise<{ action: "break"; reason: string } | null> {
229
+ if (!result.needsManualRecovery) return null;
230
+ const { ctx, pi, deps } = ic;
231
+ const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
232
+ ctx.ui.notify(
233
+ `${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`,
234
+ "error",
235
+ );
236
+ await deps.stopAuto(ctx, pi, reason);
237
+ return { action: "break", reason: "postflight-stash-restore-failed" };
238
+ }
239
+
240
+ async function restorePreflightStashOrStop(
241
+ ic: IterationContext,
242
+ preflight: PreflightResult,
243
+ milestoneId: string,
244
+ ): Promise<{ action: "break"; reason: string } | null> {
245
+ if (!preflight.stashPushed) return null;
246
+ const { ctx, s, deps } = ic;
247
+ const result = deps.postflightPopStash(
248
+ s.originalBasePath || s.basePath,
249
+ milestoneId,
250
+ preflight.stashMarker,
251
+ ctx.ui.notify.bind(ctx.ui),
252
+ );
253
+ return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
254
+ }
255
+
256
+ /**
257
+ * Run a milestone merge surrounded by preflight stash + always-on postflight
258
+ * pop. The previous code popped the stash only after a successful merge, which
259
+ * leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
260
+ * leaving the user's pre-merge working tree silently stashed away after a
261
+ * merge-conflict or other merge error. This helper restores the stash on
262
+ * every exit path, then surfaces the merge or stash failure (in priority
263
+ * order) as the loop's stop reason.
264
+ *
265
+ * Returns a `break` action when auto-mode must stop, or `null` when the merge
266
+ * succeeded and the stash (if any) was restored cleanly.
267
+ */
268
+ export async function _runMilestoneMergeWithStashRestore(
269
+ ic: IterationContext,
270
+ milestoneId: string,
271
+ ): Promise<{ action: "break"; reason: string } | null> {
272
+ const { ctx, pi, s, deps } = ic;
273
+
274
+ const preflight = deps.preflightCleanRoot(
275
+ s.originalBasePath || s.basePath,
276
+ milestoneId,
277
+ ctx.ui.notify.bind(ctx.ui),
278
+ );
279
+
280
+ let mergeError: unknown = null;
281
+ try {
282
+ deps.resolver.mergeAndExit(milestoneId, ctx.ui);
283
+ s.milestoneMergedInPhases = true;
284
+ } catch (mergeErr) {
285
+ mergeError = mergeErr;
286
+ }
287
+
288
+ // Always attempt to restore the stashed working tree, even on merge error.
289
+ // postflightPopStash itself does not throw; failures surface via the
290
+ // PostflightResult.needsManualRecovery flag.
291
+ let stashResult: PostflightResult | null = null;
292
+ if (preflight.stashPushed) {
293
+ stashResult = deps.postflightPopStash(
294
+ s.originalBasePath || s.basePath,
295
+ milestoneId,
296
+ preflight.stashMarker,
297
+ ctx.ui.notify.bind(ctx.ui),
298
+ );
299
+ }
300
+
301
+ // Merge failure takes priority over stash recovery — the merge is the
302
+ // authoritative gate. If the stash also needed manual recovery, the user
303
+ // already saw the postflightPopStash notify above.
304
+ if (mergeError) {
305
+ if (mergeError instanceof MergeConflictError) {
306
+ ctx.ui.notify(
307
+ `Merge conflict: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
308
+ "error",
309
+ );
310
+ await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${milestoneId}`);
311
+ return { action: "break", reason: "merge-conflict" };
312
+ }
313
+ logError("engine", "Milestone merge failed with non-conflict error", {
314
+ milestone: milestoneId,
315
+ error: String(mergeError),
316
+ });
317
+ ctx.ui.notify(
318
+ `Merge failed: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`,
319
+ "error",
320
+ );
321
+ await deps.stopAuto(
322
+ ctx,
323
+ pi,
324
+ `Merge error on milestone ${milestoneId}: ${String(mergeError)}`,
325
+ );
326
+ return { action: "break", reason: "merge-failed" };
327
+ }
328
+
329
+ if (stashResult) {
330
+ return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
331
+ }
332
+ return null;
333
+ }
334
+
218
335
  async function emitCancelledUnitEnd(
219
336
  ic: IterationContext,
220
337
  unitType: string,
@@ -651,42 +768,11 @@ export async function runPreDispatch(
651
768
  loopState.recentUnits.length = 0;
652
769
  loopState.stuckRecoveryAttempts = 0;
653
770
 
654
- // Worktree lifecycle on milestone transition — merge current, enter next
655
- // #2909: preflight warn + stash dirty working tree before merge
656
- const preflightTransition = deps.preflightCleanRoot(
657
- s.originalBasePath || s.basePath,
658
- s.currentMilestoneId!,
659
- ctx.ui.notify.bind(ctx.ui),
660
- );
661
- try {
662
- deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
663
- } catch (mergeErr) {
664
- if (mergeErr instanceof MergeConflictError) {
665
- // Real code conflicts — stop the loop instead of retrying forever (#2330)
666
- ctx.ui.notify(
667
- `Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
668
- "error",
669
- );
670
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
671
- return { action: "break", reason: "merge-conflict" };
672
- }
673
- // Non-conflict merge errors — stop auto to avoid advancing with unmerged work
674
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
675
- ctx.ui.notify(
676
- `Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
677
- "error",
678
- );
679
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
680
- return { action: "break", reason: "merge-failed" };
681
- }
682
- // #2909: postflight — restore stashed changes after successful merge
683
- if (preflightTransition.stashPushed) {
684
- deps.postflightPopStash(
685
- s.originalBasePath || s.basePath,
686
- s.currentMilestoneId!,
687
- preflightTransition.stashMarker,
688
- ctx.ui.notify.bind(ctx.ui),
689
- );
771
+ // Worktree lifecycle on milestone transition — merge current, enter next.
772
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
773
+ {
774
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId!);
775
+ if (stop) return stop;
690
776
  }
691
777
 
692
778
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
@@ -764,45 +850,11 @@ export async function runPreDispatch(
764
850
  m.status !== "complete" && m.status !== "parked",
765
851
  );
766
852
  if (incomplete.length === 0 && state.registry.length > 0) {
767
- // All milestones complete — merge milestone branch before stopping
853
+ // All milestones complete — merge milestone branch before stopping.
768
854
  if (s.currentMilestoneId) {
769
- // #2909: preflight warn + stash dirty working tree before merge
770
- const preflightAllComplete = deps.preflightCleanRoot(
771
- s.originalBasePath || s.basePath,
772
- s.currentMilestoneId,
773
- ctx.ui.notify.bind(ctx.ui),
774
- );
775
- try {
776
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
777
- // Prevent stopAuto from attempting the same merge (#2645)
778
- s.milestoneMergedInPhases = true;
779
- } catch (mergeErr) {
780
- if (mergeErr instanceof MergeConflictError) {
781
- ctx.ui.notify(
782
- `Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
783
- "error",
784
- );
785
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
786
- return { action: "break", reason: "merge-conflict" };
787
- }
788
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
789
- ctx.ui.notify(
790
- `Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
791
- "error",
792
- );
793
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
794
- return { action: "break", reason: "merge-failed" };
795
- }
796
- // #2909: postflight — restore stashed changes after successful merge
797
- if (preflightAllComplete.stashPushed) {
798
- deps.postflightPopStash(
799
- s.originalBasePath || s.basePath,
800
- s.currentMilestoneId,
801
- preflightAllComplete.stashMarker,
802
- ctx.ui.notify.bind(ctx.ui),
803
- );
804
- }
805
-
855
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
856
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
857
+ if (stop) return stop;
806
858
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
807
859
  }
808
860
  deps.sendDesktopNotification(
@@ -893,45 +945,11 @@ export async function runPreDispatch(
893
945
 
894
946
  // Terminal: complete
895
947
  if (state.phase === "complete") {
896
- // Milestone merge on complete (before closeout so branch state is clean)
948
+ // Milestone merge on complete (before closeout so branch state is clean).
897
949
  if (s.currentMilestoneId) {
898
- // #2909: preflight warn + stash dirty working tree before merge
899
- const preflightComplete = deps.preflightCleanRoot(
900
- s.originalBasePath || s.basePath,
901
- s.currentMilestoneId,
902
- ctx.ui.notify.bind(ctx.ui),
903
- );
904
- try {
905
- deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
906
- // Prevent stopAuto from attempting the same merge (#2645)
907
- s.milestoneMergedInPhases = true;
908
- } catch (mergeErr) {
909
- if (mergeErr instanceof MergeConflictError) {
910
- ctx.ui.notify(
911
- `Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
912
- "error",
913
- );
914
- await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
915
- return { action: "break", reason: "merge-conflict" };
916
- }
917
- logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
918
- ctx.ui.notify(
919
- `Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
920
- "error",
921
- );
922
- await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
923
- return { action: "break", reason: "merge-failed" };
924
- }
925
- // #2909: postflight — restore stashed changes after successful merge
926
- if (preflightComplete.stashPushed) {
927
- deps.postflightPopStash(
928
- s.originalBasePath || s.basePath,
929
- s.currentMilestoneId,
930
- preflightComplete.stashMarker,
931
- ctx.ui.notify.bind(ctx.ui),
932
- );
933
- }
934
-
950
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
951
+ const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
952
+ if (stop) return stop;
935
953
  // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
936
954
  }
937
955
  deps.sendDesktopNotification(
@@ -1054,6 +1072,65 @@ export async function runDispatch(
1054
1072
  let prompt = dispatchResult.prompt;
1055
1073
  const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
1056
1074
 
1075
+ // Resolve hooks and prior-slice gating before health/stuck accounting so
1076
+ // those checks run against the final dispatch unit.
1077
+ const preDispatchResult = deps.runPreDispatchHooks(
1078
+ unitType,
1079
+ unitId,
1080
+ prompt,
1081
+ s.basePath,
1082
+ );
1083
+ if (preDispatchResult.firedHooks.length > 0) {
1084
+ ctx.ui.notify(
1085
+ `Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
1086
+ "info",
1087
+ );
1088
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
1089
+ }
1090
+ if (preDispatchResult.action === "skip") {
1091
+ ctx.ui.notify(
1092
+ `Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
1093
+ "info",
1094
+ );
1095
+ await new Promise((r) => setImmediate(r));
1096
+ return { action: "continue" };
1097
+ }
1098
+ if (preDispatchResult.action === "replace") {
1099
+ prompt = preDispatchResult.prompt ?? prompt;
1100
+ if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
1101
+ } else if (preDispatchResult.prompt) {
1102
+ prompt = preDispatchResult.prompt;
1103
+ }
1104
+
1105
+ const guardBasePath = _resolveDispatchGuardBasePath(s);
1106
+ const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
1107
+ guardBasePath,
1108
+ deps.getMainBranch(guardBasePath),
1109
+ unitType,
1110
+ unitId,
1111
+ );
1112
+ if (priorSliceBlocker) {
1113
+ await deps.stopAuto(ctx, pi, priorSliceBlocker);
1114
+ debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
1115
+ return { action: "break", reason: "prior-slice-blocker" };
1116
+ }
1117
+
1118
+ // Execute-task needs a real writable checkout. The same health check also
1119
+ // exists in runUnitPhase, but the stuck-window detector runs before that
1120
+ // phase. Check here too so repeated derivations of a broken worktree stop
1121
+ // with the actionable worktree error instead of the generic stuck-loop error.
1122
+ if (s.basePath && unitType === "execute-task") {
1123
+ const gitMarker = join(s.basePath, ".git");
1124
+ const hasGit = deps.existsSync(gitMarker);
1125
+ if (!hasGit) {
1126
+ const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
1127
+ debugLog("autoLoop", { phase: "dispatch-worktree-health-fail", basePath: s.basePath, hasGit });
1128
+ ctx.ui.notify(msg, "error");
1129
+ await deps.stopAuto(ctx, pi, msg);
1130
+ return { action: "break", reason: "worktree-invalid" };
1131
+ }
1132
+ }
1133
+
1057
1134
  // ── Sliding-window stuck detection with graduated recovery ──
1058
1135
  const derivedKey = `${unitType}/${unitId}`;
1059
1136
 
@@ -1100,17 +1177,24 @@ export async function runDispatch(
1100
1177
  level: 1,
1101
1178
  action: "artifact-found",
1102
1179
  });
1103
- ctx.ui.notify(
1104
- `Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
1105
- "info",
1106
- );
1107
- if (!refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
1180
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
1181
+ if (!recoveryDb.ok) {
1108
1182
  ctx.ui.notify(
1109
- `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Keeping stuck state for retry.`,
1183
+ recoveryDb.fatal
1184
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
1185
+ : `${recoveryDb.message} Keeping stuck state for retry.`,
1110
1186
  "warning",
1111
1187
  );
1188
+ if (recoveryDb.fatal) {
1189
+ await deps.pauseAuto(ctx, pi);
1190
+ return { action: "break", reason: recoveryDb.reason };
1191
+ }
1112
1192
  return { action: "continue" };
1113
1193
  }
1194
+ ctx.ui.notify(
1195
+ `Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`,
1196
+ "info",
1197
+ );
1114
1198
  deps.invalidateAllCaches();
1115
1199
  loopState.recentUnits.length = 0;
1116
1200
  loopState.stuckRecoveryAttempts = 0;
@@ -1135,19 +1219,26 @@ export async function runDispatch(
1135
1219
  level: 2,
1136
1220
  action: "artifact-found",
1137
1221
  });
1138
- ctx.ui.notify(
1139
- `Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`,
1140
- "info",
1141
- );
1142
- if (refreshPlanSliceRecoveryDbIfNeeded(unitType)) {
1222
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
1223
+ if (recoveryDb.ok) {
1224
+ ctx.ui.notify(
1225
+ `Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`,
1226
+ "info",
1227
+ );
1143
1228
  loopState.recentUnits.length = 0;
1144
1229
  loopState.stuckRecoveryAttempts = 0;
1145
1230
  return { action: "continue" };
1146
1231
  }
1147
1232
  ctx.ui.notify(
1148
- `Stuck recovery found ${unitType} ${unitId} artifacts, but the DB refresh failed. Stopping for manual recovery.`,
1233
+ recoveryDb.fatal
1234
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
1235
+ : `${recoveryDb.message} Stopping for manual recovery.`,
1149
1236
  "warning",
1150
1237
  );
1238
+ if (recoveryDb.fatal) {
1239
+ await deps.pauseAuto(ctx, pi);
1240
+ return { action: "break", reason: recoveryDb.reason };
1241
+ }
1151
1242
  }
1152
1243
  debugLog("autoLoop", {
1153
1244
  phase: "stuck-detected",
@@ -1181,48 +1272,6 @@ export async function runDispatch(
1181
1272
  }
1182
1273
  }
1183
1274
 
1184
- // Pre-dispatch hooks
1185
- const preDispatchResult = deps.runPreDispatchHooks(
1186
- unitType,
1187
- unitId,
1188
- prompt,
1189
- s.basePath,
1190
- );
1191
- if (preDispatchResult.firedHooks.length > 0) {
1192
- ctx.ui.notify(
1193
- `Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
1194
- "info",
1195
- );
1196
- deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
1197
- }
1198
- if (preDispatchResult.action === "skip") {
1199
- ctx.ui.notify(
1200
- `Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
1201
- "info",
1202
- );
1203
- await new Promise((r) => setImmediate(r));
1204
- return { action: "continue" };
1205
- }
1206
- if (preDispatchResult.action === "replace") {
1207
- prompt = preDispatchResult.prompt ?? prompt;
1208
- if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
1209
- } else if (preDispatchResult.prompt) {
1210
- prompt = preDispatchResult.prompt;
1211
- }
1212
-
1213
- const guardBasePath = _resolveDispatchGuardBasePath(s);
1214
- const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
1215
- guardBasePath,
1216
- deps.getMainBranch(guardBasePath),
1217
- unitType,
1218
- unitId,
1219
- );
1220
- if (priorSliceBlocker) {
1221
- await deps.stopAuto(ctx, pi, priorSliceBlocker);
1222
- debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
1223
- return { action: "break", reason: "prior-slice-blocker" };
1224
- }
1225
-
1226
1275
  return {
1227
1276
  action: "next",
1228
1277
  data: {
@@ -1515,6 +1564,29 @@ export async function runUnitPhase(
1515
1564
  return { action: "break", reason: "worktree-invalid" };
1516
1565
  }
1517
1566
  } else if (projectClassification.kind === "greenfield") {
1567
+ const projectRoot = s.canonicalProjectRoot;
1568
+ if (!isSamePathLocal(s.basePath, projectRoot)) {
1569
+ const projectRootClassification = classifyProject(projectRoot);
1570
+ if (shouldDegradeEmptyWorktreeToProjectRoot(projectClassification, projectRootClassification)) {
1571
+ debugLog("runUnitPhase", {
1572
+ phase: "worktree-health-degrade-to-project-root",
1573
+ worktreePath: s.basePath,
1574
+ projectRoot,
1575
+ worktreeClassification: projectClassification,
1576
+ projectRootClassification,
1577
+ });
1578
+ ctx.ui.notify(
1579
+ `Warning: ${s.basePath} has no project content, but ${projectRoot} does. Continuing in project root because the milestone worktree cannot represent untracked project files.`,
1580
+ "warning",
1581
+ );
1582
+ s.basePath = projectRoot;
1583
+ s.isolationDegraded = true;
1584
+ projectClassification = projectRootClassification;
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ if (projectClassification.kind === "greenfield") {
1518
1590
  debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
1519
1591
  ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
1520
1592
  } else if (projectClassification.kind === "untyped-existing") {
@@ -2265,6 +2337,13 @@ export async function runFinalize(
2265
2337
 
2266
2338
  // Both pre and post verification completed without timeout — reset counter
2267
2339
  loopState.consecutiveFinalizeTimeouts = 0;
2340
+ if (preUnitSnapshot) {
2341
+ writeUnitRuntimeRecord(s.basePath, preUnitSnapshot.type, preUnitSnapshot.id, preUnitSnapshot.startedAt, {
2342
+ phase: "finalized",
2343
+ lastProgressAt: Date.now(),
2344
+ lastProgressKind: "finalize-success",
2345
+ });
2346
+ }
2268
2347
  s.currentUnit = null;
2269
2348
  clearCurrentPhase();
2270
2349
 
@@ -22,6 +22,7 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
22
22
 
23
23
  let _currentResolve: ((result: UnitResult) => void) | null = null;
24
24
  let _sessionSwitchInFlight = false;
25
+ let _pendingSwitchCancellation: { errorContext?: ErrorContext } | null = null;
25
26
 
26
27
  // ─── Setters (needed for cross-module mutation) ─────────────────────────────
27
28
 
@@ -37,6 +38,12 @@ export function _clearCurrentResolve(): void {
37
38
  _currentResolve = null;
38
39
  }
39
40
 
41
+ export function _consumePendingSwitchCancellation(): { errorContext?: ErrorContext } | null {
42
+ const pending = _pendingSwitchCancellation;
43
+ _pendingSwitchCancellation = null;
44
+ return pending;
45
+ }
46
+
40
47
  // ─── resolveAgentEnd ─────────────────────────────────────────────────────────
41
48
 
42
49
  /**
@@ -96,7 +103,7 @@ export function bumpAndResolveSynthetic(reason: string): void {
96
103
  * blocks to ensure the autoLoop is never stuck awaiting a promise that
97
104
  * will never resolve. Safe to call when no resolver is pending (no-op).
98
105
  */
99
- export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
106
+ export function resolveAgentEndCancelled(errorContext?: ErrorContext): boolean {
100
107
  if (_currentResolve) {
101
108
  // Cancellation supersedes the in-flight turn the same way timeout
102
109
  // recovery does — bump the turn epoch so any lingering writes from the
@@ -107,8 +114,22 @@ export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
107
114
  debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
108
115
  const r = _currentResolve;
109
116
  _currentResolve = null;
117
+ _pendingSwitchCancellation = null;
110
118
  r({ status: "cancelled", ...(errorContext ? { errorContext } : {}) });
119
+ return true;
120
+ }
121
+
122
+ if (_sessionSwitchInFlight) {
123
+ bumpTurnGeneration(
124
+ `cancelled-during-switch:${errorContext?.category ?? "unknown"}`,
125
+ );
126
+ _pendingSwitchCancellation = errorContext ? { errorContext } : {};
127
+ debugLog("resolveAgentEndCancelled", { status: "queued-during-switch" });
128
+ return false;
111
129
  }
130
+
131
+ debugLog("resolveAgentEndCancelled", { status: "no-pending-resolve" });
132
+ return false;
112
133
  }
113
134
 
114
135
  // ─── resetPendingResolve (test helper) ───────────────────────────────────────
@@ -120,6 +141,7 @@ export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
120
141
  export function _resetPendingResolve(): void {
121
142
  _currentResolve = null;
122
143
  _sessionSwitchInFlight = false;
144
+ _pendingSwitchCancellation = null;
123
145
  }
124
146
 
125
147
  export function _hasPendingResolveForTest(): boolean {