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
@@ -1,3 +1,6 @@
1
+ // Project/App: GSD-2
2
+ // File Purpose: Auto-loop execution, dispatch, recovery, and cancellation regression tests.
3
+
1
4
  import test, { mock } from "node:test";
2
5
  import assert from "node:assert/strict";
3
6
  import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
@@ -17,9 +20,10 @@ import {
17
20
  isSessionSwitchInFlight,
18
21
  isSessionSwitchAbortGraceActive,
19
22
  } from "../auto/resolve.js";
20
- import { runUnit } from "../auto/run-unit.js";
23
+ import { runUnit, shouldDeferUnitFailsafeTimeout } from "../auto/run-unit.js";
24
+ import { writeUnitRuntimeRecord, readUnitRuntimeRecord } from "../unit-runtime.js";
21
25
  import { autoLoop } from "../auto/loop.js";
22
- import { runDispatch } from "../auto/phases.js";
26
+ import { runDispatch, runUnitPhase } from "../auto/phases.js";
23
27
  import { detectStuck } from "../auto/detect-stuck.js";
24
28
  import type { UnitResult, AgentEndEvent } from "../auto/types.js";
25
29
  import type { LoopDeps } from "../auto/loop-deps.js";
@@ -162,6 +166,109 @@ test("resolveAgentEnd resolves a pending runUnit promise", async () => {
162
166
  assert.deepEqual(result.event, event);
163
167
  });
164
168
 
169
+ test("runUnit failsafe defers cancellation while timeout recovery is making fresh progress", async () => {
170
+ _resetPendingResolve();
171
+ mock.timers.enable();
172
+ const originalCwd = process.cwd();
173
+
174
+ try {
175
+ mock.timers.setTime(10_000);
176
+ const ctx = makeMockCtx();
177
+ const pi = makeMockPi();
178
+ const s = makeMockSession();
179
+ s.basePath = mkdtempSync(join(tmpdir(), "gsd-rununit-recovery-"));
180
+ s.currentUnit = { type: "task", id: "T01", startedAt: 1234 };
181
+
182
+ const resultPromise = runUnit(ctx, pi, s, "task", "T01", "prompt");
183
+ await waitForMicrotasks(() => pi.calls.length === 1, "unit dispatch");
184
+
185
+ writeUnitRuntimeRecord(s.basePath, "task", "T01", 1234, {
186
+ phase: "recovered",
187
+ recoveryAttempts: 1,
188
+ lastProgressKind: "hard-recovery-retry",
189
+ lastProgressAt: Date.now(),
190
+ });
191
+ assert.equal(
192
+ shouldDeferUnitFailsafeTimeout(readUnitRuntimeRecord(s.basePath, "task", "T01"), {
193
+ nowMs: Date.now(),
194
+ currentUnitStartedAt: s.currentUnit.startedAt,
195
+ freshProgressMs: 30_000,
196
+ }),
197
+ true,
198
+ "fresh recovery runtime should defer the failsafe",
199
+ );
200
+
201
+ setTimeout(() => {
202
+ writeUnitRuntimeRecord(s.basePath, "task", "T01", 1234, {
203
+ phase: "recovered",
204
+ recoveryAttempts: 1,
205
+ lastProgressKind: "hard-recovery-retry",
206
+ lastProgressAt: Date.now(),
207
+ });
208
+ }, (30 * 60 * 1000) + 29_000);
209
+
210
+ mock.timers.tick((30 * 60 * 1000) + 31_000);
211
+ await Promise.resolve();
212
+
213
+ resolveAgentEnd(makeEvent());
214
+ const result = await resultPromise;
215
+ assert.equal(result.status, "completed");
216
+ } finally {
217
+ mock.timers.reset();
218
+ process.chdir(originalCwd);
219
+ }
220
+ });
221
+
222
+ test("shouldDeferUnitFailsafeTimeout rejects stale runtime progress", () => {
223
+ assert.equal(
224
+ shouldDeferUnitFailsafeTimeout({
225
+ version: 1,
226
+ unitType: "task",
227
+ unitId: "T01",
228
+ startedAt: 1234,
229
+ updatedAt: 1,
230
+ phase: "recovered",
231
+ wrapupWarningSent: false,
232
+ continueHereFired: false,
233
+ timeoutAt: 1,
234
+ lastProgressAt: 1,
235
+ progressCount: 1,
236
+ lastProgressKind: "hard-recovery-retry",
237
+ recoveryAttempts: 1,
238
+ }, {
239
+ nowMs: 120_000,
240
+ currentUnitStartedAt: 1234,
241
+ freshProgressMs: 30_000,
242
+ }),
243
+ false,
244
+ );
245
+ });
246
+
247
+ test("shouldDeferUnitFailsafeTimeout rejects future runtime progress", () => {
248
+ assert.equal(
249
+ shouldDeferUnitFailsafeTimeout({
250
+ version: 1,
251
+ unitType: "task",
252
+ unitId: "T01",
253
+ startedAt: 1234,
254
+ updatedAt: 1,
255
+ phase: "recovered",
256
+ wrapupWarningSent: false,
257
+ continueHereFired: false,
258
+ timeoutAt: 1,
259
+ lastProgressAt: 150_000,
260
+ progressCount: 1,
261
+ lastProgressKind: "hard-recovery-retry",
262
+ recoveryAttempts: 1,
263
+ }, {
264
+ nowMs: 120_000,
265
+ currentUnitStartedAt: 1234,
266
+ freshProgressMs: 30_000,
267
+ }),
268
+ false,
269
+ );
270
+ });
271
+
165
272
  test("resolveAgentEnd drops event when no promise is pending", () => {
166
273
  _resetPendingResolve();
167
274
 
@@ -682,7 +789,6 @@ function makeMockDeps(
682
789
  pruneQueueOrder: () => {},
683
790
  isInAutoWorktree: () => false,
684
791
  shouldUseWorktreeIsolation: () => false,
685
- mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: true }),
686
792
  teardownAutoWorktree: () => {},
687
793
  createAutoWorktree: () => "/tmp/wt",
688
794
  captureIntegrationBranch: () => {},
@@ -785,6 +891,7 @@ function makeLoopSession(overrides?: Partial<Record<string, unknown>>) {
785
891
  lastBudgetAlertLevel: 0,
786
892
  pendingVerificationRetry: null,
787
893
  pendingCrashRecovery: null,
894
+ verificationRetryFailureHashes: new Map<string, string>(),
788
895
  pendingQuickTasks: [],
789
896
  sidecarQueue: [],
790
897
  autoModeStartModel: null,
@@ -1294,6 +1401,107 @@ test("autoLoop calls deriveState → resolveDispatch → runUnit in sequence", a
1294
1401
  );
1295
1402
  });
