gsd-pi 2.81.0 → 2.82.0

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 (290) hide show
  1. package/README.md +36 -24
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +111 -8
  4. package/dist/resources/extensions/gsd/auto/phases.js +190 -97
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +66 -3
  6. package/dist/resources/extensions/gsd/auto/session.js +9 -0
  7. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +43 -0
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +182 -178
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +14 -11
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  11. package/dist/resources/extensions/gsd/auto-recovery.js +6 -181
  12. package/dist/resources/extensions/gsd/auto-runtime-state.js +5 -0
  13. package/dist/resources/extensions/gsd/auto-start.js +20 -23
  14. package/dist/resources/extensions/gsd/auto-unit-closeout.js +33 -5
  15. package/dist/resources/extensions/gsd/auto-verification.js +12 -6
  16. package/dist/resources/extensions/gsd/auto-worktree.js +8 -0
  17. package/dist/resources/extensions/gsd/auto.js +265 -76
  18. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +13 -6
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -2
  20. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +4 -8
  21. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +4 -10
  22. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +9 -0
  23. package/dist/resources/extensions/gsd/git-service.js +2 -1
  24. package/dist/resources/extensions/gsd/gsd-db.js +7 -23
  25. package/dist/resources/extensions/gsd/health-widget-core.js +1 -1
  26. package/dist/resources/extensions/gsd/health-widget.js +4 -10
  27. package/dist/resources/extensions/gsd/markdown-renderer.js +0 -95
  28. package/dist/resources/extensions/gsd/native-git-bridge.js +14 -14
  29. package/dist/resources/extensions/gsd/notification-overlay.js +35 -40
  30. package/dist/resources/extensions/gsd/parallel-merge.js +53 -30
  31. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +25 -33
  32. package/dist/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  33. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  34. package/dist/resources/extensions/gsd/prompts/discuss.md +20 -2
  35. package/dist/resources/extensions/gsd/recovery-classification.js +15 -1
  36. package/dist/resources/extensions/gsd/session-lock.js +40 -0
  37. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +131 -0
  38. package/dist/resources/extensions/gsd/state-reconciliation/drift/merge-state.js +247 -0
  39. package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +50 -0
  40. package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +87 -0
  41. package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +50 -0
  42. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +124 -0
  43. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-worker.js +32 -0
  44. package/dist/resources/extensions/gsd/state-reconciliation/errors.js +41 -0
  45. package/dist/resources/extensions/gsd/state-reconciliation/index.js +99 -0
  46. package/dist/resources/extensions/gsd/state-reconciliation/registry.js +24 -0
  47. package/dist/resources/extensions/gsd/state-reconciliation/spawn-gate.js +43 -0
  48. package/dist/resources/extensions/gsd/state-reconciliation/types.js +3 -0
  49. package/dist/resources/extensions/gsd/state-reconciliation.js +5 -26
  50. package/dist/resources/extensions/gsd/tui/render-kit.js +74 -0
  51. package/dist/resources/extensions/gsd/watch/header-renderer.js +92 -69
  52. package/dist/resources/extensions/gsd/watch/splash-palette.js +10 -0
  53. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  54. package/dist/resources/extensions/gsd/worktree-lifecycle.js +722 -316
  55. package/dist/resources/extensions/gsd/worktree-telemetry.js +3 -1
  56. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  57. package/dist/web/standalone/.next/BUILD_ID +1 -1
  58. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  59. package/dist/web/standalone/.next/build-manifest.json +2 -2
  60. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  61. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  69. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/index.html +1 -1
  77. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  84. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  85. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  86. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  87. package/dist/welcome-screen.d.ts +0 -7
  88. package/dist/welcome-screen.js +60 -69
  89. package/package.json +1 -1
  90. package/packages/daemon/package.json +2 -2
  91. package/packages/mcp-server/package.json +2 -2
  92. package/packages/native/package.json +1 -1
  93. package/packages/pi-agent-core/package.json +1 -1
  94. package/packages/pi-ai/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts +2 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js +47 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/assistant-message-design.test.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +76 -9
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts +2 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js +40 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/user-message-design.test.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts +0 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.d.ts.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js +30 -29
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js +10 -3
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/adaptive-layout.test.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +13 -13
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts +1 -3
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js +58 -3
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/bash-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +2 -2
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +12 -6
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -41
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +0 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +86 -82
  128. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts +35 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js +152 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/transcript-design.js.map +1 -0
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts +16 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.d.ts.map +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js +73 -0
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/tui-style-kit.js.map +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +12 -8
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +105 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +27 -26
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js +9 -6
  152. package/packages/pi-coding-agent/dist/modes/interactive/tui-mode.test.js.map +1 -1
  153. package/packages/pi-coding-agent/package.json +1 -1
  154. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/assistant-message-design.test.ts +56 -0
  155. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +113 -9
  156. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/user-message-design.test.ts +48 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.test.ts +10 -3
  158. package/packages/pi-coding-agent/src/modes/interactive/components/adaptive-layout.ts +43 -42
  159. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +14 -14
  160. package/packages/pi-coding-agent/src/modes/interactive/components/bash-execution.ts +64 -3
  161. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +13 -7
  162. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +15 -42
  163. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +84 -104
  164. package/packages/pi-coding-agent/src/modes/interactive/components/transcript-design.ts +196 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/tui-style-kit.ts +94 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +14 -9
  167. package/packages/pi-coding-agent/src/modes/interactive/theme/theme-highlight.test.ts +23 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +106 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +27 -26
  170. package/packages/pi-coding-agent/src/modes/interactive/tui-mode.test.ts +9 -6
  171. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  172. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +14 -1
  173. package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
  174. package/packages/pi-tui/dist/overlay-layout.d.ts.map +1 -1
  175. package/packages/pi-tui/dist/overlay-layout.js +9 -6
  176. package/packages/pi-tui/dist/overlay-layout.js.map +1 -1
  177. package/packages/pi-tui/package.json +1 -1
  178. package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +20 -1
  179. package/packages/pi-tui/src/overlay-layout.ts +10 -7
  180. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  181. package/packages/rpc-client/package.json +1 -1
  182. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts +2 -0
  183. package/pkg/dist/modes/interactive/theme/theme-highlight.test.d.ts.map +1 -0
  184. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js +17 -0
  185. package/pkg/dist/modes/interactive/theme/theme-highlight.test.js.map +1 -0
  186. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  187. package/pkg/dist/modes/interactive/theme/theme.js +105 -1
  188. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  189. package/pkg/dist/modes/interactive/theme/themes.d.ts.map +1 -1
  190. package/pkg/dist/modes/interactive/theme/themes.js +27 -26
  191. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  192. package/pkg/package.json +1 -1
  193. package/src/resources/extensions/gsd/auto/loop-deps.ts +9 -5
  194. package/src/resources/extensions/gsd/auto/loop.ts +113 -9
  195. package/src/resources/extensions/gsd/auto/phases.ts +144 -19
  196. package/src/resources/extensions/gsd/auto/run-unit.ts +69 -4
  197. package/src/resources/extensions/gsd/auto/session.ts +10 -0
  198. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +82 -0
  199. package/src/resources/extensions/gsd/auto-dashboard.ts +230 -183
  200. package/src/resources/extensions/gsd/auto-dispatch.ts +15 -1
  201. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -1
  202. package/src/resources/extensions/gsd/auto-recovery.ts +7 -209
  203. package/src/resources/extensions/gsd/auto-runtime-state.ts +5 -0
  204. package/src/resources/extensions/gsd/auto-start.ts +22 -22
  205. package/src/resources/extensions/gsd/auto-unit-closeout.ts +51 -0
  206. package/src/resources/extensions/gsd/auto-verification.ts +12 -6
  207. package/src/resources/extensions/gsd/auto-worktree.ts +8 -0
  208. package/src/resources/extensions/gsd/auto.ts +295 -75
  209. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -6
  210. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +6 -2
  211. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +5 -8
  212. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +4 -10
  213. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +12 -0
  214. package/src/resources/extensions/gsd/git-service.ts +2 -0
  215. package/src/resources/extensions/gsd/gsd-db.ts +7 -23
  216. package/src/resources/extensions/gsd/health-widget-core.ts +1 -1
  217. package/src/resources/extensions/gsd/health-widget.ts +6 -10
  218. package/src/resources/extensions/gsd/journal.ts +2 -0
  219. package/src/resources/extensions/gsd/markdown-renderer.ts +4 -95
  220. package/src/resources/extensions/gsd/native-git-bridge.ts +14 -13
  221. package/src/resources/extensions/gsd/notification-overlay.ts +50 -46
  222. package/src/resources/extensions/gsd/parallel-merge.ts +61 -34
  223. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +33 -35
  224. package/src/resources/extensions/gsd/prompts/complete-slice.md +14 -12
  225. package/src/resources/extensions/gsd/prompts/discuss-headless.md +20 -2
  226. package/src/resources/extensions/gsd/prompts/discuss.md +20 -2
  227. package/src/resources/extensions/gsd/recovery-classification.ts +18 -1
  228. package/src/resources/extensions/gsd/session-lock.ts +41 -0
  229. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +172 -0
  230. package/src/resources/extensions/gsd/state-reconciliation/drift/merge-state.ts +337 -0
  231. package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +69 -0
  232. package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +109 -0
  233. package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +68 -0
  234. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +185 -0
  235. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-worker.ts +46 -0
  236. package/src/resources/extensions/gsd/state-reconciliation/errors.ts +67 -0
  237. package/src/resources/extensions/gsd/state-reconciliation/index.ts +142 -0
  238. package/src/resources/extensions/gsd/state-reconciliation/registry.ts +27 -0
  239. package/src/resources/extensions/gsd/state-reconciliation/spawn-gate.ts +60 -0
  240. package/src/resources/extensions/gsd/state-reconciliation/types.ts +83 -0
  241. package/src/resources/extensions/gsd/state-reconciliation.ts +21 -53
  242. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +1 -1
  243. package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +654 -176
  245. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +291 -4
  246. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +16 -1
  247. package/src/resources/extensions/gsd/tests/auto-start-orphan-bootstrap.test.ts +18 -0
  248. package/src/resources/extensions/gsd/tests/auto-unit-closeout.test.ts +68 -0
  249. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +28 -1
  250. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +20 -2
  251. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +44 -0
  252. package/src/resources/extensions/gsd/tests/header-renderer.test.ts +40 -0
  253. package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +10 -0
  254. package/src/resources/extensions/gsd/tests/health-widget.test.ts +14 -4
  255. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +26 -0
  256. package/src/resources/extensions/gsd/tests/integration/integration-proof.test.ts +1 -1
  257. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +116 -24
  258. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +0 -1
  259. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +1 -1
  260. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +46 -11
  261. package/src/resources/extensions/gsd/tests/notification-overlay.test.ts +78 -41
  262. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +44 -0
  263. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +12 -217
  264. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +38 -6
  265. package/src/resources/extensions/gsd/tests/post-exec-retry-bypass.test.ts +2 -2
  266. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +1 -1
  267. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +24 -1
  268. package/src/resources/extensions/gsd/tests/resume-dispatch-worktree.test.ts +7 -3
  269. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +6 -3
  270. package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +24 -0
  271. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +65 -58
  272. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +952 -0
  273. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +4 -0
  274. package/src/resources/extensions/gsd/tests/tui-header-lifecycle.test.ts +121 -1
  275. package/src/resources/extensions/gsd/tests/tui-render-kit.test.ts +66 -0
  276. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +83 -0
  277. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +6 -0
  278. package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +158 -58
  279. package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +572 -118
  280. package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +59 -2
  281. package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +18 -0
  282. package/src/resources/extensions/gsd/tui/render-kit.ts +109 -0
  283. package/src/resources/extensions/gsd/watch/header-renderer.ts +121 -79
  284. package/src/resources/extensions/gsd/watch/splash-palette.ts +11 -0
  285. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  286. package/src/resources/extensions/gsd/worktree-lifecycle.ts +1151 -524
  287. package/src/resources/extensions/gsd/worktree-telemetry.ts +7 -2
  288. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +0 -1544
  289. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_buildManifest.js +0 -0
  290. /package/dist/web/standalone/.next/static/{drLMkgfHQ8lzS229_HWYR → S44UQTFCUdA44dkjfYt6S}/_ssgManifest.js +0 -0
