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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/dist/cli-model-override.d.ts +15 -0
  2. package/dist/cli-model-override.js +21 -0
  3. package/dist/cli.js +1 -18
  4. package/dist/loader.js +6 -4
  5. package/dist/register-agent-bundles.d.ts +11 -2
  6. package/dist/register-agent-bundles.js +18 -4
  7. package/dist/resources/.managed-resources-content-hash +1 -1
  8. package/dist/resources/extensions/ask-user-questions.js +3 -2
  9. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +447 -215
  10. package/dist/resources/extensions/claude-code-cli/turn-assembler.js +33 -1
  11. package/dist/resources/extensions/gsd/auto/closeout.js +215 -0
  12. package/dist/resources/extensions/gsd/auto/dispatch-history.js +21 -6
  13. package/dist/resources/extensions/gsd/auto/dispatch.js +365 -0
  14. package/dist/resources/extensions/gsd/auto/finalize.js +347 -0
  15. package/dist/resources/extensions/gsd/auto/loop.js +4 -1
  16. package/dist/resources/extensions/gsd/auto/milestone-lease-reclaim.js +56 -0
  17. package/dist/resources/extensions/gsd/auto/orchestrator.js +85 -15
  18. package/dist/resources/extensions/gsd/auto/phase-helpers.js +146 -0
  19. package/dist/resources/extensions/gsd/auto/phases.js +17 -2372
  20. package/dist/resources/extensions/gsd/auto/pre-dispatch.js +534 -0
  21. package/dist/resources/extensions/gsd/auto/unit-phase.js +694 -0
  22. package/dist/resources/extensions/gsd/auto/workflow-unit-dispatch.js +1 -1
  23. package/dist/resources/extensions/gsd/auto/worktree-safety-phase.js +125 -0
  24. package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
  25. package/dist/resources/extensions/gsd/auto.js +15 -1
  26. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +37 -7
  27. package/dist/resources/extensions/gsd/commands-mcp-status.js +2 -2
  28. package/dist/resources/extensions/gsd/commands-workflow-templates.js +9 -2
  29. package/dist/resources/extensions/gsd/db/queries.js +30 -0
  30. package/dist/resources/extensions/gsd/doctor-environment.js +256 -125
  31. package/dist/resources/extensions/gsd/guided-flow.js +88 -2
  32. package/dist/resources/extensions/gsd/health-widget.js +87 -28
  33. package/dist/resources/extensions/gsd/mcp-bridge.js +10 -0
  34. package/dist/resources/extensions/gsd/milestone-settlement.js +2 -2
  35. package/dist/resources/extensions/gsd/notifications.js +12 -7
  36. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -1
  38. package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
  39. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -1
  40. package/dist/resources/extensions/gsd/skill-activation.js +3 -6
  41. package/dist/resources/extensions/gsd/state.js +6 -2
  42. package/dist/resources/extensions/gsd/tool-surface-readiness.js +83 -31
  43. package/dist/resources/extensions/gsd/tools/complete-task.js +62 -0
  44. package/dist/resources/extensions/gsd/unit-context-composer.js +1 -1
  45. package/dist/resources/extensions/gsd/unit-registry.js +34 -4
  46. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +2 -0
  47. package/dist/resources/extensions/gsd/workflow-mcp-readiness-cache.js +105 -0
  48. package/dist/resources/extensions/gsd/worktree-safety.js +28 -26
  49. package/dist/resources/extensions/mcp-client/manager.js +6 -1
  50. package/dist/runtime-checks.d.ts +10 -0
  51. package/dist/runtime-checks.js +27 -0
  52. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  53. package/dist/web/standalone/.next/BUILD_ID +1 -1
  54. package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
  55. package/dist/web/standalone/.next/build-manifest.json +2 -2
  56. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  57. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  58. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  66. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/index.html +1 -1
  74. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
  81. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  83. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  84. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  85. package/package.json +2 -2
  86. package/packages/cloud-mcp-gateway/package.json +2 -2
  87. package/packages/contracts/package.json +1 -1
  88. package/packages/daemon/package.json +4 -4
  89. package/packages/gsd-agent-core/dist/sdk.d.ts.map +1 -1
  90. package/packages/gsd-agent-core/dist/sdk.js +6 -4
  91. package/packages/gsd-agent-core/dist/sdk.js.map +1 -1
  92. package/packages/gsd-agent-core/package.json +5 -5
  93. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  94. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  95. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js +10 -0
  96. package/packages/gsd-agent-modes/dist/modes/interactive/components/settings-selector.js.map +1 -1
  97. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +8 -0
  98. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  99. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +50 -6
  100. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  101. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts +2 -0
  102. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.d.ts.map +1 -1
  103. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +34 -5
  104. package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
  105. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts +1 -0
  106. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  107. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +12 -0
  108. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  109. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.d.ts.map +1 -1
  110. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js +4 -0
  111. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-selectors-settings.js.map +1 -1
  112. package/packages/gsd-agent-modes/package.json +7 -7
  113. package/packages/mcp-server/README.md +12 -3
  114. package/packages/mcp-server/dist/cli-runner.d.ts +40 -0
  115. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -0
  116. package/packages/mcp-server/dist/cli-runner.js +137 -0
  117. package/packages/mcp-server/dist/cli-runner.js.map +1 -0
  118. package/packages/mcp-server/dist/cli.js +2 -58
  119. package/packages/mcp-server/dist/cli.js.map +1 -1
  120. package/packages/mcp-server/dist/pid-registry.d.ts +46 -0
  121. package/packages/mcp-server/dist/pid-registry.d.ts.map +1 -0
  122. package/packages/mcp-server/dist/pid-registry.js +452 -0
  123. package/packages/mcp-server/dist/pid-registry.js.map +1 -0
  124. package/packages/mcp-server/dist/probe-mode.d.ts +4 -0
  125. package/packages/mcp-server/dist/probe-mode.d.ts.map +1 -0
  126. package/packages/mcp-server/dist/probe-mode.js +10 -0
  127. package/packages/mcp-server/dist/probe-mode.js.map +1 -0
  128. package/packages/mcp-server/dist/stdio-watchdog.d.ts +8 -0
  129. package/packages/mcp-server/dist/stdio-watchdog.d.ts.map +1 -0
  130. package/packages/mcp-server/dist/stdio-watchdog.js +40 -0
  131. package/packages/mcp-server/dist/stdio-watchdog.js.map +1 -0
  132. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  133. package/packages/mcp-server/dist/workflow-tools.js +62 -43
  134. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  135. package/packages/mcp-server/package.json +5 -5
  136. package/packages/native/package.json +1 -1
  137. package/packages/pi-agent-core/dist/agent-loop.js +43 -2
  138. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  139. package/packages/pi-agent-core/package.json +1 -1
  140. package/packages/pi-ai/package.json +1 -1
  141. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  142. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  144. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  145. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/theme/theme.js +45 -17
  147. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  148. package/packages/pi-coding-agent/package.json +7 -7
  149. package/packages/pi-tui/dist/index.d.ts +1 -1
  150. package/packages/pi-tui/dist/index.d.ts.map +1 -1
  151. package/packages/pi-tui/dist/index.js +1 -1
  152. package/packages/pi-tui/dist/index.js.map +1 -1
  153. package/packages/pi-tui/dist/terminal-image.d.ts +33 -0
  154. package/packages/pi-tui/dist/terminal-image.d.ts.map +1 -1
  155. package/packages/pi-tui/dist/terminal-image.js +54 -2
  156. package/packages/pi-tui/dist/terminal-image.js.map +1 -1
  157. package/packages/pi-tui/dist/tui.d.ts +8 -0
  158. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  159. package/packages/pi-tui/dist/tui.js +63 -18
  160. package/packages/pi-tui/dist/tui.js.map +1 -1
  161. package/packages/pi-tui/dist/utils.d.ts.map +1 -1
  162. package/packages/pi-tui/dist/utils.js +110 -36
  163. package/packages/pi-tui/dist/utils.js.map +1 -1
  164. package/packages/pi-tui/package.json +2 -2
  165. package/packages/rpc-client/package.json +2 -2
  166. package/pkg/dist/theme/theme.d.ts.map +1 -1
  167. package/pkg/dist/theme/theme.js +45 -17
  168. package/pkg/dist/theme/theme.js.map +1 -1
  169. package/pkg/package.json +1 -1
  170. package/src/resources/extensions/ask-user-questions.ts +7 -2
  171. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +531 -226
  172. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +672 -7
  173. package/src/resources/extensions/claude-code-cli/turn-assembler.ts +38 -1
  174. package/src/resources/extensions/gsd/auto/closeout.ts +309 -0
  175. package/src/resources/extensions/gsd/auto/dispatch-history.ts +22 -6
  176. package/src/resources/extensions/gsd/auto/dispatch.ts +449 -0
  177. package/src/resources/extensions/gsd/auto/finalize.ts +445 -0
  178. package/src/resources/extensions/gsd/auto/loop.ts +4 -1
  179. package/src/resources/extensions/gsd/auto/milestone-lease-reclaim.ts +74 -0
  180. package/src/resources/extensions/gsd/auto/orchestrator.ts +95 -15
  181. package/src/resources/extensions/gsd/auto/phase-helpers.ts +199 -0
  182. package/src/resources/extensions/gsd/auto/phases.ts +58 -3061
  183. package/src/resources/extensions/gsd/auto/pre-dispatch.ts +704 -0
  184. package/src/resources/extensions/gsd/auto/unit-phase.ts +910 -0
  185. package/src/resources/extensions/gsd/auto/workflow-unit-dispatch.ts +1 -1
  186. package/src/resources/extensions/gsd/auto/worktree-safety-phase.ts +149 -0
  187. package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
  188. package/src/resources/extensions/gsd/auto.ts +20 -1
  189. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +56 -6
  190. package/src/resources/extensions/gsd/commands-mcp-status.ts +2 -2
  191. package/src/resources/extensions/gsd/commands-workflow-templates.ts +11 -4
  192. package/src/resources/extensions/gsd/db/queries.ts +29 -0
  193. package/src/resources/extensions/gsd/doctor-environment.ts +267 -142
  194. package/src/resources/extensions/gsd/guided-flow.ts +128 -2
  195. package/src/resources/extensions/gsd/health-widget.ts +91 -27
  196. package/src/resources/extensions/gsd/mcp-bridge.ts +39 -0
  197. package/src/resources/extensions/gsd/milestone-settlement.ts +2 -2
  198. package/src/resources/extensions/gsd/notifications.ts +13 -6
  199. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  200. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -1
  201. package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
  202. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -1
  203. package/src/resources/extensions/gsd/skill-activation.ts +3 -6
  204. package/src/resources/extensions/gsd/state.ts +7 -1
  205. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +1 -1
  206. package/src/resources/extensions/gsd/tests/auto-blocked-remediation-message.test.ts +1 -1
  207. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +206 -22
  208. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +76 -12
  209. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +1 -1
  210. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +77 -1
  211. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +2 -1
  212. package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +169 -1
  213. package/src/resources/extensions/gsd/tests/complete-task.test.ts +141 -5
  214. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +2 -1
  215. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +36 -0
  216. package/src/resources/extensions/gsd/tests/dispatch-history.test.ts +55 -0
  217. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +8 -0
  218. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +117 -91
  219. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +113 -0
  220. package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +16 -0
  221. package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +15 -0
  222. package/src/resources/extensions/gsd/tests/integration/doctor-environment-async.test.ts +104 -0
  223. package/src/resources/extensions/gsd/tests/integration/run-uat.test.ts +18 -0
  224. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +47 -16
  225. package/src/resources/extensions/gsd/tests/mcp-readiness-preflight.test.ts +205 -0
  226. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +6 -5
  227. package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +1 -1
  228. package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +1 -1
  229. package/src/resources/extensions/gsd/tests/milestone-settlement.test.ts +92 -0
  230. package/src/resources/extensions/gsd/tests/milestone-transition-state-rebuild.test.ts +1 -1
  231. package/src/resources/extensions/gsd/tests/notifications.test.ts +64 -9
  232. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +2 -2
  233. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +5 -0
  234. package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +1 -1
  235. package/src/resources/extensions/gsd/tests/phases-terminal-complete-idempotent.test.ts +1 -1
  236. package/src/resources/extensions/gsd/tests/plan-gate-failed-doctor-heal-hint.test.ts +3 -3
  237. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +10 -2
  238. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +2 -4
  239. package/src/resources/extensions/gsd/tests/remote-notification-from-desktop.test.ts +31 -81
  240. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +7 -1
  241. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +20 -17
  242. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +7 -3
  243. package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +1 -1
  244. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -2
  245. package/src/resources/extensions/gsd/tests/tool-surface-readiness.test.ts +184 -10
  246. package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +1 -1
  247. package/src/resources/extensions/gsd/tests/workflow-mcp-readiness-cache.test.ts +119 -0
  248. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +65 -2
  249. package/src/resources/extensions/gsd/tests/workflow-phase-contract-matrix.test.ts +332 -0
  250. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +92 -0
  251. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +1 -1
  252. package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +1 -1
  253. package/src/resources/extensions/gsd/tests/worktree-safety-phase.test.ts +100 -0
  254. package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +72 -0
  255. package/src/resources/extensions/gsd/tool-surface-readiness.ts +126 -19
  256. package/src/resources/extensions/gsd/tools/complete-task.ts +87 -0
  257. package/src/resources/extensions/gsd/unit-context-composer.ts +1 -1
  258. package/src/resources/extensions/gsd/unit-registry.ts +34 -4
  259. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -0
  260. package/src/resources/extensions/gsd/workflow-mcp-readiness-cache.ts +150 -0
  261. package/src/resources/extensions/gsd/worktree-safety.ts +41 -39
  262. package/src/resources/extensions/mcp-client/manager.ts +7 -1
  263. /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_buildManifest.js +0 -0
  264. /package/dist/web/standalone/.next/static/{FBNo5cT_chy7YNoAQsU3o → xyMkEaICFHJoa98VgJyzY}/_ssgManifest.js +0 -0