1296
1403
 
1404
+ test("autoLoop journals post-unit finalize stop after completed unit", async () => {
1405
+ _resetPendingResolve();
1406
+
1407
+ const ctx = makeMockCtx();
1408
+ ctx.ui.setStatus = () => {};
1409
+ ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
1410
+ const pi = makeMockPi();
1411
+ const s = makeLoopSession();
1412
+ const journalEvents: Array<{ eventType: string; data?: any }> = [];
1413
+
1414
+ const deps = makeMockDeps({
1415
+ postUnitPreVerification: async () => {
1416
+ deps.callLog.push("postUnitPreVerification");
1417
+ s.lastGitActionFailure = "commit failed";
1418
+ return "dispatched" as const;
1419
+ },
1420
+ emitJournalEvent: (entry: any) => {
1421
+ journalEvents.push(entry);
1422
+ },
1423
+ });
1424
+
1425
+ const loopPromise = autoLoop(ctx, pi, s, deps);
1426
+ await new Promise((r) => setTimeout(r, 50));
1427
+ resolveAgentEnd(makeEvent());
1428
+ await loopPromise;
1429
+
1430
+ assert.ok(
1431
+ deps.callLog.includes("postUnitPreVerification"),
1432
+ "completed units must enter post-unit pre-verification before stopping",
1433
+ );
1434
+ assert.ok(
1435
+ !deps.callLog.includes("runPostUnitVerification"),
1436
+ "git-closeout stop should not run later verification phases",
1437
+ );
1438
+
1439
+ const unitEndIndex = journalEvents.findIndex((e) => e.eventType === "unit-end");
1440
+ const finalizeStartIndex = journalEvents.findIndex((e) => e.eventType === "post-unit-finalize-start");
1441
+ const finalizeEndIndex = journalEvents.findIndex((e) => e.eventType === "post-unit-finalize-end");
1442
+ const iterationEndIndex = journalEvents.findIndex((e) => e.eventType === "iteration-end");
1443
+
1444
+ assert.ok(unitEndIndex >= 0, "unit-end should be journaled after agent completion");
1445
+ assert.ok(finalizeStartIndex > unitEndIndex, "post-unit finalize must start after unit-end");
1446
+ assert.ok(finalizeEndIndex > finalizeStartIndex, "post-unit finalize must journal its stop result");
1447
+ assert.ok(iterationEndIndex > finalizeEndIndex, "iteration-end must be emitted even when finalize stops");
1448
+
1449
+ assert.deepEqual(journalEvents[finalizeEndIndex]!.data, {
1450
+ iteration: 1,
1451
+ unitType: "execute-task",
1452
+ unitId: "M001/S01/T01",
1453
+ status: "stopped",
1454
+ action: "break",
1455
+ reason: "git-closeout-failure",
1456
+ });
1457
+ assert.deepEqual(journalEvents[iterationEndIndex]!.data, {
1458
+ iteration: 1,
1459
+ status: "stopped",
1460
+ reason: "git-closeout-failure",
1461
+ unitType: "execute-task",
1462
+ unitId: "M001/S01/T01",
1463
+ failureClass: "git",
1464
+ });
1465
+ });
1466
+
1467
+ test("autoLoop journals iteration-end when unit phase breaks after cancelled unit", async () => {
1468
+ _resetPendingResolve();
1469
+
1470
+ const ctx = makeMockCtx();
1471
+ ctx.ui.setStatus = () => {};
1472
+ ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
1473
+ const pi = makeMockPi();
1474
+ const s = makeLoopSession();
1475
+ const journalEvents: Array<{ eventType: string; data?: any }> = [];
1476
+
1477
+ const deps = makeMockDeps({
1478
+ emitJournalEvent: (entry: any) => {
1479
+ journalEvents.push(entry);
1480
+ },
1481
+ });
1482
+
1483
+ const loopPromise = autoLoop(ctx, pi, s, deps);
1484
+ await new Promise((r) => setTimeout(r, 50));
1485
+ resolveAgentEndCancelled();
1486
+ await loopPromise;
1487
+
1488
+ const unitEndIndex = journalEvents.findIndex(
1489
+ (e) => e.eventType === "unit-end" && e.data?.status === "cancelled",
1490
+ );
1491
+ const iterationEndIndex = journalEvents.findIndex((e) => e.eventType === "iteration-end");
1492
+
1493
+ assert.ok(unitEndIndex >= 0, "cancelled unit should still emit unit-end");
1494
+ assert.ok(iterationEndIndex > unitEndIndex, "unit-phase break must close the iteration after unit-end");
1495
+ assert.deepEqual(journalEvents[iterationEndIndex]!.data, {
1496
+ iteration: 1,
1497
+ status: "stopped",
1498
+ reason: "unit-aborted",
1499
+ unitType: "execute-task",
1500
+ unitId: "M001/S01/T01",
1501
+ failureClass: "execution",
1502
+ });
1503
+ });
1504
+
1297
1505
  test("crash lock records session file from AFTER newSession, not before (#1710)", async (t) => {
1298
1506
  _resetPendingResolve();
1299
1507
 
@@ -1404,86 +1612,152 @@ test("crash lock records session file from AFTER newSession, not before (#1710)"
1404
1612
 
1405
1613
  test("autoLoop handles verification retry by continuing loop", async (t) => {
1406
1614
  _resetPendingResolve();
1615
+ mock.timers.enable({ apis: ["Date", "setTimeout"], now: 10_000 });
1407
1616
 
1408
- const ctx = makeMockCtx();
1409
- ctx.ui.setStatus = () => {};
1410
- ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
1411
- const pi = makeMockPi();
1617
+ try {
1618
+ const ctx = makeMockCtx();
1619
+ ctx.ui.setStatus = () => {};
1620
+ ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
1621
+ const pi = makeMockPi();
1412
1622
 
1413
- let verifyCallCount = 0;
1414
- let deriveCallCount = 0;
1415
- const s = makeLoopSession();
1623
+ let verifyCallCount = 0;
1624
+ let deriveCallCount = 0;
1625
+ const s = makeLoopSession();
1416
1626
 
1417
- // Pre-queued verification actions: each entry provides a side-effect + return value
1418
- type VerifyAction = { sideEffect?: () => void; response: "retry" | "continue" };
1419
- const verificationActions: VerifyAction[] = [
1420
- {
1421
- sideEffect: () => {
1422
- // Simulate retry — set pendingVerificationRetry on session
1627
+ // Pre-queued verification actions: each entry provides a side-effect + return value
1628
+ type VerifyAction = { sideEffect?: () => void; response: "retry" | "continue" };
1629
+ const verificationActions: VerifyAction[] = [
1630
+ {
1631
+ sideEffect: () => {
1632
+ // Simulate retry — set pendingVerificationRetry on session
1633
+ s.pendingVerificationRetry = {
1634
+ unitId: "M001/S01/T01",
1635
+ failureContext: "test failed: expected X got Y",
1636
+ attempt: 1,
1637
+ };
1638
+ },
1639
+ response: "retry",
1640
+ },
1641
+ { response: "continue" },
1642
+ ];
1643
+
1644
+ const deps = makeMockDeps({
1645
+ deriveState: async () => {
1646
+ deriveCallCount++;
1647
+ deps.callLog.push("deriveState");
1648
+ return {
1649
+ phase: "executing",
1650
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
1651
+ activeSlice: { id: "S01", title: "Slice 1" },
1652
+ activeTask: { id: "T01" },
1653
+ registry: [{ id: "M001", status: "active" }],
1654
+ blockers: [],
1655
+ } as any;
1656
+ },
1657
+ runPostUnitVerification: async () => {
1658
+ const action = verificationActions[verifyCallCount] ?? { response: "continue" as const };
1659
+ verifyCallCount++;
1660
+ deps.callLog.push("runPostUnitVerification");
1661
+ action.sideEffect?.();
1662
+ return action.response;
1663
+ },
1664
+ postUnitPostVerification: async () => {
1665
+ deps.callLog.push("postUnitPostVerification");
1666
+ // After the retry cycle completes, deactivate
1667
+ s.active = false;
1668
+ return "continue" as const;
1669
+ },
1670
+ });
1671
+
1672
+ const loopPromise = autoLoop(ctx, pi, s, deps);
1673
+
1674
+ // First iteration: runUnit → verification returns "retry" → loop continues
1675
+ await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
1676
+ resolveAgentEnd(makeEvent()); // resolve first unit
1677
+
1678
+ await drainMicrotasks(100);
1679
+ mock.timers.tick(30_000);
1680
+ await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
1681
+ resolveAgentEnd(makeEvent()); // resolve retry unit
1682
+
1683
+ await loopPromise;
1684
+
1685
+ // Verify deriveState was called twice (two iterations)
1686
+ const deriveCount = deps.callLog.filter((c) => c === "deriveState").length;
1687
+ assert.ok(
1688
+ deriveCount >= 2,
1689
+ `deriveState should be called at least 2 times (got ${deriveCount})`,
1690
+ );
1691
+
1692
+ // Verify verification was called twice
1693
+ assert.equal(
1694
+ verifyCallCount,
1695
+ 2,
1696
+ "verification should have been called twice (once retry, once pass)",
1697
+ );
1698
+ } finally {
1699
+ mock.timers.reset();
1700
+ }
1701
+ });
1702
+
1703
+ test("autoLoop pauses instead of redispatching identical verification failure context", async () => {
1704
+ _resetPendingResolve();
1705
+ mock.timers.enable({ apis: ["Date", "setTimeout"], now: 15_000 });
1706
+
1707
+ try {
1708
+ const ctx = makeMockCtx();
1709
+ ctx.ui.setStatus = () => {};
1710
+ ctx.ui.notify = () => {};
1711
+ ctx.sessionManager = { getSessionFile: () => "/tmp/session.json" };
1712
+ const pi = makeMockPi();
1713
+ const s = makeLoopSession();
1714
+ let verifyCallCount = 0;
1715
+ let pauseCallCount = 0;
1716
+
1717
+ const deps = makeMockDeps({
1718
+ deriveState: async () =>
1719
+ ({
1720
+ phase: "executing",
1721
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
1722
+ activeSlice: { id: "S01", title: "Slice 1" },
1723
+ activeTask: { id: "T01" },
1724
+ registry: [{ id: "M001", status: "active" }],
1725
+ blockers: [],
1726
+ }) as any,
1727
+ runPostUnitVerification: async () => {
1728
+ verifyCallCount++;
1729
+ deps.callLog.push("runPostUnitVerification");
1423
1730
  s.pendingVerificationRetry = {
1424
1731
  unitId: "M001/S01/T01",
1425
1732
  failureContext: "test failed: expected X got Y",
1426
- attempt: 1,
1733
+ attempt: verifyCallCount,
1427
1734
  };
1735
+ return "retry" as const;
1428
1736
  },
1429
- response: "retry",
1430
- },
1431
- { response: "continue" },
1432
- ];
1433
-
1434
- const deps = makeMockDeps({
1435
- deriveState: async () => {
1436
- deriveCallCount++;
1437
- deps.callLog.push("deriveState");
1438
- return {
1439
- phase: "executing",
1440
- activeMilestone: { id: "M001", title: "Test", status: "active" },
1441
- activeSlice: { id: "S01", title: "Slice 1" },
1442
- activeTask: { id: "T01" },
1443
- registry: [{ id: "M001", status: "active" }],
1444
- blockers: [],
1445
- } as any;
1446
- },
1447
- runPostUnitVerification: async () => {
1448
- const action = verificationActions[verifyCallCount] ?? { response: "continue" as const };
1449
- verifyCallCount++;
1450
- deps.callLog.push("runPostUnitVerification");
1451
- action.sideEffect?.();
1452
- return action.response;
1453
- },
1454
- postUnitPostVerification: async () => {
1455
- deps.callLog.push("postUnitPostVerification");
1456
- // After the retry cycle completes, deactivate
1457
- s.active = false;
1458
- return "continue" as const;
1459
- },
1460
- });
1461
-
1462
- const loopPromise = autoLoop(ctx, pi, s, deps);
1737
+ pauseAuto: async () => {
1738
+ pauseCallCount++;
1739
+ s.active = false;
1740
+ },
1741
+ });
1463
1742
 
1464
- // First iteration: runUnit verification returns "retry" → loop continues
1465
- await new Promise((r) => setTimeout(r, 50));
1466
- resolveAgentEnd(makeEvent()); // resolve first unit
1743
+ const loopPromise = autoLoop(ctx, pi, s, deps);
1467
1744
 
1468
- // Second iteration: runUnit verification returns "continue"
1469
- await new Promise((r) => setTimeout(r, 50));
1470
- resolveAgentEnd(makeEvent()); // resolve retry unit
1745
+ await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
1746
+ resolveAgentEnd(makeEvent());
1747
+ await drainMicrotasks(100);
1748
+ mock.timers.tick(30_000);
1471
1749
 
1472
- await loopPromise;
1750
+ await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
1751
+ resolveAgentEnd(makeEvent());
1473
1752
 
1474
- // Verify deriveState was called twice (two iterations)
1475
- const deriveCount = deps.callLog.filter((c) => c === "deriveState").length;
1476
- assert.ok(
1477
- deriveCount >= 2,
1478
- `deriveState should be called at least 2 times (got ${deriveCount})`,
1479
- );
1753
+ await loopPromise;
1480
1754
 
1481
- // Verify verification was called twice
1482
- assert.equal(
1483
- verifyCallCount,
1484
- 2,
1485
- "verification should have been called twice (once retry, once pass)",
1486
- );
1755
+ assert.equal(verifyCallCount, 2);
1756
+ assert.equal(pi.calls.length, 2, "duplicate failure should not be redispatched a third time");
1757
+ assert.equal(pauseCallCount, 1, "duplicate failure should pause auto-mode");
1758
+ } finally {
1759
+ mock.timers.reset();
1760
+ }
1487
1761
  });
1488
1762
 
1489
1763
  test("autoLoop handles dispatch stop action", async (t) => {
@@ -1870,76 +2144,84 @@ test("stuck detection: window resets recovery when deriveState returns a differe
1870
2144
  );
1871
2145
  });
1872
2146
 
1873
- test("stuck detection: does not push to window during verification retry", async () => {
2147
+ test("stuck detection: verification retries remain visible to the sliding window", async () => {
1874
2148
  _resetPendingResolve();
2149
+ mock.timers.enable({ apis: ["Date", "setTimeout"], now: 20_000 });
1875
2150
 
1876
- const ctx = makeMockCtx();
1877
- ctx.ui.setStatus = () => {};
1878
- ctx.ui.notify = () => {};
1879
- const pi = makeMockPi();
1880
- const s = makeLoopSession();
2151
+ try {
2152
+ const ctx = makeMockCtx();
2153
+ ctx.ui.setStatus = () => {};
2154
+ ctx.ui.notify = () => {};
2155
+ const pi = makeMockPi();
2156
+ const s = makeLoopSession();
1881
2157
 
1882
- let verifyCallCount = 0;
1883
- let stopReason = "";
2158
+ let verifyCallCount = 0;
2159
+ let stopReason = "";
1884
2160
 
1885
- // Pre-queued responses: 3 retries then a continue (exit)
1886
- const verifyActions: Array<() => "retry" | "continue"> = [
1887
- () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 1 }; return "retry"; },
1888
- () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 2 }; return "retry"; },
1889
- () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed", attempt: 3 }; return "retry"; },
1890
- () => { s.active = false; return "continue"; },
1891
- ];
2161
+ // Pre-queued responses: 3 retries then a continue (exit). Failure
2162
+ // contexts differ so this test exercises stuck-window behavior without
2163
+ // tripping duplicate-failure suppression.
2164
+ const verifyActions: Array<() => "retry" | "continue"> = [
2165
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 1", attempt: 1 }; return "retry"; },
2166
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 2", attempt: 2 }; return "retry"; },
2167
+ () => { s.pendingVerificationRetry = { unitId: "M001/S01/T01", failureContext: "test failed: 3", attempt: 3 }; return "retry"; },
2168
+ () => { s.active = false; return "continue"; },
2169
+ ];
1892
2170
 
1893
- const deps = makeMockDeps({
1894
- deriveState: async () =>
1895
- ({
1896
- phase: "executing",
1897
- activeMilestone: { id: "M001", title: "Test", status: "active" },
1898
- activeSlice: { id: "S01", title: "Slice 1" },
1899
- activeTask: { id: "T01" },
1900
- registry: [{ id: "M001", status: "active" }],
1901
- blockers: [],
1902
- }) as any,
1903
- resolveDispatch: async () => ({
1904
- action: "dispatch" as const,
1905
- unitType: "execute-task",
1906
- unitId: "M001/S01/T01",
1907
- prompt: "do the thing",
1908
- }),
1909
- runPostUnitVerification: async () => {
1910
- const action = verifyActions[verifyCallCount] ?? (() => { s.active = false; return "continue" as const; });
1911
- verifyCallCount++;
1912
- deps.callLog.push("runPostUnitVerification");
1913
- return action();
1914
- },
1915
- stopAuto: async (_ctx?: any, _pi?: any, reason?: string) => {
1916
- deps.callLog.push("stopAuto");
1917
- stopReason = reason ?? "";
1918
- s.active = false;
1919
- },
1920
- });
2171
+ const deps = makeMockDeps({
2172
+ deriveState: async () =>
2173
+ ({
2174
+ phase: "executing",
2175
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
2176
+ activeSlice: { id: "S01", title: "Slice 1" },
2177
+ activeTask: { id: "T01" },
2178
+ registry: [{ id: "M001", status: "active" }],
2179
+ blockers: [],
2180
+ }) as any,
2181
+ resolveDispatch: async () => ({
2182
+ action: "dispatch" as const,
2183
+ unitType: "execute-task",
2184
+ unitId: "M001/S01/T01",
2185
+ prompt: "do the thing",
2186
+ }),
2187
+ runPostUnitVerification: async () => {
2188
+ const action = verifyActions[verifyCallCount] ?? (() => { s.active = false; return "continue" as const; });
2189
+ verifyCallCount++;
2190
+ deps.callLog.push("runPostUnitVerification");
2191
+ return action();
2192
+ },
2193
+ stopAuto: async (_ctx?: any, _pi?: any, reason?: string) => {
2194
+ deps.callLog.push("stopAuto");
2195
+ stopReason = reason ?? "";
2196
+ s.active = false;
2197
+ },
2198
+ });
1921
2199
 
1922
- const loopPromise = autoLoop(ctx, pi, s, deps);
2200
+ const loopPromise = autoLoop(ctx, pi, s, deps);
1923
2201
 
1924
- // Resolve agent_end for 4 iterations (1 initial + 3 retries)
1925
- for (let i = 0; i < 4; i++) {
1926
- await new Promise((r) => setTimeout(r, 30));
1927
- resolveAgentEnd(makeEvent());
1928
- }
2202
+ // Resolve agent_end for 3 attempts. The 4th iteration should stop before
2203
+ // dispatch because retry dispatches stay visible to stuck detection.
2204
+ for (let i = 1; i <= 3; i++) {
2205
+ await waitForMicrotasks(() => pi.calls.length === i, `dispatch ${i}`);
2206
+ resolveAgentEnd(makeEvent());
2207
+ await drainMicrotasks(100);
2208
+ mock.timers.tick(30_000);
2209
+ }
1929
2210
 
1930
- await loopPromise;
2211
+ await loopPromise;
1931
2212
 
1932
- // Even though same unit was derived 4 times, verification retries should
1933
- // not push to the sliding window, so stuck detection should not have fired
1934
- assert.ok(
1935
- !stopReason.includes("Stuck"),
1936
- `stuck detection should not fire during verification retries, got: ${stopReason}`,
1937
- );
1938
- assert.equal(
1939
- verifyCallCount,
1940
- 4,
1941
- "verification should have been called 4 times (1 initial + 3 retries)",
1942
- );
2213
+ assert.ok(
2214
+ stopReason.includes("Stuck"),
2215
+ `stuck detection should fire during repeated verification retries, got: ${stopReason}`,
2216
+ );
2217
+ assert.equal(
2218
+ verifyCallCount,
2219
+ 3,
2220
+ "verification should stop before a 4th repeated retry dispatch",
2221
+ );
2222
+ } finally {
2223
+ mock.timers.reset();
2224
+ }
1943
2225
  });
1944
2226
 
1945
2227
  // ── detectStuck unit tests ────────────────────────────────────────────────────
@@ -2021,7 +2303,8 @@ test("detectStuck: truncates long error strings", () => {
2021
2303
  { key: "A", error: longError },
2022
2304
  ]);
2023
2305
  assert.ok(result?.stuck);
2024
- assert.ok(result!.reason.length < 300, "reason should be truncated");
2306
+ assert.ok(result!.reason.includes(longError.slice(0, 200)), "reason should include the truncated error prefix");
2307
+ assert.equal(result!.reason.includes(longError), false, "reason should not include the full long error");
2025
2308
  });