@@ -399,6 +399,12 @@ export async function autoLoop(
399
399
 
400
400
  let dispatchId: number | null = null;
401
401
  let dispatchSettled = false;
402
+ let iterationEndEmitted = false;
403
+ const emitIterationEnd = (details: Record<string, unknown> = {}): void => {
404
+ if (iterationEndEmitted) return;
405
+ iterationEndEmitted = true;
406
+ journalReporter.emit("iteration-end", { iteration, ...details });
407
+ };
402
408
  const completeIteration = (): void => {
403
409
  completeWorkflowIteration({
404
410
  get consecutiveErrors() { return consecutiveErrors; },
@@ -407,11 +413,15 @@ export async function autoLoop(
407
413
  set consecutiveCooldowns(value) { consecutiveCooldowns = value; },
408
414
  recentErrorMessages,
409
415
  }, {
410
- emitIterationEnd: () => journalReporter.emit("iteration-end", { iteration }),
416
+ emitIterationEnd: () => emitIterationEnd(),
411
417
  saveStuckState: () => saveStuckState(s, loopState),
412
418
  logIterationComplete: () => debugLog("autoLoop", { phase: "iteration-complete", iteration }),
413
419
  });
414
420
  };
421
+ const finishIncompleteIteration = (details: Record<string, unknown>): void => {
422
+ emitIterationEnd(details);
423
+ saveStuckState(s, loopState);
424
+ };
415
425
 
416
426
  try {
417
427
  // ── Blanket try/catch: one bad iteration must not kill the session
@@ -492,6 +502,7 @@ export async function autoLoop(
492
502
  });
493
503
  if (engineState.isComplete) {
494
504
  finishTurn("completed");
505
+ emitIterationEnd({ status: "completed", reason: "custom-engine-complete" });
495
506
  await deps.stopAuto(ctx, pi, "Workflow complete");
496
507
  break;
497
508
  }
@@ -509,16 +520,23 @@ export async function autoLoop(
509
520
  });
510
521
  if (dispatchFlow.action === "break") {
511
522
  finishTurn("stopped", "manual-attention", "custom-engine-dispatch-stop");
523
+ finishIncompleteIteration({
524
+ status: "stopped",
525
+ reason: "custom-engine-dispatch-stop",
526
+ failureClass: "manual-attention",
527
+ });
512
528
  break;
513
529
  }
514
530
  if (dispatchFlow.action === "continue") {
515
531
  finishTurn("skipped");
532
+ emitIterationEnd({ status: "skipped", reason: "custom-engine-dispatch-skip" });
516
533
  continue;
517
534
  }
518
535
 
519
536
  // dispatch.action === "dispatch"
520
537
  if (dispatch.action !== "dispatch") {
521
538
  finishTurn("skipped");
539
+ emitIterationEnd({ status: "skipped", reason: "custom-engine-dispatch-mismatch" });
522
540
  continue;
523
541
  }
524
542
  const step = dispatch.step;
@@ -547,6 +565,13 @@ export async function autoLoop(
547
565
  });