@@ -0,0 +1,704 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Auto-loop pre-dispatch phase.
3
+
4
+ import { join } from "node:path";
5
+ import { existsSync, cpSync } from "node:fs";
6
+ import { basename } from "node:path";
7
+ import { UokGateRunner } from "../uok/gate-runner.js";
8
+ import { resolveUokFlags } from "../uok/flags.js";
9
+ import {
10
+ ensurePlanV2Graph,
11
+ isEmptyPlanV2GraphResult,
12
+ isMissingFinalizedContextResult,
13
+ } from "../uok/plan-v2.js";
14
+ import { getEligibleSlices } from "../slice-parallel-eligibility.js";
15
+ import { isSliceParallelActive, startSliceParallel } from "../slice-parallel-orchestrator.js";
16
+ import { reconcileBeforeSpawn } from "../state-reconciliation.js";
17
+ import {
18
+ countUnmappedActiveRequirements,
19
+ formatCompletePhaseNextAction,
20
+ } from "../requirements-backlog.js";
21
+ import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
22
+ import { getIsolationMode } from "../preferences.js";
23
+ import { gsdRoot } from "../paths.js";
24
+ import { atomicWriteSync } from "../atomic-write.js";
25
+ import { logWarning } from "../workflow-logger.js";
26
+ import { debugLog } from "../debug-logger.js";
27
+ import {
28
+ persistStuckRecoveryAttempts,
29
+ _resolveDispatchGuardBasePath,
30
+ shouldRunPlanV2Gate,
31
+ isSamePathLocal,
32
+ } from "./phase-helpers.js";
33
+ import {
34
+ closeoutAndStop,
35
+ generateMilestoneReport,
36
+ _runMilestoneMergeOnceWithStashRestore,
37
+ shouldSkipTerminalMilestoneCloseout,
38
+ } from "./closeout.js";
39
+ import type { IterationContext, LoopState, PhaseResult, PreDispatchData } from "./types.js";
40
+
41
+ type BlockerKind = "needs-remediation-dead-end" | "other";
42
+
43
+ function classifyBlocker(blocker: string): BlockerKind {
44
+ const normalized = blocker.toLowerCase();
45
+ if (normalized.includes("needs-remediation") && normalized.includes("all slices are complete")) {
46
+ return "needs-remediation-dead-end";
47
+ }
48
+ return "other";
49
+ }
50
+
51
+ function sanitizeBlockerForUser(blocker: string): string {
52
+ return blocker.replaceAll("gsd_reassess_roadmap", "/gsd dispatch reassess");
53
+ }
54
+
55
+ /**
56
+ * Formats blocked resume guidance for users, ensuring internal tool names are
57
+ * never surfaced in notification text.
58
+ */
59
+ function formatBlockedResumeMessage(blockers: string[]): string {
60
+ const classifiedBlockers = blockers.map((blocker) => ({
61
+ blocker: sanitizeBlockerForUser(blocker),
62
+ kind: classifyBlocker(blocker),
63
+ }));
64
+ const hasNeedsRemediationDeadEnd = classifiedBlockers.some(
65
+ (classifiedBlocker) => classifiedBlocker.kind === "needs-remediation-dead-end"
66
+ );
67
+ if (hasNeedsRemediationDeadEnd) {
68
+ return "Blocked: milestone validation requires remediation but all slices are complete. Run /gsd dispatch reassess to add remediation slices, then /gsd auto to continue.";
69
+ }
70
+ return `Blocked: ${classifiedBlockers.map((classifiedBlocker) => classifiedBlocker.blocker).join(", ")}. Fix and run /gsd auto to resume.`;
71
+ }
72
+
73
+ /**
74
+ * Phase 1: Pre-dispatch — resource guard, health gate, state derivation,
75
+ * milestone transition, terminal conditions.
76
+ * Returns break to exit the loop, or next with PreDispatchData on success.
77
+ */
78
+ export async function runPreDispatch(
79
+ ic: IterationContext,
80
+ loopState: LoopState,
81
+ ): Promise<PhaseResult<PreDispatchData>> {
82
+ const { ctx, pi, s, deps, prefs } = ic;
83
+ const uokFlags = resolveUokFlags(prefs);
84
+ const runPreDispatchGate = async (input: {
85
+ gateId: string;
86
+ gateType: string;
87
+ outcome: "pass" | "fail" | "retry" | "manual-attention";
88
+ failureClass: "none" | "policy" | "input" | "execution" | "artifact" | "verification" | "closeout" | "git" | "timeout" | "manual-attention" | "unknown";
89
+ rationale: string;
90
+ findings?: string;
91
+ milestoneId?: string;
92
+ }): Promise<void> => {
93
+ if (!uokFlags.gates) return;
94
+ const gateRunner = new UokGateRunner();
95
+ gateRunner.register({
96
+ id: input.gateId,
97
+ type: input.gateType,
98
+ execute: async () => ({
99
+ outcome: input.outcome,
100
+ failureClass: input.failureClass,
101
+ rationale: input.rationale,
102
+ findings: input.findings ?? "",
103
+ }),
104
+ });
105
+ await gateRunner.run(input.gateId, {
106
+ basePath: s.basePath,
107
+ traceId: `pre-dispatch:${ic.flowId}`,
108
+ turnId: `iter-${ic.iteration}`,
109
+ milestoneId: input.milestoneId ?? s.currentMilestoneId ?? undefined,
110
+ unitType: "pre-dispatch",
111
+ unitId: `iter-${ic.iteration}`,
112
+ });
113
+ };
114
+
115
+ // Resource version guard
116
+ const staleMsg = deps.checkResourcesStale(s.resourceVersionOnStart);
117
+ if (staleMsg) {
118
+ await runPreDispatchGate({
119
+ gateId: "resource-version-guard",
120
+ gateType: "policy",
121
+ outcome: "fail",
122
+ failureClass: "policy",
123
+ rationale: "resource version guard blocked dispatch",
124
+ findings: staleMsg,
125
+ });
126
+ await deps.stopAuto(ctx, pi, staleMsg);
127
+ debugLog("autoLoop", { phase: "exit", reason: "resources-stale" });
128
+ return { action: "break", reason: "resources-stale" };
129
+ }
130
+ await runPreDispatchGate({
131
+ gateId: "resource-version-guard",
132
+ gateType: "policy",
133
+ outcome: "pass",
134
+ failureClass: "none",
135
+ rationale: "resource version guard passed",
136
+ });
137
+
138
+ deps.invalidateAllCaches();
139
+ s.lastPromptCharCount = undefined;
140
+ s.lastBaselineCharCount = undefined;
141
+
142
+ // Pre-dispatch health gate
143
+ try {
144
+ const expectedCurrentUnit = null;
145
+ const healthGate = await deps.preDispatchHealthGate(s.basePath);
146
+ if (healthGate.fixesApplied.length > 0) {
147
+ ctx.ui.notify(
148
+ `Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`,
149
+ "info",
150
+ );
151
+ }
152
+ if (!healthGate.proceed) {
153
+ await runPreDispatchGate({
154
+ gateId: "pre-dispatch-health-gate",
155
+ gateType: "execution",
156
+ outcome: "manual-attention",
157
+ failureClass: "manual-attention",
158
+ rationale: "pre-dispatch health gate blocked dispatch",
159
+ findings: healthGate.reason,
160
+ });
161
+ ctx.ui.notify(
162
+ healthGate.reason || "Pre-dispatch health check failed — run /gsd doctor for details.",
163
+ "error",
164
+ );
165
+ await deps.pauseAuto(ctx, pi, undefined, { expectedCurrentUnit });
166
+ debugLog("autoLoop", { phase: "exit", reason: "health-gate-failed" });
167
+ return { action: "break", reason: "health-gate-failed" };
168
+ }
169
+ await runPreDispatchGate({
170
+ gateId: "pre-dispatch-health-gate",
171
+ gateType: "execution",
172
+ outcome: "pass",
173
+ failureClass: "none",
174
+ rationale: "pre-dispatch health gate passed",
175
+ findings: healthGate.fixesApplied.length > 0 ? healthGate.fixesApplied.join(", ") : "",
176
+ });
177
+ } catch (e) {
178
+ await runPreDispatchGate({
179
+ gateId: "pre-dispatch-health-gate",
180
+ gateType: "execution",
181
+ outcome: "manual-attention",
182
+ failureClass: "manual-attention",
183
+ rationale: "pre-dispatch health gate threw unexpectedly",
184
+ findings: String(e),
185
+ });
186
+ logWarning("engine", "Pre-dispatch health gate threw unexpectedly", { error: String(e) });
187
+ }
188
+
189
+ // Sync project root artifacts into worktree
190
+ if (
191
+ s.originalBasePath &&
192
+ !isSamePathLocal(s.basePath, s.originalBasePath) &&
193
+ s.currentMilestoneId &&
194
+ s.scope
195
+ ) {
196
+ deps.worktreeProjection.projectRootToWorktree(s.scope);
197
+ }
198
+
199
+ // Derive state — use canonical project root so the cache key is stable
200
+ // across worktree↔project-root path-form alternation. See PR #5236
201
+ // (workspace handle infrastructure) and the Phase A pt 2 plan.
202
+ let state = await deps.deriveState(s.canonicalProjectRoot);
203
+ const { getDeepStageGate } = await import("../auto-dispatch.js");
204
+ const deepStageGate = getDeepStageGate(prefs, s.basePath);
205
+ const canRunDeepSetupGate =
206
+ state.phase === "pre-planning" ||
207
+ state.phase === "needs-discussion" ||
208
+ state.phase === "planning";
209
+ if (
210
+ canRunDeepSetupGate &&
211
+ (deepStageGate.status === "pending" || deepStageGate.status === "blocked")
212
+ ) {
213
+ debugLog("autoLoop", {
214
+ phase: "deep-project-stage-gate",
215
+ stage: deepStageGate.stage,
216
+ status: deepStageGate.status,
217
+ reason: deepStageGate.reason,
218
+ });
219
+ return {
220
+ action: "next",
221
+ data: {
222
+ state: {
223
+ ...state,
224
+ phase: "pre-planning",
225
+ activeMilestone: null,
226
+ activeSlice: null,
227
+ activeTask: null,
228
+ nextAction: deepStageGate.reason,
229
+ },
230
+ mid: "PROJECT",
231
+ midTitle: "Project setup",
232
+ },
233
+ };
234
+ }
235
+
236
+ if (uokFlags.planV2 && shouldRunPlanV2Gate(state.phase)) {
237
+ let compiled = ensurePlanV2Graph(s.basePath, state);
238
+ if (isEmptyPlanV2GraphResult(compiled)) {
239
+ deps.invalidateAllCaches();
240
+ state = await deps.deriveState(s.canonicalProjectRoot);
241
+ compiled = shouldRunPlanV2Gate(state.phase)
242
+ ? ensurePlanV2Graph(s.basePath, state)
243
+ : {
244
+ ok: true,
245
+ reason: "empty plan-v2 graph recovered by state rederive",
246
+ nodeCount: 0,
247
+ };
248
+ }
249
+ if (!compiled.ok) {
250
+ const reason = compiled.reason ?? "Plan v2 compilation failed";
251
+ if (isMissingFinalizedContextResult(compiled)) {
252
+ await runPreDispatchGate({
253
+ gateId: "plan-v2-gate",
254
+ gateType: "policy",
255
+ outcome: "pass",
256
+ failureClass: "none",
257
+ rationale: "plan v2 missing context recovery deferred to dispatch",
258
+ findings: reason,
259
+ milestoneId: state.activeMilestone?.id ?? undefined,
260
+ });
261
+ } else {
262
+ await runPreDispatchGate({
263
+ gateId: "plan-v2-gate",
264
+ gateType: "policy",
265
+ outcome: "manual-attention",
266
+ failureClass: "manual-attention",
267
+ rationale: "plan v2 compile gate failed",
268
+ findings: reason,
269
+ milestoneId: state.activeMilestone?.id ?? undefined,
270
+ });
271
+ ctx.ui.notify(`Plan gate failed-closed: ${reason}\n\nIf this keeps happening, try: /gsd doctor heal`, "error");
272
+ await deps.pauseAuto(ctx, pi);
273
+ return { action: "break", reason: "plan-v2-gate-failed" };
274
+ }
275
+ }
276
+ if (compiled.ok) {
277
+ await runPreDispatchGate({
278
+ gateId: "plan-v2-gate",
279
+ gateType: "policy",
280
+ outcome: "pass",
281
+ failureClass: "none",
282
+ rationale: "plan v2 compile gate passed",
283
+ milestoneId: state.activeMilestone?.id ?? undefined,
284
+ });
285
+ }
286
+ }
287
+ deps.syncCmuxSidebar(prefs, state);
288
+ let mid = state.activeMilestone?.id;
289
+ let midTitle = state.activeMilestone?.title;
290
+ debugLog("autoLoop", {
291
+ phase: "state-derived",
292
+ iteration: ic.iteration,
293
+ mid,
294
+ statePhase: state.phase,
295
+ });
296
+
297
+ // ── Slice-level parallelism gate (#2340) ─────────────────────────────
298
+ // When slice_parallel is enabled, check if multiple slices are eligible
299
+ // for parallel execution. If so, dispatch them in parallel and stop the
300
+ // sequential loop. Workers are spawned via slice-parallel-orchestrator.ts.
301
+ if (
302
+ prefs?.slice_parallel?.enabled &&
303
+ mid &&
304
+ !process.env.GSD_PARALLEL_WORKER &&
305
+ isDbAvailable()
306
+ ) {
307
+ try {
308
+ const projectRoot = _resolveDispatchGuardBasePath(s);
309
+ if (isSliceParallelActive(projectRoot)) {
310
+ ctx.ui.notify("Slice-parallel: workers are still running; waiting for completion before next dispatch.", "info");
311
+ await new Promise<void>((resolve) => setTimeout(resolve, 1000));
312
+ return { action: "continue" };
313
+ }
314
+ const dbSlices = getMilestoneSlices(mid);
315
+ if (dbSlices.length > 0) {
316
+ const doneIds = new Set(dbSlices.filter(sl => sl.status === "complete" || sl.status === "done").map(sl => sl.id));
317
+ const sliceInputs = dbSlices.map(sl => ({
318
+ id: sl.id,
319
+ done: doneIds.has(sl.id),
320
+ depends: sl.depends ?? [],
321
+ }));
322
+ const eligible = getEligibleSlices(sliceInputs, doneIds);
323
+ if (eligible.length > 1) {
324
+ debugLog("autoLoop", {
325
+ phase: "slice-parallel-dispatch",
326
+ iteration: ic.iteration,
327
+ mid,
328
+ eligibleSlices: eligible.map(e => e.id),
329
+ });
330
+ ctx.ui.notify(
331
+ `Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`,
332
+ "info",
333
+ );
334
+ // ADR-017 #5707: reconcile before spawning so each worker doesn't
335
+ // independently race on the same drift. Failure aborts the spawn.
336
+ const spawnGate = await reconcileBeforeSpawn(projectRoot);
337
+ if (!spawnGate.ok) {
338
+ ctx.ui.notify(
339
+ `Slice-parallel: aborting spawn — ${spawnGate.reason}`,
340
+ "error",
341
+ );
342
+ return { action: "break", reason: `slice-parallel-reconciliation-failed: ${spawnGate.reason}` };
343
+ }
344
+ const result = await startSliceParallel(
345
+ projectRoot,
346
+ mid,
347
+ eligible,
348
+ {
349
+ maxWorkers: prefs.slice_parallel.max_workers ?? 2,
350
+ useExecutionGraph: uokFlags.executionGraph,
351
+ },
352
+ );
353
+ if (result.started.length > 0) {
354
+ ctx.ui.notify(
355
+ `Slice-parallel: started ${result.started.length} worker(s): ${result.started.join(", ")}.`,
356
+ "info",
357
+ );
358
+ return { action: "continue" };
359
+ }
360
+ if (result.errors.length > 0) {
361
+ const detail = result.errors
362
+ .map((err) => `${err.sid}: ${err.error}`)
363
+ .join("; ");
364
+ ctx.ui.notify(
365
+ `Slice-parallel startup failed; falling back to sequential execution. ${detail}`,
366
+ "warning",
367
+ );
368
+ }
369
+ // Fall through to sequential if no workers started
370
+ }
371
+ }
372
+ } catch (err) {
373
+ debugLog("autoLoop", {
374
+ phase: "slice-parallel-check-error",
375
+ error: err instanceof Error ? err.message : String(err),
376
+ });
377
+ // Non-fatal — fall through to sequential dispatch
378
+ }
379
+ }
380
+
381
+ // ── Milestone transition ────────────────────────────────────────────
382
+ if (mid && s.currentMilestoneId && mid !== s.currentMilestoneId) {
383
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "milestone-transition", data: { from: s.currentMilestoneId, to: mid } });
384
+ ctx.ui.notify(
385
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}: ${midTitle}.`,
386
+ "info",
387
+ );
388
+ deps.sendDesktopNotification(
389
+ "GSD",
390
+ `Milestone ${s.currentMilestoneId} complete!`,
391
+ "success",
392
+ "milestone",
393
+ basename(s.originalBasePath || s.basePath),
394
+ );
395
+ deps.logCmuxEvent(
396
+ prefs,
397
+ `Milestone ${s.currentMilestoneId} complete. Advancing to ${mid}.`,
398
+ "success",
399
+ );
400
+
401
+ const vizPrefs = prefs;
402
+ if (vizPrefs?.auto_visualize) {
403
+ ctx.ui.notify("Run /gsd visualize to see progress overview.", "info");
404
+ }
405
+ if (vizPrefs?.auto_report !== false) {
406
+ try {
407
+ await generateMilestoneReport(s, ctx, s.currentMilestoneId!);
408
+ } catch (err) {
409
+ ctx.ui.notify(
410
+ `Report generation failed: ${err instanceof Error ? err.message : String(err)}`,
411
+ "warning",
412
+ );
413
+ }
414
+ }
415
+
416
+ // Reset dispatch counters for new milestone
417
+ s.unitDispatchCount.clear();
418
+ s.unitRecoveryCount.clear();
419
+ s.unitLifetimeDispatches.clear();
420
+ loopState.recentUnits.length = 0;
421
+ loopState.stuckRecoveryAttempts = 0;
422
+ persistStuckRecoveryAttempts(s, loopState);
423
+
424
+ // Worktree lifecycle on milestone transition — merge current, enter next.
425
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
426
+ {
427
+ const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId!);
428
+ if (stop) return stop;
429
+ }
430
+
431
+ // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
432
+
433
+ deps.invalidateAllCaches();
434
+
435
+ state = await deps.deriveState(s.canonicalProjectRoot);
436
+ mid = state.activeMilestone?.id;
437
+ midTitle = state.activeMilestone?.title;
438
+
439
+ if (mid) {
440
+ if (deps.getIsolationMode(s.basePath) !== "none") {
441
+ deps.captureIntegrationBranch(s.basePath, mid);
442
+ }
443
+ const enterResult = deps.lifecycle.enterMilestone(mid, ctx.ui);
444
+ if (!enterResult.ok) {
445
+ ctx.ui.notify(
446
+ `Milestone transition stopped: failed to enter ${mid} (${enterResult.reason}).`,
447
+ "error",
448
+ );
449
+ if (enterResult.reason === "lease-conflict") {
450
+ await deps.pauseAuto(ctx, pi);
451
+ }
452
+ return { action: "break", reason: "milestone-enter-failed" };
453
+ }
454
+ } else {
455
+ // mid is undefined — no milestone to capture integration branch for
456
+ }
457
+
458
+ const pendingIds = state.registry
459
+ .filter(
460
+ (m: { status: string }) =>
461
+ m.status !== "complete" && m.status !== "parked",
462
+ )
463
+ .map((m: { id: string }) => m.id);
464
+ deps.pruneQueueOrder(s.basePath, pendingIds);
465
+
466
+ // Archive the old completed-units.json instead of wiping it (#2313).
467
+ try {
468
+ const completedKeysPath = join(gsdRoot(s.basePath), "completed-units.json");
469
+ if (existsSync(completedKeysPath) && s.currentMilestoneId) {
470
+ const archivePath = join(
471
+ gsdRoot(s.basePath),
472
+ `completed-units-${s.currentMilestoneId}.json`,
473
+ );
474
+ cpSync(completedKeysPath, archivePath);
475
+ }
476
+ atomicWriteSync(completedKeysPath, JSON.stringify([], null, 2));
477
+ } catch (e) {
478
+ logWarning("engine", "Failed to archive completed-units on milestone transition", { error: String(e) });
479
+ }
480
+
481
+ // Rebuild STATE.md immediately so it reflects the new active milestone.
482
+ // This bypasses the 30-second throttle in the normal rebuild path —
483
+ // milestone transitions are rare and important enough to warrant an
484
+ // immediate write.
485
+ try {
486
+ await deps.rebuildState(s.basePath);
487
+ } catch (e) {
488
+ logWarning("engine", "STATE.md rebuild failed after milestone transition", { error: String(e) });
489
+ }
490
+
491
+ // Re-project ROADMAP/PLAN markdown from the authoritative DB. Worktree DB
492
+ // reconciliation during merge can leave main-branch markdown stale relative
493
+ // to gsd.db (the 3M/3S/10T vs 3M/5S/16T drift class at /gsd startup).
494
+ try {
495
+ const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
496
+ await rebuildMarkdownProjectionsFromDb(s.canonicalProjectRoot);
497
+ if (s.basePath !== s.canonicalProjectRoot) {
498
+ await rebuildMarkdownProjectionsFromDb(s.basePath);
499
+ }
500
+ } catch (e) {
501
+ logWarning("engine", "markdown projection rebuild failed after milestone transition", { error: String(e) });
502
+ }
503
+ }
504
+
505
+ if (mid) {
506
+ s.currentMilestoneId = mid;
507
+ deps.setActiveMilestoneId(s.basePath, mid);
508
+ }
509
+
510
+ // ── Terminal conditions ──────────────────────────────────────────────
511
+
512
+ if (state.phase === "complete") {
513
+ const closeoutSkip = await shouldSkipTerminalMilestoneCloseout(s, state, mid);
514
+ if (closeoutSkip.skip) {
515
+ debugLog("autoLoop", { phase: "complete", reason: "milestone-already-closed", milestoneId: closeoutSkip.milestoneId });
516
+ return { action: "break", reason: "milestone-complete" };
517
+ }
518
+ }
519
+
520
+ if (!mid) {
521
+ if (s.currentUnit) {
522
+ await deps.closeoutUnit(
523
+ ctx,
524
+ s.basePath,
525
+ s.currentUnit.type,
526
+ s.currentUnit.id,
527
+ s.currentUnit.startedAt,
528
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
529
+ );
530
+ }
531
+
532
+ const incomplete = state.registry.filter(
533
+ (m: { status: string }) =>
534
+ m.status !== "complete" && m.status !== "parked",
535
+ );
536
+ if (incomplete.length === 0 && state.registry.length > 0) {
537
+ // All milestones complete — merge milestone branch before stopping.
538
+ if (s.currentMilestoneId) {
539
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
540
+ const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId);
541
+ if (stop) return stop;
542
+ // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
543
+ }
544
+ const unmappedActive = countUnmappedActiveRequirements();
545
+ const completionStopReason = formatCompletePhaseNextAction(unmappedActive);
546
+ deps.sendDesktopNotification(
547
+ "GSD",
548
+ unmappedActive > 0 ? "All milestones complete — requirements backlog remains" : "All milestones complete!",
549
+ "success",
550
+ "milestone",
551
+ basename(s.originalBasePath || s.basePath),
552
+ );
553
+ deps.logCmuxEvent(
554
+ prefs,
555
+ completionStopReason,
556
+ "success",
557
+ );
558
+ await deps.stopAuto(ctx, pi, completionStopReason, {
559
+ completionWidget: {
560
+ milestoneId: s.currentMilestoneId,
561
+ milestoneTitle: midTitle,
562
+ allMilestonesComplete: true,
563
+ },
564
+ });
565
+ } else if (incomplete.length === 0 && state.registry.length === 0) {
566
+ // Empty registry — no milestones visible, likely a path resolution bug
567
+ const diag = `basePath=${s.basePath}, phase=${state.phase}`;
568
+ ctx.ui.notify(
569
+ `No milestones visible in current scope. Possible path resolution issue.\n Diagnostic: ${diag}`,
570
+ "error",
571
+ );
572
+ await deps.stopAuto(
573
+ ctx,
574
+ pi,
575
+ `No milestones found — check basePath resolution`,
576
+ );
577
+ } else if (state.phase === "blocked") {
578
+ const blockedResumeMessage = formatBlockedResumeMessage(state.blockers);
579
+ // Pause instead of hard-stop so the session is resumable with `/gsd auto`.
580
+ // Hard-stop here was causing premature termination when slice dependencies
581
+ // were temporarily unresolvable (e.g. after reassessment added new slices).
582
+ await deps.pauseAuto(ctx, pi);
583
+ ctx.ui.notify(blockedResumeMessage, "warning");
584
+ deps.sendDesktopNotification("GSD", blockedResumeMessage, "warning", "attention", basename(s.originalBasePath || s.basePath));
585
+ deps.logCmuxEvent(prefs, blockedResumeMessage, "warning");
586
+ } else {
587
+ const ids = incomplete.map((m: { id: string }) => m.id).join(", ");
588
+ const diag = `basePath=${s.basePath}, milestones=[${state.registry.map((m: { id: string; status: string }) => `${m.id}:${m.status}`).join(", ")}], phase=${state.phase}`;
589
+ ctx.ui.notify(
590
+ `Unexpected: ${incomplete.length} incomplete milestone(s) (${ids}) but no active milestone.\n Diagnostic: ${diag}`,
591
+ "error",
592
+ );
593
+ await deps.stopAuto(
594
+ ctx,
595
+ pi,
596
+ `No active milestone — ${incomplete.length} incomplete (${ids}), see diagnostic above`,
597
+ );
598
+ }
599
+ debugLog("autoLoop", { phase: "exit", reason: "no-active-milestone" });
600
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "no-active-milestone" } });
601
+ return { action: "break", reason: "no-active-milestone" };
602
+ }
603
+
604
+ if (!midTitle) {
605
+ midTitle = mid;
606
+ ctx.ui.notify(
607
+ `Milestone ${mid} has no title in roadmap — using ID as fallback.`,
608
+ "warning",
609
+ );
610
+ }
611
+
612
+ // Mid-merge safety check
613
+ const mergeReconcileResult = deps.reconcileMergeState(s.basePath, ctx);
614
+ if (mergeReconcileResult === "blocked") {
615
+ await deps.pauseAuto(ctx, pi);
616
+ debugLog("autoLoop", { phase: "exit", reason: "merge-reconciliation-blocked" });
617
+ return { action: "break", reason: "merge-reconciliation-blocked" };
618
+ }
619
+ if (mergeReconcileResult === "reconciled") {
620
+ deps.invalidateAllCaches();
621
+ state = await deps.deriveState(s.canonicalProjectRoot);
622
+ mid = state.activeMilestone?.id;
623
+ midTitle = state.activeMilestone?.title;
624
+ }
625
+
626
+ if (!mid || !midTitle) {
627
+ const noMilestoneReason = !mid
628
+ ? "No active milestone after merge reconciliation"
629
+ : `Milestone ${mid} has no title after reconciliation`;
630
+ await closeoutAndStop(ctx, pi, s, deps, noMilestoneReason);
631
+ debugLog("autoLoop", {
632
+ phase: "exit",
633
+ reason: "no-milestone-after-reconciliation",
634
+ });
635
+ return { action: "break", reason: "no-milestone-after-reconciliation" };
636
+ }
637
+
638
+ // Terminal: complete
639
+ if (state.phase === "complete") {
640
+ // Milestone merge on complete (before closeout so branch state is clean).
641
+ if (s.currentMilestoneId) {
642
+ // #2909 / #5538-followup: preflight stash + always-on postflight pop.
643
+ const stop = await _runMilestoneMergeOnceWithStashRestore(ic, s.currentMilestoneId);
644
+ if (stop) return stop;
645
+ // PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
646
+ }
647
+ deps.sendDesktopNotification(
648
+ "GSD",
649
+ `Milestone ${mid} complete!`,
650
+ "success",
651
+ "milestone",
652
+ basename(s.originalBasePath || s.basePath),
653
+ );
654
+ deps.logCmuxEvent(
655
+ prefs,
656
+ `Milestone ${mid} complete.`,
657
+ "success",
658
+ );
659
+ if (s.currentUnit) {
660
+ await deps.closeoutUnit(
661
+ ctx,
662
+ s.basePath,
663
+ s.currentUnit.type,
664
+ s.currentUnit.id,
665
+ s.currentUnit.startedAt,
666
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
667
+ );
668
+ s.clearCurrentUnit();
669
+ }
670
+ await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`, {
671
+ completionWidget: {
672
+ milestoneId: mid,
673
+ milestoneTitle: midTitle,
674
+ },
675
+ });
676
+ debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
677
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
678
+ return { action: "break", reason: "milestone-complete" };
679
+ }
680
+
681
+ // Terminal: blocked — pause instead of hard-stop so the session is resumable.
682
+ if (state.phase === "blocked") {
683
+ const blockedResumeMessage = formatBlockedResumeMessage(state.blockers);
684
+ if (s.currentUnit) {
685
+ await deps.closeoutUnit(
686
+ ctx,
687
+ s.basePath,
688
+ s.currentUnit.type,
689
+ s.currentUnit.id,
690
+ s.currentUnit.startedAt,
691
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
692
+ );
693
+ }
694
+ await deps.pauseAuto(ctx, pi);
695
+ ctx.ui.notify(blockedResumeMessage, "warning");
696
+ deps.sendDesktopNotification("GSD", blockedResumeMessage, "warning", "attention", basename(s.originalBasePath || s.basePath));
697
+ deps.logCmuxEvent(prefs, blockedResumeMessage, "warning");
698
+ debugLog("autoLoop", { phase: "exit", reason: "blocked" });
699
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "blocked", blockers: state.blockers } });
700
+ return { action: "break", reason: "blocked" };
701
+ }
702
+
703
+ return { action: "next", data: { state, mid, midTitle } };
704
+ }