2026
2309
 
2027
2310
  // NOTE: the "stuck-detected" / "stuck-counter-reset" debug-log grep was
@@ -2265,6 +2548,108 @@ test("resolveAgentEndCancelled with errorContext passes it through to resolved p
2265
2548
  assert.equal(resolved.errorContext!.isTransient, true);
2266
2549
  });
2267
2550
 
2551
+ test("runUnitPhase pauses ghost completions before closeout and finalize side effects", async (t) => {
2552
+ _resetPendingResolve();
2553
+
2554
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-ghost-completion-"));
2555
+ t.after(() => {
2556
+ _resetPendingResolve();
2557
+ rmSync(basePath, { recursive: true, force: true });
2558
+ });
2559
+
2560
+ let closeoutCalls = 0;
2561
+ let preVerificationCalls = 0;
2562
+ let postVerificationCalls = 0;
2563
+ const journalEvents: any[] = [];
2564
+ const deps = makeMockDeps({
2565
+ closeoutUnit: async () => {
2566
+ closeoutCalls++;
2567
+ },
2568
+ postUnitPreVerification: async () => {
2569
+ preVerificationCalls++;
2570
+ return "continue";
2571
+ },
2572
+ postUnitPostVerification: async () => {
2573
+ postVerificationCalls++;
2574
+ return "continue";
2575
+ },
2576
+ emitJournalEvent: (event: any) => {
2577
+ journalEvents.push(event);
2578
+ },
2579
+ });
2580
+ const ctx = {
2581
+ ...makeMockCtx(),
2582
+ ui: {
2583
+ notify: () => {},
2584
+ setStatus: () => {},
2585
+ setWorkingMessage: () => {},
2586
+ },
2587
+ sessionManager: {
2588
+ getEntries: () => [],
2589
+ },
2590
+ modelRegistry: {
2591
+ getProviderAuthMode: () => undefined,
2592
+ isProviderRequestReady: () => true,
2593
+ },
2594
+ } as any;
2595
+ const pi = {
2596
+ ...makeMockPi(),
2597
+ sendMessage: () => {
2598
+ queueMicrotask(() => resolveAgentEnd({ messages: [] }));
2599
+ },
2600
+ } as any;
2601
+ const s = makeLoopSession({
2602
+ basePath,
2603
+ canonicalProjectRoot: basePath,
2604
+ originalBasePath: basePath,
2605
+ });
2606
+ let seq = 0;
2607
+
2608
+ const result = await runUnitPhase(
2609
+ { ctx, pi, s, deps, prefs: undefined, iteration: 1, flowId: "flow-ghost", nextSeq: () => ++seq },
2610
+ {
2611
+ unitType: "execute-task",
2612
+ unitId: "M001/S01/T01",
2613
+ prompt: "do work",
2614
+ finalPrompt: "do work",
2615
+ pauseAfterUatDispatch: false,
2616
+ state: {
2617
+ phase: "executing",
2618
+ activeMilestone: { id: "M001", title: "Milestone" },
2619
+ activeSlice: { id: "S01", title: "Slice" },
2620
+ activeTask: { id: "T01", title: "Task" },
2621
+ registry: [{ id: "M001", title: "Milestone", status: "active" }],
2622
+ recentDecisions: [],
2623
+ blockers: [],
2624
+ nextAction: "",
2625
+ progress: { milestones: { done: 0, total: 1 } },
2626
+ requirements: { active: 0, validated: 0, deferred: 0, outOfScope: 0, blocked: 0, total: 0 },
2627
+ } as any,
2628
+ mid: "M001",
2629
+ midTitle: "Milestone",
2630
+ isRetry: false,
2631
+ previousTier: undefined,
2632
+ },
2633
+ { recentUnits: [], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 },
2634
+ );
2635
+
2636
+ assert.equal(result.action, "break");
2637
+ assert.equal((result as any).reason, "ghost-completion");
2638
+ assert.equal(deps.callLog.includes("pauseAuto"), true);
2639
+ assert.equal(closeoutCalls, 0);
2640
+ assert.equal(preVerificationCalls, 0);
2641
+ assert.equal(postVerificationCalls, 0);
2642
+ assert.equal(s.currentUnit, null);
2643
+ assert.ok(
2644
+ journalEvents.some((event) =>
2645
+ event.eventType === "unit-end" &&
2646
+ event.data?.status === "cancelled" &&
2647
+ event.data?.errorContext?.message.includes("stale ghost completion")
2648
+ ),
2649
+ "ghost completion should emit a cancelled unit-end",
2650
+ );
2651
+ });
2652
+
2268
2653
  test("resolveAgentEndCancelled without args produces no errorContext field", async () => {
2269
2654
  _resetPendingResolve();
2270
2655
 
@@ -2316,60 +2701,75 @@ test("session-switch abort grace window is short-lived and resettable", () => {
2316
2701
 
2317
2702
  test("autoLoop re-iterates when postUnitPreVerification returns retry (#1571)", async () => {
2318
2703
  _resetPendingResolve();
2704
+ mock.timers.enable({ apis: ["Date", "setTimeout"], now: 30_000 });
2319
2705
 
2320
- const ctx = makeMockCtx();
2321
- ctx.ui.setStatus = () => {};
2322
- const pi = makeMockPi();
2323
- const s = makeLoopSession();
2706
+ try {
2707
+ const ctx = makeMockCtx();
2708
+ ctx.ui.setStatus = () => {};
2709
+ const pi = makeMockPi();
2710
+ const s = makeLoopSession();
2324
2711
 
2325
- let preVerifyCallCount = 0;
2326
- // Pre-queued responses: first call returns "retry", second returns "continue"
2327
- const preVerifyResponses = ["retry", "continue"] as const;
2712
+ let preVerifyCallCount = 0;
2713
+ // Pre-queued responses: first call returns "retry", second returns "continue"
2714
+ const preVerifyResponses = ["retry", "continue"] as const;
2328
2715
 
2329
- const deps = makeMockDeps({
2330
- deriveState: async () => {
2331
- deps.callLog.push("deriveState");
2332
- return {
2333
- phase: "executing",
2334
- activeMilestone: { id: "M001", title: "Test", status: "active" },
2335
- activeSlice: { id: "S01", title: "Slice 1" },
2336
- activeTask: { id: "T01" },
2337
- registry: [{ id: "M001", status: "active" }],
2338
- blockers: [],
2339
- } as any;
2340
- },
2341
- postUnitPreVerification: async () => {
2342
- deps.callLog.push("postUnitPreVerification");
2343
- return preVerifyResponses[preVerifyCallCount++] ?? "continue";
2344
- },
2345
- postUnitPostVerification: async () => {
2346
- deps.callLog.push("postUnitPostVerification");
2347
- s.active = false;
2348
- return "continue" as const;
2349
- },
2350
- });
2716
+ const deps = makeMockDeps({
2717
+ deriveState: async () => {
2718
+ deps.callLog.push("deriveState");
2719
+ return {
2720
+ phase: "executing",
2721
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
2722
+ activeSlice: { id: "S01", title: "Slice 1" },
2723
+ activeTask: { id: "T01" },
2724
+ registry: [{ id: "M001", status: "active" }],
2725
+ blockers: [],
2726
+ } as any;
2727
+ },
2728
+ postUnitPreVerification: async () => {
2729
+ deps.callLog.push("postUnitPreVerification");
2730
+ const response = preVerifyResponses[preVerifyCallCount++] ?? "continue";
2731
+ if (response === "retry") {
2732
+ s.pendingVerificationRetry = {
2733
+ unitId: "M001/S01/T01",
2734
+ failureContext: "missing artifact",
2735
+ attempt: 1,
2736
+ };
2737
+ }
2738
+ return response;
2739
+ },
2740
+ postUnitPostVerification: async () => {
2741
+ deps.callLog.push("postUnitPostVerification");
2742
+ s.active = false;
2743
+ return "continue" as const;
2744
+ },
2745
+ });
2351
2746
 
2352
- const loopPromise = autoLoop(ctx, pi, s, deps);
2747
+ const loopPromise = autoLoop(ctx, pi, s, deps);
2353
2748
 
2354
- await new Promise((r) => setTimeout(r, 50));
2355
- resolveAgentEnd(makeEvent());
2749
+ await waitForMicrotasks(() => pi.calls.length === 1, "first dispatch");
2750
+ resolveAgentEnd(makeEvent());
2356
2751
 
2357
- await new Promise((r) => setTimeout(r, 50));
2358
- resolveAgentEnd(makeEvent());
2752
+ await drainMicrotasks(100);
2753
+ mock.timers.tick(30_000);
2754
+ await waitForMicrotasks(() => pi.calls.length === 2, "retry dispatch");
2755
+ resolveAgentEnd(makeEvent());
2359
2756
 
2360
- await loopPromise;
2757
+ await loopPromise;
2361
2758
 
2362
- assert.equal(preVerifyCallCount, 2, "preVerification should be called twice");
2759
+ assert.equal(preVerifyCallCount, 2, "preVerification should be called twice");
2363
2760
 
2364
- const postVerifyCalls = deps.callLog.filter(
2365
- (c: string) => c === "runPostUnitVerification",
2366
- );
2367
- const postPostVerifyCalls = deps.callLog.filter(
2368
- (c: string) => c === "postUnitPostVerification",
2369
- );
2761
+ const postVerifyCalls = deps.callLog.filter(
2762
+ (c: string) => c === "runPostUnitVerification",
2763
+ );
2764
+ const postPostVerifyCalls = deps.callLog.filter(
2765
+ (c: string) => c === "postUnitPostVerification",
2766
+ );
2370
2767
 
2371
- assert.equal(postVerifyCalls.length, 1, "runPostUnitVerification should only be called once");
2372
- assert.equal(postPostVerifyCalls.length, 1, "postUnitPostVerification should only be called once");
2768
+ assert.equal(postVerifyCalls.length, 1, "runPostUnitVerification should only be called once");
2769
+ assert.equal(postPostVerifyCalls.length, 1, "postUnitPostVerification should only be called once");
2770
+ } finally {
2771
+ mock.timers.reset();
2772
+ }
2373
2773
  });
2374
2774
 
2375
2775
  // ─── stopAuto unitPromise leak regression (#1799) ────────────────────────────
@@ -2807,6 +3207,77 @@ test("dispatch Worktree Safety wins before stuck detection for execute-task with
2807
3207
  );
2808
3208
  });
2809
3209
 
3210
+ test("runDispatch runs stuck detection while artifact verification retry is pending (#5719)", async (t) => {
3211
+ _resetPendingResolve();
3212
+
3213
+ const ctx = makeMockCtx();
3214
+ const pi = makeMockPi();
3215
+ const notifications: string[] = [];
3216
+ ctx.ui.notify = (msg: string) => { notifications.push(msg); };
3217
+
3218
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-5719-retry-stuck-"));
3219
+ t.after(() => rmSync(basePath, { recursive: true, force: true }));
3220
+
3221
+ const s = makeLoopSession({
3222
+ basePath,
3223
+ pendingVerificationRetry: {
3224
+ unitId: "M001/S01/T01",
3225
+ failureContext: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
3226
+ attempt: 1,
3227
+ },
3228
+ });
3229
+ const deps = makeMockDeps();
3230
+ const loopState = {
3231
+ recentUnits: [
3232
+ {
3233
+ key: "execute-task/M001/S01/T01",
3234
+ error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
3235
+ },
3236
+ { key: "plan-slice/M001/S02", error: "other failure" },
3237
+ {
3238
+ key: "complete-slice/M001/S01",
3239
+ error: "ENOENT: no such file or directory, access '/tmp/missing-plan.md'",
3240
+ },
3241
+ ],
3242
+ stuckRecoveryAttempts: 0,
3243
+ consecutiveFinalizeTimeouts: 0,
3244
+ };
3245
+
3246
+ const result = await runDispatch(
3247
+ {
3248
+ ctx,
3249
+ pi,
3250
+ s,
3251
+ deps,
3252
+ prefs: undefined,
3253
+ iteration: 1,
3254
+ flowId: "test-flow",
3255
+ nextSeq: () => 1,
3256
+ },
3257
+ {
3258
+ state: {
3259
+ phase: "executing",
3260
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
3261
+ activeSlice: { id: "S01", title: "Slice 1" },
3262
+ activeTask: { id: "T01" },
3263
+ registry: [{ id: "M001", status: "active" }],
3264
+ blockers: [],
3265
+ } as any,
3266
+ mid: "M001",
3267
+ midTitle: "Test",
3268
+ },
3269
+ loopState,
3270
+ );
3271
+
3272
+ assert.equal(result.action, "next", "level-1 stuck recovery should still allow the recovery dispatch");
3273
+ assert.equal(loopState.stuckRecoveryAttempts, 1, "stuck recovery should record the first recovery attempt");
3274
+ assert.ok(deps.callLog.includes("invalidateAllCaches"), "stuck recovery should invalidate caches");
3275
+ assert.ok(
3276
+ notifications.some((n) => n.includes("Missing file referenced twice")),
3277
+ "notification should surface the repeated ENOENT stuck reason",
3278
+ );
3279
+ });
3280
+
2810
3281
  test("dispatch Worktree Safety stops unknown unit types with missing Tool Contract", async (t) => {
2811
3282
  _resetPendingResolve();
2812
3283
 
@@ -3272,6 +3743,13 @@ test("autoLoop classifies ModelPolicyDispatchBlockedError as blocked, not a retr
3272
3743
  );
3273
3744
  assert.ok(unitEnd, "should emit unit-end with status=blocked");
3274
3745
  assert.equal(unitEnd!.data.reason, "model-policy-dispatch-blocked");
3746
+ const unitEndIndex = journalEvents.findIndex(
3747
+ e => e.eventType === "unit-end" && e.data?.status === "blocked",
3748
+ );
3749
+ const iterationEndIndex = journalEvents.findIndex(
3750
+ e => e.eventType === "iteration-end" && e.data?.status === "blocked",
3751
+ );
3752
+ assert.ok(iterationEndIndex > unitEndIndex, "blocked policy iterations must close after unit-end");
3275
3753
 
3276
3754
  // Loop must pause for manual attention, NOT retry until 3-strike hard stop.
3277
3755
  assert.equal(pauseAutoCalls, 1, "should pause once on policy block");