548
566
  if (guardsResult.action === "break") {
549
567
  finishTurn("stopped", "manual-attention", "guard-break");
568
+ finishIncompleteIteration({
569
+ status: "stopped",
570
+ reason: "guard-break",
571
+ unitType: iterData.unitType,
572
+ unitId: iterData.unitId,
573
+ failureClass: "manual-attention",
574
+ });
550
575
  break;
551
576
  }
552
577
 
@@ -578,6 +603,13 @@ export async function autoLoop(
578
603
  unitId: iterData.unitId,
579
604
  });
580
605
  if (unitPhaseResult.action === "break") {
606
+ finishIncompleteIteration({
607
+ status: "stopped",
608
+ reason: unitPhaseResult.reason ?? "unit-break",
609
+ unitType: iterData.unitType,
610
+ unitId: iterData.unitId,
611
+ failureClass: "execution",
612
+ });
581
613
  finishTurn("stopped", "execution", "unit-break");
582
614
  break;
583
615
  }
@@ -596,7 +628,16 @@ export async function autoLoop(
596
628
  finishTurn,
597
629
  },
598
630
  });
599
- if (verifyFlow.action === "break") break;
631
+ if (verifyFlow.action === "break") {
632
+ finishIncompleteIteration({
633
+ status: "paused",
634
+ reason: "custom-engine-verify-pause",
635
+ unitType: iterData.unitType,
636
+ unitId: iterData.unitId,
637
+ failureClass: "manual-attention",
638
+ });
639
+ break;
640
+ }
600
641
  }
