@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
@@ -153,6 +153,38 @@ export function attachExternalResultsToToolBlocks(toolBlocks, toolResultsById) {
153
153
  block.externalResult = externalResult;
154
154
  }
155
155
  }
156
+ function textFromExternalResult(result) {
157
+ return (result?.content ?? [])
158
+ .map((block) => typeof block.text === "string" ? block.text : "")
159
+ .join("\n");
160
+ }
161
+ function isEmptyToolArguments(value) {
162
+ return !!value
163
+ && typeof value === "object"
164
+ && !Array.isArray(value)
165
+ && Object.keys(value).length === 0;
166
+ }
167
+ function sameToolSurface(a, b) {
168
+ return a.name === b.name
169
+ && a.mcpServer === b.mcpServer;
170
+ }
171
+ export function shouldSuppressDuplicateToolUnavailableBlock(block, allBlocks) {
172
+ if (block.type !== "toolCall")
173
+ return false;
174
+ const externalResult = block.externalResult;
175
+ if (!externalResult?.isError)
176
+ return false;
177
+ if (!isEmptyToolArguments(block.arguments))
178
+ return false;
179
+ if (!/No such tool available:/i.test(textFromExternalResult(externalResult)))
180
+ return false;
181
+ return allBlocks.some((candidate) => {
182
+ if (candidate.type !== "toolCall" || candidate.id === block.id)
183
+ return false;
184
+ const candidateResult = candidate.externalResult;
185
+ return candidateResult?.isError === false && sameToolSurface(block, candidate);
186
+ });
187
+ }
156
188
  /**
157
189
  * Build the final assistant content that Agent Core consumes in
158
190
  * `externalToolExecution` mode. This preserves tool-call blocks, attaches any
@@ -165,7 +197,7 @@ export function buildFinalAssistantContent(params) {
165
197
  mergePendingToolCalls(mergedToolBlocks, params.pendingContent);
166
198
  }
167
199
  attachExternalResultsToToolBlocks(mergedToolBlocks, params.toolResultsById);
168
- const finalContent = [...mergedToolBlocks];
200
+ const finalContent = mergedToolBlocks.filter((block) => !shouldSuppressDuplicateToolUnavailableBlock(block, mergedToolBlocks));
169
201
  if (params.pendingContent && params.pendingContent.length > 0) {
170
202
  for (const block of params.pendingContent) {
171
203
  if (block.type === "text" || block.type === "thinking") {
@@ -0,0 +1,215 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Auto-loop closeout, milestone report, and merge helpers.
3
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
4
+ import { MergeConflictError } from "../git-service.js";
5
+ import { findUnmergedCompletedMilestones } from "../unmerged-milestone-guard.js";
6
+ import { getIsolationMode } from "../preferences.js";
7
+ import { isDbAvailable, getMilestone } from "../gsd-db.js";
8
+ import { refreshWorkflowDatabaseFromDisk } from "../db-workspace.js";
9
+ import { isClosedStatus } from "../status-guards.js";
10
+ import { logWarning, logError } from "../workflow-logger.js";
11
+ import { debugLog } from "../debug-logger.js";
12
+ import { _resolveReportBasePath } from "./phase-helpers.js";
13
+ /**
14
+ * If a unit is in-flight, close it out, then stop auto-mode.
15
+ */
16
+ export async function closeoutAndStop(ctx, pi, s, deps, reason) {
17
+ if (s.currentUnit) {
18
+ await deps.closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
19
+ s.clearCurrentUnit();
20
+ }
21
+ await deps.stopAuto(ctx, pi, reason);
22
+ }
23
+ export async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
24
+ if (!result.needsManualRecovery)
25
+ return null;
26
+ const { ctx, pi, deps } = ic;
27
+ const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
28
+ ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
29
+ await deps.stopAuto(ctx, pi, reason);
30
+ return { action: "break", reason: "postflight-stash-restore-failed" };
31
+ }
32
+ export async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
33
+ if (!preflight.stashPushed)
34
+ return null;
35
+ const { s, deps } = ic;
36
+ const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ic.ctx.ui.notify.bind(ic.ctx.ui));
37
+ return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
38
+ }
39
+ /**
40
+ * Run a milestone merge surrounded by preflight stash + always-on postflight
41
+ * pop. The previous code popped the stash only after a successful merge, which
42
+ * leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
43
+ * leaving the user's pre-merge working tree silently stashed away after a
44
+ * merge-conflict or other merge error. This helper restores the stash on
45
+ * every exit path, then surfaces the merge or stash failure (in priority
46
+ * order) as the loop's stop reason.
47
+ *
48
+ * Returns a `break` action when auto-mode must stop, or `null` when the merge
49
+ * succeeded and the stash (if any) was restored cleanly.
50
+ */
51
+ export async function _runMilestoneMergeWithStashRestore(ic, milestoneId, options = {}) {
52
+ const { ctx, pi, s, deps } = ic;
53
+ const preflight = deps.preflightCleanRoot(s.originalBasePath || s.basePath, milestoneId, ctx.ui.notify.bind(ctx.ui));
54
+ if (preflight.blocked) {
55
+ const reason = preflight.blockedReason === "unmerged-conflicts"
56
+ ? `Pre-merge unresolved Git conflicts block milestone ${milestoneId}`
57
+ : `Pre-merge dirty working tree overlaps milestone ${milestoneId}`;
58
+ await deps.stopAuto(ctx, pi, reason, {
59
+ preserveCompletedMilestoneBranch: true,
60
+ preserveCloseoutTranscript: options.preserveCloseoutTranscript,
61
+ });
62
+ return {
63
+ action: "break",
64
+ reason: preflight.blockedReason === "unmerged-conflicts"
65
+ ? "preflight-unmerged-conflicts"
66
+ : "preflight-dirty-overlap",
67
+ };
68
+ }
69
+ let mergeError = null;
70
+ const exitResult = deps.lifecycle.exitMilestone(milestoneId, { merge: true }, ctx.ui);
71
+ if (exitResult.ok) {
72
+ s.milestoneMergedInPhases = true;
73
+ try {
74
+ const projectRoot = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
75
+ const { rebuildMarkdownProjectionsFromDb } = await import("../commands-maintenance.js");
76
+ await rebuildMarkdownProjectionsFromDb(projectRoot);
77
+ }
78
+ catch (err) {
79
+ logWarning("engine", `markdown projection rebuild after milestone merge failed: ${err instanceof Error ? err.message : String(err)}`);
80
+ }
81
+ }
82
+ else {
83
+ mergeError = exitResult.cause ?? new Error(`exit ${exitResult.reason}`);
84
+ }
85
+ // Always attempt to restore the stashed working tree, even on merge error.
86
+ // postflightPopStash itself does not throw; failures surface via the
87
+ // PostflightResult.needsManualRecovery flag.
88
+ let stashResult = null;
89
+ if (preflight.stashPushed) {
90
+ stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
91
+ }
92
+ // Merge failure takes priority over stash recovery — the merge is the
93
+ // authoritative gate. If the stash also needed manual recovery, the user
94
+ // already saw the postflightPopStash notify above.
95
+ if (mergeError) {
96
+ if (mergeError instanceof MergeConflictError) {
97
+ // A merge conflict is a recoverable human checkpoint, not an
98
+ // infrastructure failure — the user resolves the conflict and runs
99
+ // `/gsd auto` to resume. Pause (don't stop): stopAuto tears down the
100
+ // session and, because `milestoneMergedInPhases` stays false here,
101
+ // re-runs the already-failed worktree merge in its cleanup step
102
+ // (#2645), then drops the user out of the interactive TUI onto a
103
+ // "stopped" surface.
104
+ const conflictReason = `Merge conflict on milestone ${milestoneId}: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`;
105
+ ctx.ui.notify(conflictReason, "error");
106
+ await deps.pauseAuto(ctx, pi, {
107
+ message: conflictReason,
108
+ category: "unknown",
109
+ });
110
+ return { action: "break", reason: "merge-conflict" };
111
+ }
112
+ logError("engine", "Milestone merge failed with non-conflict error", {
113
+ milestone: milestoneId,
114
+ error: String(mergeError),
115
+ });
116
+ // Like a merge conflict, a non-conflict merge failure (index lock,
117
+ // network, permissions) is recoverable — the user fixes the cause and
118
+ // runs `/gsd auto` to resume. Pause (don't stop) so the session stays
119
+ // resumable and stopAuto's teardown does not re-run the failed merge.
120
+ const mergeFailReason = `Merge error on milestone ${milestoneId}: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`;
121
+ ctx.ui.notify(mergeFailReason, "error");
122
+ await deps.pauseAuto(ctx, pi, {
123
+ message: mergeFailReason,
124
+ category: "unknown",
125
+ });
126
+ return { action: "break", reason: "merge-failed" };
127
+ }
128
+ if (stashResult) {
129
+ return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
130
+ }
131
+ return null;
132
+ }
133
+ export async function _runMilestoneMergeOnceWithStashRestore(ic, milestoneId, options = {}) {
134
+ if (ic.s.milestoneMergedInPhases) {
135
+ debugLog("autoLoop", {
136
+ phase: "milestone-merge-skip",
137
+ reason: "already-merged-in-phases",
138
+ milestoneId,
139
+ });
140
+ return null;
141
+ }
142
+ return _runMilestoneMergeWithStashRestore(ic, milestoneId, options);
143
+ }
144
+ export async function shouldSkipTerminalMilestoneCloseout(s, state, mid) {
145
+ const closeoutMilestoneId = mid ?? s.currentMilestoneId ?? state.lastCompletedMilestone?.id;
146
+ if (s.completionStopInProgress) {
147
+ return { skip: true, milestoneId: closeoutMilestoneId };
148
+ }
149
+ if (!closeoutMilestoneId) {
150
+ return { skip: false };
151
+ }
152
+ if (isDbAvailable())
153
+ refreshWorkflowDatabaseFromDisk();
154
+ const closeoutBasePath = s.originalBasePath || s.canonicalProjectRoot || s.basePath;
155
+ let closeoutMergePending = false;
156
+ if (getIsolationMode(closeoutBasePath) !== "none") {
157
+ try {
158
+ const blockers = await findUnmergedCompletedMilestones(closeoutBasePath);
159
+ closeoutMergePending = blockers.some((blocker) => blocker.milestoneId === closeoutMilestoneId);
160
+ }
161
+ catch {
162
+ // Fail open: without git/DB inspection we cannot safely treat closeout as done.
163
+ closeoutMergePending = true;
164
+ }
165
+ }
166
+ const milestoneAlreadyClosedOut = isDbAvailable()
167
+ && isClosedStatus(getMilestone(closeoutMilestoneId)?.status ?? "")
168
+ && !closeoutMergePending;
169
+ if (milestoneAlreadyClosedOut) {
170
+ return { skip: true, milestoneId: closeoutMilestoneId };
171
+ }
172
+ return { skip: false, milestoneId: closeoutMilestoneId };
173
+ }
174
+ /**
175
+ * Generate and write an HTML milestone report snapshot.
176
+ */
177
+ export async function generateMilestoneReport(s, ctx, milestoneId) {
178
+ const { loadVisualizerData } = await importExtensionModule(import.meta.url, "../visualizer-data.js");
179
+ const { generateHtmlReport } = await importExtensionModule(import.meta.url, "../export-html.js");
180
+ const { writeReportSnapshot } = await importExtensionModule(import.meta.url, "../reports.js");
181
+ const { basename } = await import("node:path");
182
+ const reportBasePath = _resolveReportBasePath(s);
183
+ const snapData = await loadVisualizerData(reportBasePath);
184
+ const completedMs = snapData.milestones.find((m) => m.id === milestoneId);
185
+ const msTitle = completedMs?.title ?? milestoneId;
186
+ const gsdVersion = process.env.GSD_VERSION ?? "0.0.0";
187
+ const projName = basename(reportBasePath);
188
+ const doneSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.filter((sl) => sl.done).length, 0);
189
+ const totalSlices = snapData.milestones.reduce((acc, m) => acc + m.slices.length, 0);
190
+ const outPath = writeReportSnapshot({
191
+ basePath: reportBasePath,
192
+ html: generateHtmlReport(snapData, {
193
+ projectName: projName,
194
+ projectPath: reportBasePath,
195
+ gsdVersion,
196
+ milestoneId,
197
+ indexRelPath: "index.html",
198
+ }),
199
+ milestoneId,
200
+ milestoneTitle: msTitle,
201
+ kind: "milestone",
202
+ projectName: projName,
203
+ projectPath: reportBasePath,
204
+ gsdVersion,
205
+ totalCost: snapData.totals?.cost ?? 0,
206
+ totalTokens: snapData.totals?.tokens.total ?? 0,
207
+ totalDuration: snapData.totals?.duration ?? 0,
208
+ doneSlices,
209
+ totalSlices,
210
+ doneMilestones: snapData.milestones.filter((m) => m.status === "complete").length,
211
+ totalMilestones: snapData.milestones.length,
212
+ phase: snapData.phase,
213
+ });
214
+ ctx.ui.notify(`Report saved: .gsd/reports/${basename(outPath)} — open index.html to browse progression.`, "info");
215
+ }
@@ -26,7 +26,7 @@
26
26
  * grammar itself lives in auto/dispatch-key.ts and is re-exported here for