601
642
  if (verifyResult === "retry") {
602
643
  const retryOutcome = await handleCustomEngineVerifyRetry({
@@ -630,7 +671,22 @@ export async function autoLoop(
630
671
  finishTurn,
631
672
  },
632
673
  });
633
- if (retryFlow.action === "break") break;
674
+ if (retryFlow.action === "break") {
675
+ finishIncompleteIteration({
676
+ status: retryOutcome.action === "stop" ? "stopped" : "paused",
677
+ reason: retryOutcome.action === "retry" ? "custom-engine-verify-retry" : retryOutcome.turnError,
678
+ unitType: iterData.unitType,
679
+ unitId: iterData.unitId,
680
+ failureClass: "manual-attention",
681
+ });
682
+ break;
683
+ }
684
+ finishIncompleteIteration({
685
+ status: "retry",
686
+ reason: "custom-engine-verify-retry",
687
+ unitType: iterData.unitType,
688
+ unitId: iterData.unitId,
689
+ });
634
690
  continue;
635
691
  }
636
692
 
@@ -841,6 +897,13 @@ export async function autoLoop(
841
897
  markFailed: markDispatchFailed,
842
898
  logWriteFailure: logDispatchLedgerWriteFailure,
843
899
  }) || dispatchSettled;
900
+ finishIncompleteIteration({
901
+ status: "stopped",
902
+ reason: unitPhaseResult.reason ?? "unit-break",
903
+ unitType: iterData.unitType,
904
+ unitId: iterData.unitId,
905
+ failureClass: "execution",
906
+ });
844
907
  finishTurn("stopped", "execution", "unit-break");
845
908
  break;
846
909
  }
@@ -848,12 +911,25 @@ export async function autoLoop(
848
911
  // ── Phase 5: Finalize ───────────────────────────────────────────────
849
912
 
850
913
  let finalizeResult: Awaited<ReturnType<typeof runFinalize>>;
914
+ journalReporter.emit("post-unit-finalize-start", {
915
+ iteration,
916
+ unitType: iterData.unitType,
917
+ unitId: iterData.unitId,
918
+ });
851
919
  try {
852
920
  finalizeResult = await runFinalize(ic, iterData, loopState, sidecarItem);
853
921
  } catch (err) {
922
+ const error = formatDispatchExceptionSummary({ error: err });
923
+ journalReporter.emit("post-unit-finalize-end", {
924
+ iteration,
925
+ unitType: iterData.unitType,
926
+ unitId: iterData.unitId,
927
+ status: "failed",
928
+ error,
929
+ });
854
930
  dispatchSettled = settleDispatchFailed(
855
931
  dispatchId,
856
- formatDispatchExceptionSummary({ error: err }),
932
+ error,
857
933
  {
858
934
  markFailed: markDispatchFailed,
859
935
  logWriteFailure: logDispatchLedgerWriteFailure,
@@ -865,6 +941,15 @@ export async function autoLoop(
865
941
  unitType: iterData.unitType,
866
942
  unitId: iterData.unitId,
867
943
  });
944
+ const finalizeReason = finalizeResult.action === "break" ? finalizeResult.reason : undefined;
945
+ journalReporter.emit("post-unit-finalize-end", {
946
+ iteration,
947
+ unitType: iterData.unitType,
948
+ unitId: iterData.unitId,
949
+ status: finalizeResult.action === "next" ? "completed" : finalizeResult.action === "continue" ? "retry" : "stopped",
950
+ action: finalizeResult.action,
951
+ ...(finalizeReason ? { reason: finalizeReason } : {}),
952
+ });
868
953
  const finalizeDecision = decideFinalizeResult(
869
954
  finalizeResult.action === "break"
870
955
  ? { action: "break", reason: finalizeResult.reason }
@@ -877,6 +962,13 @@ export async function autoLoop(
877
962
  markFailed: markDispatchFailed,
878
963
  logWriteFailure: logDispatchLedgerWriteFailure,
879
964
  }) || dispatchSettled;
965
+ finishIncompleteIteration({
966
+ status: "stopped",
967
+ reason: finalizeReason ?? "finalize-break",
968
+ unitType: iterData.unitType,
969
+ unitId: iterData.unitId,
970
+ failureClass: finalizeDecision.failureClass,
971
+ });
880
972
  finishTurn("stopped", finalizeDecision.failureClass, finalizeDecision.turnError);
881
973
  break;
882
974
  }
@@ -885,6 +977,12 @@ export async function autoLoop(
885
977
  markFailed: markDispatchFailed,
886
978
  logWriteFailure: logDispatchLedgerWriteFailure,
887
979
  }) || dispatchSettled;
980
+ finishIncompleteIteration({
981
+ status: "retry",
982
+ reason: "finalize-retry",
983
+ unitType: iterData.unitType,
984
+ unitId: iterData.unitId,
985
+ });
888
986
  finishTurn("retry");
889
987
  continue;
890
988
  }
@@ -909,11 +1007,6 @@ export async function autoLoop(
909
1007
  ) || dispatchSettled;
910
1008
  }
911
1009
 
912
- // Always emit iteration-end on error so the journal records iteration
913
- // completion even on failure (#2344). Without this, errors in
914
- // runFinalize leave the journal incomplete, making diagnosis harder.
915
- journalReporter.emit("iteration-end", { iteration, error: msg });
916
-
917
1010
  // ── Pre-send model-policy block: not a retryable error (#4959 / #4850) ──
918
1011
  // The model-policy gate runs before the prompt is sent. When every
919
1012
  // candidate model is denied (cross-provider disabled + flat-rate
@@ -938,6 +1031,12 @@ export async function autoLoop(
938
1031
  });
939
1032
  ctx.ui.notify(policyDecision.notifyMessage, "error");
940
1033
  journalReporter.emit("unit-end", policyDecision.journalData);
1034
+ finishIncompleteIteration({
1035
+ status: "blocked",
1036
+ reason: "model-policy-dispatch-blocked",
1037
+ unitType: loopErr.unitType,
1038
+ unitId: loopErr.unitId,
1039
+ });
941
1040
  // Carry the blocked unit identity into the turn-result observer:
942
1041
  // the throw originated inside dispatch, so observedUnitType/Id were
943
1042
  // not assigned by the success path at lines 453/631/647 — but the
@@ -951,6 +1050,11 @@ export async function autoLoop(
951
1050
  break;
952
1051
  }
953
1052
 
1053
+ // Always emit iteration-end on error so the journal records iteration
1054
+ // completion even on failure (#2344). Without this, errors in
1055
+ // runFinalize leave the journal incomplete, making diagnosis harder.
1056
+ finishIncompleteIteration({ status: "failed", error: msg });
1057
+
954
1058
  // ── Infrastructure errors: immediate stop, no retry ──
955
1059
  // These are unrecoverable (disk full, OOM, etc.). Retrying just burns
956
1060
  // LLM budget on guaranteed failures.
@@ -58,6 +58,7 @@ import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "
58
58
  import { getEligibleSlices } from "../slice-parallel-eligibility.js";
59
59
  import { startSliceParallel } from "../slice-parallel-orchestrator.js";
60
60
  import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
61
+ import { reconcileBeforeSpawn } from "../state-reconciliation.js";
61
62
  import type { MinimalModelRegistry } from "../context-budget.js";
62
63
  import type { PostflightResult, PreflightResult } from "../clean-root-preflight.js";
63
64
  import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
@@ -74,6 +75,8 @@ import {
74
75
  } from "../workflow-mcp.js";
75
76
  import { resolveManifest } from "../unit-context-manifest.js";
76
77
  import { createWorktreeSafetyModule, type WorktreeSafetyResult } from "../worktree-safety.js";
78
+ import { isSuspiciousGhostCompletion } from "../auto-unit-closeout.js";
79
+ import { decideVerificationRetry, verificationRetryKey } from "./verification-retry-policy.js";
77
80
 
78
81
  // ─── Path Comparison Helper ───────────────────────────────────────────────
79
82
  /** Compare two paths for physical identity, tolerating trailing slashes and symlinks. */
@@ -81,6 +84,56 @@ function isSamePathLocal(a: string, b: string): boolean {
81
84
  return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
82
85
  }
83
86
 