27
27
  * import stability.
28
28
  */
29
- import { buildDispatchKey, normalizeDispatchKey } from "./dispatch-key.js";
29
+ import { buildDispatchKey, normalizeDispatchKey, parseDispatchKey } from "./dispatch-key.js";
30
30
  import { detectStuck } from "./detect-stuck.js";
31
31
  import { getLatestForUnit, getRecentUnitKeysForProjectRoot, } from "../db/unit-dispatches.js";
32
32
  import { debugLog } from "../debug-logger.js";
@@ -37,12 +37,18 @@ export { buildDispatchKey, normalizeDispatchKey, parseDispatchKey } from "./disp
37
37
  * cutover (issue #5791).
38
38
  */
39
39
  export const STUCK_WINDOW_SIZE = 6;
40
- function lookupLatestLedgerError(unitType, unitId) {
40
+ /**
41
+ * Fetch the latest dispatch ledger error for a unit, the single canonical
42
+ * lookup shared by the DispatchHistory window and the legacy
43
+ * `loopState.recentUnits` path in dispatch.ts. The ledger keys rows by the
44
+ * bare unit id with the unit type in its own column, so the lookup must use
45
+ * the bare id and require a unit_type match (a compound `type/id` key would
46
+ * miss the row, and another unit type's error on the same id must never be
47
+ * attached — it would trip the repeat-error rule spuriously).
48
+ */
49
+ export function lookupLatestLedgerError(unitType, unitId) {
41
50
  try {
42
51
  const row = getLatestForUnit(unitId);
43
- // The ledger keys rows by bare unit id; require a unit_type match so
44
- // another unit type's error on the same id is never attached (it would
45
- // trip the repeat-error rule spuriously).
46
52
  if (!row || row.unit_type !== unitType)
47
53
  return undefined;
48
54
  return row.error_summary ?? undefined;
@@ -85,7 +91,16 @@ export function createDispatchHistory(options) {
85
91
  const persisted = getRecentUnitKeysForProjectRoot(scopeId, windowSize);
86
92
  if (persisted.length === 0)
87
93
  return 0;
88
- window = persisted.map(({ key }) => ({ key: normalizeDispatchKey(key) }));
94
+ const rebuilt = [];
95
+ for (const { key } of persisted) {
96
+ const normalized = normalizeDispatchKey(key);
97
+ const parsed = parseDispatchKey(normalized);
98
+ const error = parsed && rebuilt.some((entry) => entry.key === normalized)
99
+ ? lookupLatestLedgerError(parsed.unitType, parsed.unitId)
100
+ : undefined;
101
+ rebuilt.push({ key: normalized, error });
102
+ }
103
+ window = rebuilt;
89
104
  while (window.length > windowSize)
90
105
  window.shift();
91
106
  return window.length;
@@ -0,0 +1,365 @@
1
+ // Project/App: gsd-pi
2
+ // File Purpose: Auto-loop dispatch phase.
3
+ import { detectStuck } from "./detect-stuck.js";
4
+ import { STUCK_WINDOW_SIZE, lookupLatestLedgerError } from "./dispatch-history.js";
5
+ import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact, } from "../auto-recovery.js";
6
+ import { getConsecutiveDispatchBlocker } from "../dispatch-guard.js";
7
+ import { debugLog } from "../debug-logger.js";
8
+ import { getToolBaselineSnapshot, getRegisteredToolSnapshot, } from "../auto-model-selection.js";
9
+ import { supportsStructuredQuestions } from "../workflow-mcp.js";
10
+ import { isDbAvailable, getTask, getSlice } from "../gsd-db.js";
11
+ import { refreshWorkflowDatabaseFromDisk } from "../db-workspace.js";
12
+ import { isClosedStatus } from "../status-guards.js";
13
+ import { parseUnitId } from "../unit-id.js";
14
+ import { validateSourceWriteWorktreeSafety } from "./worktree-safety-phase.js";
15
+ import { closeoutAndStop } from "./closeout.js";
16
+ import { persistStuckRecoveryAttempts, _resolveDispatchGuardBasePath, } from "./phase-helpers.js";
17
+ export function getAlreadyClosedDispatchReason(unitType, unitId) {
18
+ if (!isDbAvailable())
19
+ return null;
20
+ refreshWorkflowDatabaseFromDisk();
21
+ const { milestone, slice, task } = parseUnitId(unitId);
22
+ if (unitType === "execute-task" && milestone && slice && task) {
23
+ const row = getTask(milestone, slice, task);
24
+ return row && isClosedStatus(row.status)
25
+ ? `execute-task ${unitId} is already ${row.status}`
26
+ : null;
27
+ }
28
+ if (unitType === "complete-slice" && milestone && slice) {
29
+ const row = getSlice(milestone, slice);
30
+ return row && isClosedStatus(row.status)
31
+ ? `complete-slice ${unitId} is already ${row.status}`
32
+ : null;
33
+ }
34
+ return null;
35
+ }
36
+ function isUnhandledPhaseWarning(dispatchResult) {
37
+ return dispatchResult.action === "stop" &&
38
+ dispatchResult.level === "warning" &&
39
+ dispatchResult.matchedRule === "<no-match>" &&
40
+ /^Unhandled phase "/.test(dispatchResult.reason);
41
+ }
42
+ export { isUnhandledPhaseWarning };
43
+ /**
44
+ * Phase 3: Dispatch resolution — resolve next unit, stuck detection, pre-dispatch hooks.
45
+ * Returns break/continue to control the loop, or next with IterationData on success.
46
+ */
47
+ export async function runDispatch(ic, preData, loopState) {
48
+ const { ctx, pi, s, deps, prefs } = ic;
49
+ const { state, mid, midTitle } = preData;
50
+ const provider = ctx.model?.provider;
51
+ const authMode = provider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
52
+ ? ctx.modelRegistry.getProviderAuthMode(provider)
53
+ : undefined;
54
+ // Use the baseline snapshot rather than the live active-tool set: a prior
55
+ // unit's per-provider narrowing (hook overrides, Groq 128-tool cap, etc.)
56
+ // can strip required MCP tools from the live set even though
57
+ // selectAndApplyModel will restore them before the unit is dispatched.
58
+ // Checking a stale-narrowed set causes false transport-preflight warnings
59
+ // that repeat on every /gsd auto resume (#477 follow-up).
60
+ const activeTools = getToolBaselineSnapshot(pi);
61
+ const registeredTools = getRegisteredToolSnapshot(pi);
62
+ // Deep planning intentionally keeps human checkpoints in plain chat. In
63
+ // Claude Code/local MCP transports, structured question requests can be
64
+ // cancelled outside the normal chat flow, which made approval gates easy to
65
+ // skip or bury under tool output.
66
+ const structuredQuestionsAvailable = prefs?.planning_depth === "deep"
67
+ ? "false"
68
+ : supportsStructuredQuestions(activeTools, {
69
+ authMode,
70
+ baseUrl: ctx.model?.baseUrl,
71
+ }) ? "true" : "false";
72
+ debugLog("autoLoop", { phase: "dispatch-resolve", iteration: ic.iteration });
73
+ let dispatchResult = await deps.resolveDispatch({
74
+ basePath: s.basePath,
75
+ mid,
76
+ midTitle,
77
+ state,
78
+ prefs,
79
+ session: s,
80
+ structuredQuestionsAvailable,
81
+ sessionContextWindow: ctx.model?.contextWindow,
82
+ sessionProvider: ctx.model?.provider,
83
+ modelRegistry: ctx.modelRegistry,
84
+ activeTools,
85
+ registeredTools,
86
+ sessionBaseUrl: ctx.model?.baseUrl,
87
+ sessionAuthMode: authMode,
88
+ });
89
+ if (isUnhandledPhaseWarning(dispatchResult)) {
90
+ deps.invalidateAllCaches();
91
+ const freshState = await deps.deriveState(s.canonicalProjectRoot);
92
+ const freshMid = freshState.activeMilestone?.id ?? mid;
93
+ const freshMidTitle = freshState.activeMilestone?.title ?? freshMid ?? midTitle;
94
+ debugLog("autoLoop", {
95
+ phase: "dispatch-unhandled-phase-retry",
96
+ iteration: ic.iteration,
97
+ stalePhase: state.phase,
98
+ freshPhase: freshState.phase,
99
+ });
100
+ dispatchResult = await deps.resolveDispatch({
101
+ basePath: s.basePath,
102
+ mid: freshMid,
103
+ midTitle: freshMidTitle,
104
+ state: freshState,
105
+ prefs,
106
+ session: s,
107
+ structuredQuestionsAvailable,
108
+ sessionContextWindow: ctx.model?.contextWindow,
109
+ sessionProvider: ctx.model?.provider,
110
+ modelRegistry: ctx.modelRegistry,
111
+ activeTools,
112
+ registeredTools,
113
+ sessionBaseUrl: ctx.model?.baseUrl,
114
+ sessionAuthMode: authMode,
115
+ });
116
+ }
117
+ if (dispatchResult.action === "stop") {
118
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "dispatch-stop", rule: dispatchResult.matchedRule, data: { reason: dispatchResult.reason } });
119
+ // Warning-level stops are recoverable human checkpoints (e.g. UAT verdict
120
+ // gate) — pause instead of hard-stopping so the session is resumable with
121
+ // `/gsd auto`. Error/info-level stops remain hard stops for infrastructure
122
+ // failures and terminal conditions respectively.
123
+ // See: https://github.com/open-gsd/gsd-pi/issues/2474
124
+ if (dispatchResult.level === "warning") {
125
+ ctx.ui.notify(dispatchResult.reason, "warning");
126
+ await deps.pauseAuto(ctx, pi, {
127
+ message: dispatchResult.reason,
128
+ category: "unknown",
129
+ });
130
+ }
131
+ else {
132
+ await closeoutAndStop(ctx, pi, s, deps, dispatchResult.reason);
133
+ }
134
+ debugLog("autoLoop", { phase: "exit", reason: "dispatch-stop" });
135
+ return { action: "break", reason: "dispatch-stop" };
136
+ }
137
+ if (dispatchResult.action !== "dispatch") {
138
+ // Non-dispatch action (e.g. "skip") — re-derive state
139
+ await new Promise((r) => setImmediate(r));
140
+ return { action: "continue" };
141
+ }
142
+ let unitType = dispatchResult.unitType;
143
+ let unitId = dispatchResult.unitId;
144
+ let prompt = dispatchResult.prompt;
145
+ let pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
146
+ let dispatchState = state;
147
+ let dispatchMid = mid;
148
+ let dispatchMidTitle = midTitle;
149
+ const pendingRetryDispatch = s.pendingVerificationRetryDispatch;
150
+ if (pendingRetryDispatch) {
151
+ unitType = pendingRetryDispatch.unitType;
152
+ unitId = pendingRetryDispatch.unitId;
153
+ prompt = pendingRetryDispatch.prompt;
154
+ pauseAfterUatDispatch = pendingRetryDispatch.pauseAfterUatDispatch;
155
+ dispatchState = pendingRetryDispatch.state;
156
+ dispatchMid = pendingRetryDispatch.mid ?? mid;
157
+ dispatchMidTitle = pendingRetryDispatch.midTitle ?? midTitle;
158
+ s.pendingVerificationRetryDispatch = null;
159
+ debugLog("autoLoop", {
160
+ phase: "dispatch-pending-verification-retry",
161
+ unitType,
162
+ unitId,
163
+ });
164
+ }
165
+ const alreadyClosedReason = getAlreadyClosedDispatchReason(unitType, unitId);
166
+ if (alreadyClosedReason) {
167
+ s.pendingVerificationRetry = null;
168
+ loopState.recentUnits = [];
169
+ loopState.stuckRecoveryAttempts = Math.max(loopState.stuckRecoveryAttempts, 1);
170
+ deps.invalidateAllCaches();
171
+ debugLog("autoLoop", {
172
+ phase: "dispatch-skip-already-closed",
173
+ unitType,
174
+ unitId,
175
+ reason: alreadyClosedReason,
176
+ });
177
+ deps.emitJournalEvent({
178
+ ts: new Date().toISOString(),
179
+ flowId: ic.flowId,
180
+ seq: ic.nextSeq(),
181
+ eventType: "guard-block",
182
+ data: { unitType, unitId, reason: alreadyClosedReason },
183
+ });
184
+ ctx.ui.notify(`Skipping ${unitType} ${unitId}: ${alreadyClosedReason}.`, "info");
185
+ await new Promise((r) => setImmediate(r));
186
+ return { action: "continue" };
187
+ }
188
+ deps.emitJournalEvent({
189
+ ts: new Date().toISOString(),
190
+ flowId: ic.flowId,
191
+ seq: ic.nextSeq(),
192
+ eventType: "dispatch-match",
193
+ rule: pendingRetryDispatch ? "verification-retry" : dispatchResult.matchedRule,
194
+ data: { unitType, unitId },
195
+ });
196
+ // Resolve hooks and prior-slice gating before health/stuck accounting so
197
+ // those checks run against the final dispatch unit.
198
+ const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
199
+ if (preDispatchResult.firedHooks.length > 0) {
200
+ ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
201
+ deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
202
+ }
203
+ if (preDispatchResult.action === "skip") {
204
+ ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
205
+ await new Promise((r) => setImmediate(r));
206
+ return { action: "continue" };
207
+ }
208
+ if (preDispatchResult.action === "replace") {
209
+ prompt = preDispatchResult.prompt ?? prompt;
210
+ if (preDispatchResult.unitType)
211
+ unitType = preDispatchResult.unitType;
212
+ }
213
+ else if (preDispatchResult.prompt) {
214
+ prompt = preDispatchResult.prompt;
215
+ }
216
+ const guardBasePath = _resolveDispatchGuardBasePath(s);
217
+ let mainBranch = "main";
218
+ try {
219
+ mainBranch = deps.getMainBranch(guardBasePath);
220
+ }
221
+ catch (err) {
222
+ debugLog("autoLoop", { phase: "getMainBranch-failed", error: String(err) });
223
+ }
224
+ const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, mainBranch, unitType, unitId);
225
+ if (priorSliceBlocker) {
226
+ await deps.stopAuto(ctx, pi, priorSliceBlocker);
227
+ debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
228
+ return { action: "break", reason: "prior-slice-blocker" };
229
+ }
230
+ const consecutiveDispatchBlocker = getConsecutiveDispatchBlocker(loopState, state.phase, unitType, unitId);
231
+ if (consecutiveDispatchBlocker) {
232
+ await deps.stopAuto(ctx, pi, consecutiveDispatchBlocker);
233
+ debugLog("autoLoop", { phase: "exit", reason: "consecutive-dispatch-blocker" });
234
+ return { action: "break", reason: "consecutive-dispatch-blocker" };
235
+ }
236
+ const worktreeSafetyBlock = await validateSourceWriteWorktreeSafety(ic, unitType, unitId, mid, "pre-dispatch");
237
+ if (worktreeSafetyBlock)
238
+ return worktreeSafetyBlock;
239
+ // ── Sliding-window stuck detection with graduated recovery ──
240
+ const derivedKey = `${unitType}/${unitId}`;
241
+ // Always record this dispatch in the sliding window and run detection so
242
+ // Rules 1/3/4 can catch retry loops with repeated failure content (#5719).
243
+ // Rules 2/2b suppress legitimate retry backoff through the dispatch ledger.
244
+ //
245
+ // Mirror DispatchHistory.recordDispatch: attach the latest ledger error
246
+ // only on a repeat (the key already exists in the window) so a first
247
+ // dispatch never trips the repeat-error rule, and first-dispatch advances
248
+ // (the common path) pay zero DB cost. The ledger keys rows by the bare unit
249
+ // id with the unit type in its own column, so look up by (unitType, unitId)
250
+ // — the compound `derivedKey` would miss the row and silently drop
251
+ // repeat-error detection here. derivedKey stays the window-entry key.
252
+ const recentError = loopState.recentUnits.some((entry) => entry.key === derivedKey)
253
+ ? lookupLatestLedgerError(unitType, unitId)
254
+ : undefined;
255
+ loopState.recentUnits.push({ key: derivedKey, error: recentError });
256
+ while (loopState.recentUnits.length > STUCK_WINDOW_SIZE) {
257
+ loopState.recentUnits.shift();
258
+ }
259
+ const stuckSignal = detectStuck(loopState.recentUnits, {
260
+ pendingRetry: !!s.pendingVerificationRetry,
261
+ retryAttempt: s.pendingVerificationRetry?.attempt,
262
+ });
263
+ if (stuckSignal) {
264
+ debugLog("autoLoop", {
265
+ phase: "stuck-check",
266
+ unitType,
267
+ unitId,
268
+ reason: stuckSignal.reason,
269
+ recoveryAttempts: loopState.stuckRecoveryAttempts,
270
+ });
271
+ if (loopState.stuckRecoveryAttempts === 0) {
272
+ // Level 1: try verifying the artifact, then cache invalidation + retry
273
+ loopState.stuckRecoveryAttempts++;
274
+ persistStuckRecoveryAttempts(s, loopState);
275
+ const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
276
+ if (artifactExists) {
277
+ debugLog("autoLoop", {
278
+ phase: "stuck-recovery",
279
+ level: 1,
280
+ action: "artifact-found",
281
+ });
282
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId, s.basePath);
283
+ if (!recoveryDb.ok) {
284
+ ctx.ui.notify(recoveryDb.fatal
285
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
286
+ : `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
287
+ if (recoveryDb.fatal) {
288
+ await deps.pauseAuto(ctx, pi);
289
+ return { action: "break", reason: recoveryDb.reason };
290
+ }
291
+ return { action: "continue" };
292
+ }
293
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
294
+ deps.invalidateAllCaches();
295
+ loopState.recentUnits.length = 0;
296
+ return { action: "continue" };
297
+ }
298
+ ctx.ui.notify(`Stuck on ${unitType} ${unitId} (${stuckSignal.reason}). Invalidating caches and retrying.`, "warning");
299
+ deps.invalidateAllCaches();
300
+ }
301
+ else {
302
+ // Level 2: hard stop — genuinely stuck
303
+ deps.invalidateAllCaches();
304
+ const artifactExists = verifyExpectedArtifact(unitType, unitId, s.basePath);
305
+ if (artifactExists) {
306
+ debugLog("autoLoop", {
307
+ phase: "stuck-recovery",
308
+ level: 2,
309
+ action: "artifact-found",
310
+ });
311
+ const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId, s.basePath);
312
+ if (recoveryDb.ok) {
313
+ ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
314
+ loopState.recentUnits.length = 0;
315
+ return { action: "continue" };
316
+ }
317
+ ctx.ui.notify(recoveryDb.fatal
318
+ ? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
319
+ : `${recoveryDb.message} Stopping for manual recovery.`, "warning");
320
+ if (recoveryDb.fatal) {
321
+ await deps.pauseAuto(ctx, pi);
322
+ return { action: "break", reason: recoveryDb.reason };
323
+ }
324
+ }
325
+ debugLog("autoLoop", {
326
+ phase: "stuck-detected",
327
+ unitType,
328
+ unitId,
329
+ reason: stuckSignal.reason,
330
+ });
331
+ const stuckDiag = diagnoseExpectedArtifact(unitType, unitId, s.basePath);
332
+ const stuckRemediation = buildLoopRemediationSteps(unitType, unitId, s.basePath);
333
+ const stuckParts = [`Stuck on ${unitType} ${unitId} — ${stuckSignal.reason}.`];
334
+ if (stuckDiag)
335
+ stuckParts.push(`Expected: ${stuckDiag}`);
336
+ if (stuckRemediation)
337
+ stuckParts.push(`To recover:\n${stuckRemediation}`);
338
+ ctx.ui.notify(stuckParts.join(" "), "error");
339
+ await deps.stopAuto(ctx, pi, `Stuck: ${stuckSignal.reason}`);
340
+ return { action: "break", reason: "stuck-detected" };
341
+ }
342
+ }
343
+ else {
344
+ // Progress detected — reset recovery counter
345
+ if (loopState.stuckRecoveryAttempts > 0) {
346
+ debugLog("autoLoop", {
347
+ phase: "stuck-counter-reset",
348
+ from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
349
+ to: derivedKey,
350
+ });
351
+ loopState.stuckRecoveryAttempts = 0;
352
+ persistStuckRecoveryAttempts(s, loopState);
353
+ }
354
+ }
355
+ return {
356
+ action: "next",
357
+ data: {
358
+ unitType, unitId, prompt, finalPrompt: prompt,
359
+ pauseAfterUatDispatch,
360
+ state: dispatchState, mid: dispatchMid, midTitle: dispatchMidTitle,
361
+ isRetry: Boolean(pendingRetryDispatch), previousTier: undefined,
362
+ hookModelOverride: preDispatchResult.model,
363
+ },
364
+ };
365
+ }