87
+ async function applyVerificationRetryPolicy(
88
+ ic: IterationContext,
89
+ unitType: string | undefined,
90
+ phase: "artifact-verification-retry" | "verification-retry",
91
+ ): Promise<PhaseResult | null> {
92
+ const { ctx, pi, s, deps } = ic;
93
+ const retryInfo = s.pendingVerificationRetry;
94
+ const key = unitType && retryInfo
95
+ ? verificationRetryKey(unitType, retryInfo.unitId)
96
+ : undefined;
97
+ const decision = decideVerificationRetry({
98
+ unitType,
99
+ retryInfo,
100
+ previousFailureHash: key ? s.verificationRetryFailureHashes.get(key) : undefined,
101
+ });
102
+
103
+ if (decision.action === "pause") {
104
+ s.pendingVerificationRetry = null;
105
+ debugLog("autoLoop", {
106
+ phase: `${phase}-paused`,
107
+ reason: decision.reason,
108
+ unitType,
109
+ unitId: retryInfo?.unitId,
110
+ failureHash: decision.failureHash,
111
+ });
112
+ ctx.ui.notify(
113
+ decision.reason === "duplicate-failure-context"
114
+ ? `Verification retry for ${unitType ?? "unit"} ${retryInfo?.unitId ?? "unknown"} produced the same failure context. Pausing auto-mode instead of re-dispatching.`
115
+ : "Verification retry requested without retry context. Pausing auto-mode instead of re-dispatching.",
116
+ "warning",
117
+ );
118
+ await deps.pauseAuto(ctx, pi);
119
+ return { action: "break", reason: decision.reason };
120
+ }
121
+
122
+ s.verificationRetryFailureHashes.set(decision.key, decision.failureHash);
123
+ debugLog("autoLoop", {
124
+ phase: `${phase}-backoff`,
125
+ iteration: ic.iteration,
126
+ unitType,
127
+ unitId: retryInfo?.unitId,
128
+ attempt: retryInfo?.attempt,
129
+ delayMs: decision.delayMs,
130
+ baseDelayMs: decision.baseDelayMs,
131
+ failureHash: decision.failureHash,
132
+ });
133
+ await new Promise<void>((resolve) => setTimeout(resolve, decision.delayMs));
134
+ return null;
135
+ }
136
+
84
137
  export function shouldDegradeEmptyWorktreeToProjectRoot(
85
138
  worktreeClassification: ReturnType<typeof classifyProject>,
86
139
  projectRootClassification: ReturnType<typeof classifyProject>,
@@ -826,6 +879,16 @@ export async function runPreDispatch(
826
879
  `Slice-parallel: dispatching ${eligible.length} eligible slices for ${mid}.`,
827
880
  "info",
828
881
  );
882
+ // ADR-017 #5707: reconcile before spawning so each worker doesn't
883
+ // independently race on the same drift. Failure aborts the spawn.
884
+ const spawnGate = await reconcileBeforeSpawn(s.basePath);
885
+ if (!spawnGate.ok) {
886
+ ctx.ui.notify(
887
+ `Slice-parallel: aborting spawn — ${spawnGate.reason}`,
888
+ "error",
889
+ );
890
+ return { action: "break", reason: `slice-parallel-reconciliation-failed: ${spawnGate.reason}` };
891
+ }
829
892
  const result = await startSliceParallel(
830
893
  s.basePath,
831
894
  mid,
@@ -1008,7 +1071,13 @@ export async function runPreDispatch(
1008
1071
  "All milestones complete.",
1009
1072
  "success",
1010
1073
  );
1011
- await deps.stopAuto(ctx, pi, "All milestones complete");
1074
+ await deps.stopAuto(ctx, pi, "All milestones complete", {
1075
+ completionWidget: {
1076
+ milestoneId: s.currentMilestoneId,
1077
+ milestoneTitle: midTitle,
1078
+ allMilestonesComplete: true,
1079
+ },
1080
+ });
1012
1081
  } else if (incomplete.length === 0 && state.registry.length === 0) {
1013
1082
  // Empty registry — no milestones visible, likely a path resolution bug
1014
1083
  const diag = `basePath=${s.basePath}, phase=${state.phase}`;
@@ -1103,7 +1172,23 @@ export async function runPreDispatch(
1103
1172
  `Milestone ${mid} complete.`,
1104
1173
  "success",
1105
1174
  );
1106
- await closeoutAndStop(ctx, pi, s, deps, `Milestone ${mid} complete`);
1175
+ if (s.currentUnit) {
1176
+ await deps.closeoutUnit(
1177
+ ctx,
1178
+ s.basePath,
1179
+ s.currentUnit.type,
1180
+ s.currentUnit.id,
1181
+ s.currentUnit.startedAt,
1182
+ deps.buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id),
1183
+ );
1184
+ s.currentUnit = null;
1185
+ }
1186
+ await deps.stopAuto(ctx, pi, `Milestone ${mid} complete`, {
1187
+ completionWidget: {
1188
+ milestoneId: mid,
1189
+ milestoneTitle: midTitle,
1190
+ },
1191
+ });
1107
1192
  debugLog("autoLoop", { phase: "exit", reason: "milestone-complete" });
1108
1193
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "terminal", data: { reason: "milestone-complete", milestoneId: mid } });
1109
1194
  return { action: "break", reason: "milestone-complete" };
@@ -1266,16 +1351,14 @@ export async function runDispatch(
1266
1351
  // ── Sliding-window stuck detection with graduated recovery ──
1267
1352
  const derivedKey = `${unitType}/${unitId}`;
1268
1353
 
1269
- // Always record this dispatch in the sliding window so detectStuck() has
1270
- // accurate history. Skipping the push when pendingVerificationRetry is set
1271
- // caused infinite artifact-retry loops to be invisible to stuck detection
1272
- // (#2007). Only the *response* to a stuck signal is suppressed during retries.
1354
+ // Always record this dispatch in the sliding window and run detection so
1355
+ // Rules 1/3/4 can catch retry loops with repeated failure content (#5719).
1356
+ // Rules 2/2b suppress legitimate retry backoff through the dispatch ledger.
1273
1357
  loopState.recentUnits.push({ key: derivedKey });
1274
1358
  if (loopState.recentUnits.length > STUCK_WINDOW_SIZE) loopState.recentUnits.shift();
1275
1359
 
1276
- if (!s.pendingVerificationRetry) {
1277
- const stuckSignal = detectStuck(loopState.recentUnits);
1278
- if (stuckSignal) {
1360
+ const stuckSignal = detectStuck(loopState.recentUnits);
1361
+ if (stuckSignal) {
1279
1362
  debugLog("autoLoop", {
1280
1363
  phase: "stuck-check",
1281
1364
  unitType,
@@ -1391,16 +1474,15 @@ export async function runDispatch(
1391
1474
  );
1392
1475
  return { action: "break", reason: "stuck-detected" };
1393
1476
  }
1394
- } else {
1395
- // Progress detected — reset recovery counter
1396
- if (loopState.stuckRecoveryAttempts > 0) {
1397
- debugLog("autoLoop", {
1398
- phase: "stuck-counter-reset",
1399
- from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
1400
- to: derivedKey,
1401
- });
1402
- loopState.stuckRecoveryAttempts = 0;
1403
- }
1477
+ } else {
1478
+ // Progress detected — reset recovery counter
1479
+ if (loopState.stuckRecoveryAttempts > 0) {
1480
+ debugLog("autoLoop", {
1481
+ phase: "stuck-counter-reset",
1482
+ from: loopState.recentUnits[loopState.recentUnits.length - 2]?.key ?? "",
1483
+ to: derivedKey,
1484
+ });
1485
+ loopState.stuckRecoveryAttempts = 0;
1404
1486
  }
1405
1487
  }
1406
1488
 
@@ -1984,6 +2066,33 @@ export async function runUnitPhase(
1984
2066
  status: unitResult.status,
1985
2067
  });
1986
2068
 
2069
+ if (
2070
+ unitResult.status === "completed" &&
2071
+ s.currentUnit &&
2072
+ (unitResult.event?.messages?.length ?? 0) === 0 &&
2073
+ isSuspiciousGhostCompletion(ctx, unitResult.requestDispatchedAt ?? s.currentUnit.startedAt)
2074
+ ) {
2075
+ const message =
2076
+ `${unitType} ${unitId} completed without assistant output or tool calls; treating as a stale ghost completion.`;
2077
+ debugLog("autoLoop", {
2078
+ phase: "ghost-completion",
2079
+ iteration: ic.iteration,
2080
+ unitType,
2081
+ unitId,
2082
+ elapsedMs: Date.now() - (unitResult.requestDispatchedAt ?? s.currentUnit.startedAt),
2083
+ });
2084
+ logWarning("engine", message);
2085
+ ctx.ui.notify(`${message} Pausing auto-mode before closeout side effects.`, "warning");
2086
+ await emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, {
2087
+ message,
2088
+ category: "unknown",
2089
+ isTransient: true,
2090
+ });
2091
+ s.currentUnit = null;
2092
+ await deps.pauseAuto(ctx, pi);
2093
+ return { action: "break", reason: "ghost-completion" };
2094
+ }
2095
+
1987
2096
  // Now that runUnit has called newSession(), the session file path is correct.
1988
2097
  const sessionFile = deps.getSessionFile(ctx);
1989
2098
  deps.updateSessionLock(
@@ -2364,6 +2473,14 @@ export async function runFinalize(
2364
2473
  attempt: retryInfo?.attempt,
2365
2474
  },
2366
2475
  });
2476
+ const retryPolicyResult = await applyVerificationRetryPolicy(
2477
+ ic,
2478
+ preUnitSnapshot?.type,
2479
+ "artifact-verification-retry",
2480
+ );
2481
+ if (retryPolicyResult) {
2482
+ return retryPolicyResult;
2483
+ }
2367
2484
  // Continue the loop — next iteration will inject the retry context into the prompt.
2368
2485
  debugLog("autoLoop", { phase: "artifact-verification-retry", iteration: ic.iteration });
2369
2486
  return { action: "continue" };
@@ -2401,6 +2518,14 @@ export async function runFinalize(
2401
2518
  debugLog("autoLoop", { phase: "sidecar-verification-retry-skipped", iteration: ic.iteration });
2402
2519
  } else {
2403
2520
  // s.pendingVerificationRetry was set by runPostUnitVerification.
2521
+ const retryPolicyResult = await applyVerificationRetryPolicy(
2522
+ ic,
2523
+ iterData.unitType,
2524
+ "verification-retry",
2525
+ );
2526
+ if (retryPolicyResult) {
2527
+ return retryPolicyResult;
2528
+ }
2404
2529
  // Continue the loop — next iteration will inject the retry context into the prompt.
2405
2530
  debugLog("autoLoop", { phase: "verification-retry", iteration: ic.iteration });
2406
2531
  return { action: "continue" };
@@ -26,6 +26,31 @@ import { debugLog } from "../debug-logger.js";
26
26
  import { logWarning, logError } from "../workflow-logger.js";
27
27
  import { resolveAutoSupervisorConfig } from "../preferences.js";
28
28
  import { formatAutoUnitWorkingMessage } from "../working-output-messages.js";
29
+ import { readUnitRuntimeRecord, type AutoUnitRuntimeRecord } from "../unit-runtime.js";
30
+
31
+ const UNIT_FAILSAFE_BUFFER_MS = 30_000;
32
+ const UNIT_FAILSAFE_RECHECK_MS = 30_000;
33
+
34
+ export function shouldDeferUnitFailsafeTimeout(
35
+ runtime: AutoUnitRuntimeRecord | null,
36
+ opts: {
37
+ nowMs: number;
38
+ currentUnitStartedAt?: number;
39
+ freshProgressMs: number;
40
+ },
41
+ ): boolean {
42
+ if (!runtime) return false;
43
+ if (opts.currentUnitStartedAt === undefined) return false;
44
+ if (runtime.startedAt !== opts.currentUnitStartedAt) return false;
45
+ if (runtime.lastProgressAt <= 0) return false;
46
+ const progressAgeMs = opts.nowMs - runtime.lastProgressAt;
47
+ if (progressAgeMs < 0) return false;
48
+ if (progressAgeMs > opts.freshProgressMs) return false;
49
+ if (runtime.phase === "recovered") return true;
50
+ if (runtime.lastProgressKind.includes("recovery")) return true;
51
+ if (runtime.recoveryAttempts && runtime.recoveryAttempts > 0) return true;
52
+ return progressAgeMs >= 0 && progressAgeMs <= opts.freshProgressMs;
53
+ }
29
54
 
30
55
  // Tracks the latest session-switch attempt so a late timeout settlement from an
31
56
  // older runUnit() call cannot clear the guard for a newer one.
@@ -192,8 +217,12 @@ export async function runUnit(
192
217
  // Without this, a crashed agent that never emits agent_end hangs the loop (#3161).
193
218
  const supervisor = resolveAutoSupervisorConfig();
194
219
  const UNIT_HARD_TIMEOUT_MS = Math.max(
195
- 30_000,
196
- ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + 30_000,
220
+ UNIT_FAILSAFE_BUFFER_MS,
221
+ ((supervisor.hard_timeout_minutes ?? 30) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS,
222
+ );
223
+ const freshProgressMs = Math.max(
224
+ UNIT_FAILSAFE_BUFFER_MS,
225
+ ((supervisor.idle_timeout_minutes ?? 10) * 60 * 1000) + UNIT_FAILSAFE_BUFFER_MS,
197
226
  );
198
227
  let unitTimeoutHandle: ReturnType<typeof setTimeout> | undefined;
199
228
  let result: UnitResult;
@@ -205,9 +234,45 @@ export async function runUnit(
205
234
 
206
235
  debugLog("runUnit", { phase: "awaiting-agent-end", unitType, unitId });
207
236
  const timeoutResult = new Promise<UnitResult>((resolve) => {
208
- unitTimeoutHandle = setTimeout(() => {
237
+ const settleOrDefer = () => {
238
+ let runtime: AutoUnitRuntimeRecord | null;
239
+ try {
240
+ runtime = readUnitRuntimeRecord(s.basePath, unitType, unitId);
241
+ } catch (error) {
242
+ debugLog("runUnit", {
243
+ phase: "unit-failsafe-runtime-read-failed",
244
+ unitType,
245
+ unitId,
246
+ error: error instanceof Error ? error.message : String(error),
247
+ });
248
+ resolve({
249
+ status: "cancelled",
250
+ errorContext: {
251
+ message: "Unit hard timeout — supervision may have failed; runtime progress could not be read",
252
+ category: "timeout",
253
+ isTransient: true,
254
+ },
255
+ });
256
+ return;
257
+ }
258
+ if (shouldDeferUnitFailsafeTimeout(runtime, {
259
+ nowMs: Date.now(),
260
+ currentUnitStartedAt: s.currentUnit?.startedAt,
261
+ freshProgressMs,
262
+ })) {
263
+ debugLog("runUnit", {
264
+ phase: "unit-failsafe-deferred",
265
+ unitType,
266
+ unitId,
267
+ runtimePhase: runtime?.phase,
268
+ lastProgressKind: runtime?.lastProgressKind,
269
+ });
270
+ unitTimeoutHandle = setTimeout(settleOrDefer, UNIT_FAILSAFE_RECHECK_MS);
271
+ return;
272
+ }
209
273
  resolve({ status: "cancelled", errorContext: { message: "Unit hard timeout — supervision may have failed", category: "timeout", isTransient: true } });
210
- }, UNIT_HARD_TIMEOUT_MS);
274
+ };
275
+ unitTimeoutHandle = setTimeout(settleOrDefer, UNIT_HARD_TIMEOUT_MS);
211
276
  });
212
277
  result = await runWithTurnGeneration(capturedTurnGen, () =>
213
278
  Promise.race([unitPromise, timeoutResult]),
@@ -88,6 +88,7 @@ export class AutoSession {
88
88
  // ── Lifecycle ────────────────────────────────────────────────────────────
89
89
  active = false;
90
90
  paused = false;
91
+ completionStopInProgress = false;
91
92
  stepMode = false;
92
93
  verbose = false;
93
94
  activeEngineId: string | null = null;
@@ -158,6 +159,7 @@ export class AutoSession {
158
159
  pendingCrashRecovery: string | null = null;
159
160
  pendingVerificationRetry: PendingVerificationRetry | null = null;
160
161
  readonly verificationRetryCount = new Map<string, number>();
162
+ readonly verificationRetryFailureHashes = new Map<string, string>();
161
163
  pausedSessionFile: string | null = null;
162
164
  pausedUnitType: string | null = null;
163
165
  pausedUnitId: string | null = null;
@@ -286,6 +288,7 @@ export class AutoSession {
286
288
  // Lifecycle
287
289
  this.active = false;
288
290
  this.paused = false;
291
+ this.completionStopInProgress = false;
289
292
  this.stepMode = false;
290
293
  this.verbose = false;
291
294
  this.activeEngineId = null;
@@ -334,6 +337,7 @@ export class AutoSession {
334
337
  this.pendingCrashRecovery = null;
335
338
  this.pendingVerificationRetry = null;
336
339
  this.verificationRetryCount.clear();
340
+ this.verificationRetryFailureHashes.clear();
337
341
  this.pausedSessionFile = null;
338
342
  this.pausedUnitType = null;
339
343
  this.pausedUnitId = null;
@@ -372,6 +376,12 @@ export class AutoSession {
372
376
  // Loop promise state lives in auto-loop.ts module scope
373
377
  }
374
378
 
379
+ resetAfterStop(options: { preserveCompletionSurface?: boolean } = {}): void {
380
+ const completionStopInProgress = options.preserveCompletionSurface ? this.completionStopInProgress : false;
381
+ this.reset();
382
+ this.completionStopInProgress = completionStopInProgress;
383
+ }
384
+
375
385
  toJSON(): Record<string, unknown> {
376
386
  const orchestrationStatus = this.orchestration?.getStatus();
377
387
  return {