@bastani/atomic 0.9.5-alpha.7 → 0.9.5-alpha.9

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 (317) hide show
  1. package/CHANGELOG.md +33 -2
  2. package/dist/builtin/cursor/package.json +2 -2
  3. package/dist/builtin/intercom/CHANGELOG.md +17 -0
  4. package/dist/builtin/intercom/README.md +27 -21
  5. package/dist/builtin/intercom/broker/broker.ts +3 -6
  6. package/dist/builtin/intercom/broker/paths.ts +42 -5
  7. package/dist/builtin/intercom/broker/spawn.ts +78 -10
  8. package/dist/builtin/intercom/config.ts +6 -3
  9. package/dist/builtin/intercom/contact-supervisor-tool.ts +6 -4
  10. package/dist/builtin/intercom/index-heavy.ts +1 -1
  11. package/dist/builtin/intercom/index.ts +162 -130
  12. package/dist/builtin/intercom/intercom-tool.ts +9 -3
  13. package/dist/builtin/intercom/package.json +10 -1
  14. package/dist/builtin/intercom/result-renderers.ts +1 -1
  15. package/dist/builtin/intercom/ui/compose.ts +2 -2
  16. package/dist/builtin/intercom/ui/inline-message.ts +2 -2
  17. package/dist/builtin/intercom/ui/session-list.ts +2 -2
  18. package/dist/builtin/mcp/CHANGELOG.md +11 -0
  19. package/dist/builtin/mcp/README.md +6 -6
  20. package/dist/builtin/mcp/direct-tools.ts +41 -23
  21. package/dist/builtin/mcp/index.ts +31 -4
  22. package/dist/builtin/mcp/init.ts +4 -43
  23. package/dist/builtin/mcp/metadata-hydration.ts +37 -0
  24. package/dist/builtin/mcp/package.json +1 -1
  25. package/dist/builtin/mcp/proxy-info-modes.ts +67 -14
  26. package/dist/builtin/mcp/startup-warmup.ts +98 -0
  27. package/dist/builtin/mcp/tool-result-renderer.ts +3 -3
  28. package/dist/builtin/subagents/CHANGELOG.md +18 -0
  29. package/dist/builtin/subagents/package.json +1 -1
  30. package/dist/builtin/subagents/src/extension/index.ts +14 -25
  31. package/dist/builtin/subagents/src/extension/startup-maintenance.ts +89 -0
  32. package/dist/builtin/subagents/src/runs/background/async-execution-common.ts +23 -18
  33. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +20 -10
  34. package/dist/builtin/subagents/src/runs/background/subagent-runner-streaming.ts +13 -3
  35. package/dist/builtin/subagents/src/runs/foreground/execution-attempt.ts +10 -2
  36. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +28 -1
  37. package/dist/builtin/subagents/src/tui/render-event-formatting.ts +15 -48
  38. package/dist/builtin/subagents/src/tui/render-layout.ts +9 -0
  39. package/dist/builtin/subagents/src/tui/render-widget-graph.ts +27 -25
  40. package/dist/builtin/subagents/src/tui/render-widget.ts +72 -66
  41. package/dist/builtin/subagents/src/tui/render.ts +1 -1
  42. package/dist/builtin/web-access/package.json +1 -1
  43. package/dist/builtin/workflows/CHANGELOG.md +29 -0
  44. package/dist/builtin/workflows/README.md +4 -2
  45. package/dist/builtin/workflows/builtin/goal-artifacts.ts +3 -1
  46. package/dist/builtin/workflows/builtin/goal-prompts.ts +23 -2
  47. package/dist/builtin/workflows/builtin/goal-reducer.ts +21 -0
  48. package/dist/builtin/workflows/builtin/goal-review.ts +60 -6
  49. package/dist/builtin/workflows/builtin/goal-runner.ts +71 -22
  50. package/dist/builtin/workflows/builtin/goal-types.ts +7 -1
  51. package/dist/builtin/workflows/builtin/ralph-core.ts +52 -4
  52. package/dist/builtin/workflows/builtin/ralph-review-gate.ts +10 -3
  53. package/dist/builtin/workflows/builtin/ralph-reviewer-prompt.ts +23 -2
  54. package/dist/builtin/workflows/builtin/ralph-runner.ts +38 -16
  55. package/dist/builtin/workflows/builtin/review-convergence.ts +111 -0
  56. package/dist/builtin/workflows/package.json +1 -1
  57. package/dist/builtin/workflows/skills/create-spec/SKILL.md +2 -0
  58. package/dist/builtin/workflows/skills/impeccable/SKILL.md +16 -13
  59. package/dist/builtin/workflows/skills/impeccable/reference/adapt.md +1 -0
  60. package/dist/builtin/workflows/skills/impeccable/reference/adapt.native.md +58 -0
  61. package/dist/builtin/workflows/skills/impeccable/reference/android.md +40 -0
  62. package/dist/builtin/workflows/skills/impeccable/reference/animate.md +2 -0
  63. package/dist/builtin/workflows/skills/impeccable/reference/audit.md +2 -0
  64. package/dist/builtin/workflows/skills/impeccable/reference/audit.native.md +139 -0
  65. package/dist/builtin/workflows/skills/impeccable/reference/bolder.md +69 -62
  66. package/dist/builtin/workflows/skills/impeccable/reference/critique.md +21 -7
  67. package/dist/builtin/workflows/skills/impeccable/reference/document.md +1 -1
  68. package/dist/builtin/workflows/skills/impeccable/reference/hooks.md +92 -0
  69. package/dist/builtin/workflows/skills/impeccable/reference/init.md +29 -7
  70. package/dist/builtin/workflows/skills/impeccable/reference/ios.md +45 -0
  71. package/dist/builtin/workflows/skills/impeccable/reference/layout.md +25 -1
  72. package/dist/builtin/workflows/skills/impeccable/reference/typeset.md +23 -1
  73. package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +2 -1
  74. package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +75 -14
  75. package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +1 -1
  76. package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +31 -0
  77. package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +64 -0
  78. package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +118 -11
  79. package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +16 -11
  80. package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +66 -0
  81. package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +42 -10
  82. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  83. package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +4 -6
  84. package/dist/builtin/workflows/skills/impeccable/scripts/hook-admin.mjs +660 -0
  85. package/dist/builtin/workflows/skills/impeccable/scripts/hook-before-edit.mjs +516 -0
  86. package/dist/builtin/workflows/skills/impeccable/scripts/hook-lib.mjs +1764 -0
  87. package/dist/builtin/workflows/skills/impeccable/scripts/hook.mjs +61 -0
  88. package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +4 -2
  89. package/dist/builtin/workflows/skills/impeccable/scripts/lib/is-generated.mjs +2 -2
  90. package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +13 -16
  91. package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +17 -11
  92. package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +104 -37
  93. package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +9 -0
  94. package/dist/builtin/workflows/src/durable/backend.ts +12 -0
  95. package/dist/builtin/workflows/src/durable/child-primitive.ts +12 -8
  96. package/dist/builtin/workflows/src/durable/dbos-backend.ts +63 -2
  97. package/dist/builtin/workflows/src/durable/dbos-envelope.ts +28 -0
  98. package/dist/builtin/workflows/src/durable/resume-catalog.ts +12 -0
  99. package/dist/builtin/workflows/src/durable/resume-runtime.ts +5 -2
  100. package/dist/builtin/workflows/src/durable/stage-primitive.ts +165 -27
  101. package/dist/builtin/workflows/src/durable/types.ts +30 -1
  102. package/dist/builtin/workflows/src/engine/primitives/task.ts +1 -0
  103. package/dist/builtin/workflows/src/engine/run-durable-finalize.ts +1 -2
  104. package/dist/builtin/workflows/src/engine/run-returned-status.ts +50 -16
  105. package/dist/builtin/workflows/src/engine/run.ts +32 -32
  106. package/dist/builtin/workflows/src/engine/runtime.ts +5 -0
  107. package/dist/builtin/workflows/src/extension/extension-factory.ts +3 -0
  108. package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +9 -2
  109. package/dist/builtin/workflows/src/extension/extension-runtime-state.ts +137 -27
  110. package/dist/builtin/workflows/src/extension/lifecycle-notifications.ts +22 -6
  111. package/dist/builtin/workflows/src/extension/public-types.ts +1 -1
  112. package/dist/builtin/workflows/src/extension/workflow-command-completions.ts +13 -1
  113. package/dist/builtin/workflows/src/extension/workflow-command-registration.ts +19 -4
  114. package/dist/builtin/workflows/src/extension/workflow-command-surfaces.ts +9 -0
  115. package/dist/builtin/workflows/src/extension/workflow-prompts.ts +2 -0
  116. package/dist/builtin/workflows/src/extension/workflow-run-control-command.ts +48 -23
  117. package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +20 -8
  118. package/dist/builtin/workflows/src/extension/workflow-tool.ts +24 -8
  119. package/dist/builtin/workflows/src/runs/background/status.ts +4 -3
  120. package/dist/builtin/workflows/src/runs/foreground/executor-continuation.ts +13 -3
  121. package/dist/builtin/workflows/src/runs/foreground/executor-direct-helpers.ts +29 -8
  122. package/dist/builtin/workflows/src/runs/foreground/executor-hil.ts +2 -1
  123. package/dist/builtin/workflows/src/runs/foreground/executor-stage-factory.ts +4 -3
  124. package/dist/builtin/workflows/src/runs/foreground/stage-runner-controller.ts +18 -19
  125. package/dist/builtin/workflows/src/runs/foreground/stage-runner-unresolved-overflow.ts +38 -0
  126. package/dist/builtin/workflows/src/runs/shared/worktree-git.ts +155 -20
  127. package/dist/builtin/workflows/src/runs/shared/worktree-types.ts +6 -0
  128. package/dist/builtin/workflows/src/runs/shared/worktree.ts +2 -1
  129. package/dist/builtin/workflows/src/shared/persistence-restore-helpers.ts +18 -1
  130. package/dist/builtin/workflows/src/shared/persistence-restore.ts +23 -5
  131. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +4 -0
  132. package/dist/builtin/workflows/src/shared/returned-run-status.ts +116 -0
  133. package/dist/builtin/workflows/src/shared/workflow-failures-decisions.ts +2 -0
  134. package/dist/builtin/workflows/src/tui/graph-view-render-helpers.ts +21 -3
  135. package/dist/builtin/workflows/src/tui/graph-view-state.ts +3 -0
  136. package/dist/builtin/workflows/src/tui/graph-view-types.ts +3 -0
  137. package/dist/builtin/workflows/src/tui/status-list.ts +22 -15
  138. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +4 -4
  139. package/dist/bun/internal-intercom-broker.d.ts +6 -0
  140. package/dist/bun/internal-intercom-broker.d.ts.map +1 -0
  141. package/dist/bun/internal-intercom-broker.js +39 -0
  142. package/dist/bun/internal-intercom-broker.js.map +1 -0
  143. package/dist/bun/split-loader.d.ts.map +1 -1
  144. package/dist/bun/split-loader.js +21 -8
  145. package/dist/bun/split-loader.js.map +1 -1
  146. package/dist/core/agent-session-auto-compaction.d.ts +21 -2
  147. package/dist/core/agent-session-auto-compaction.d.ts.map +1 -1
  148. package/dist/core/agent-session-auto-compaction.js +163 -11
  149. package/dist/core/agent-session-auto-compaction.js.map +1 -1
  150. package/dist/core/agent-session-events.d.ts.map +1 -1
  151. package/dist/core/agent-session-events.js +10 -1
  152. package/dist/core/agent-session-events.js.map +1 -1
  153. package/dist/core/agent-session-methods.d.ts +12 -3
  154. package/dist/core/agent-session-methods.d.ts.map +1 -1
  155. package/dist/core/agent-session-methods.js.map +1 -1
  156. package/dist/core/agent-session-models.d.ts +2 -2
  157. package/dist/core/agent-session-models.d.ts.map +1 -1
  158. package/dist/core/agent-session-models.js.map +1 -1
  159. package/dist/core/agent-session-prompt.d.ts.map +1 -1
  160. package/dist/core/agent-session-prompt.js +1 -0
  161. package/dist/core/agent-session-prompt.js.map +1 -1
  162. package/dist/core/agent-session-retry.d.ts +2 -0
  163. package/dist/core/agent-session-retry.d.ts.map +1 -1
  164. package/dist/core/agent-session-retry.js +152 -4
  165. package/dist/core/agent-session-retry.js.map +1 -1
  166. package/dist/core/agent-session-services.d.ts +1 -0
  167. package/dist/core/agent-session-services.d.ts.map +1 -1
  168. package/dist/core/agent-session-services.js +1 -0
  169. package/dist/core/agent-session-services.js.map +1 -1
  170. package/dist/core/agent-session-types.d.ts +15 -1
  171. package/dist/core/agent-session-types.d.ts.map +1 -1
  172. package/dist/core/agent-session-types.js.map +1 -1
  173. package/dist/core/agent-session.d.ts +6 -0
  174. package/dist/core/agent-session.d.ts.map +1 -1
  175. package/dist/core/agent-session.js +6 -0
  176. package/dist/core/agent-session.js.map +1 -1
  177. package/dist/core/compaction/context-compaction-runner.d.ts.map +1 -1
  178. package/dist/core/compaction/context-compaction-runner.js +36 -20
  179. package/dist/core/compaction/context-compaction-runner.js.map +1 -1
  180. package/dist/core/copilot-anthropic-sse-repair.d.ts +23 -0
  181. package/dist/core/copilot-anthropic-sse-repair.d.ts.map +1 -0
  182. package/dist/core/copilot-anthropic-sse-repair.js +340 -0
  183. package/dist/core/copilot-anthropic-sse-repair.js.map +1 -0
  184. package/dist/core/copilot-gemini-reasoning.d.ts +16 -8
  185. package/dist/core/copilot-gemini-reasoning.d.ts.map +1 -1
  186. package/dist/core/copilot-gemini-reasoning.js +27 -22
  187. package/dist/core/copilot-gemini-reasoning.js.map +1 -1
  188. package/dist/core/copilot-hosts.d.ts +12 -0
  189. package/dist/core/copilot-hosts.d.ts.map +1 -0
  190. package/dist/core/copilot-hosts.js +33 -0
  191. package/dist/core/copilot-hosts.js.map +1 -0
  192. package/dist/core/extensions/agent-events.d.ts +1 -1
  193. package/dist/core/extensions/agent-events.d.ts.map +1 -1
  194. package/dist/core/extensions/agent-events.js.map +1 -1
  195. package/dist/core/extensions/loader-virtual-modules.d.ts +8 -1
  196. package/dist/core/extensions/loader-virtual-modules.d.ts.map +1 -1
  197. package/dist/core/extensions/loader-virtual-modules.js +2 -1
  198. package/dist/core/extensions/loader-virtual-modules.js.map +1 -1
  199. package/dist/core/footer-data-provider.d.ts +6 -0
  200. package/dist/core/footer-data-provider.d.ts.map +1 -1
  201. package/dist/core/footer-data-provider.js +72 -29
  202. package/dist/core/footer-data-provider.js.map +1 -1
  203. package/dist/core/http-dispatcher.d.ts.map +1 -1
  204. package/dist/core/http-dispatcher.js +5 -5
  205. package/dist/core/http-dispatcher.js.map +1 -1
  206. package/dist/core/openai-responses-payload-sanitizer.d.ts +1 -0
  207. package/dist/core/openai-responses-payload-sanitizer.d.ts.map +1 -1
  208. package/dist/core/openai-responses-payload-sanitizer.js +24 -9
  209. package/dist/core/openai-responses-payload-sanitizer.js.map +1 -1
  210. package/dist/core/sdk-types.d.ts +2 -0
  211. package/dist/core/sdk-types.d.ts.map +1 -1
  212. package/dist/core/sdk-types.js.map +1 -1
  213. package/dist/core/sdk.d.ts.map +1 -1
  214. package/dist/core/sdk.js +1 -0
  215. package/dist/core/sdk.js.map +1 -1
  216. package/dist/core/settings-manager-basic-accessors.d.ts +1 -0
  217. package/dist/core/settings-manager-basic-accessors.d.ts.map +1 -1
  218. package/dist/core/settings-manager-basic-accessors.js +6 -0
  219. package/dist/core/settings-manager-basic-accessors.js.map +1 -1
  220. package/dist/core/settings-types.d.ts +1 -0
  221. package/dist/core/settings-types.d.ts.map +1 -1
  222. package/dist/core/settings-types.js.map +1 -1
  223. package/dist/core/slash-commands.d.ts +2 -0
  224. package/dist/core/slash-commands.d.ts.map +1 -1
  225. package/dist/core/slash-commands.js +141 -0
  226. package/dist/core/slash-commands.js.map +1 -1
  227. package/dist/index-extensions.d.ts +1 -1
  228. package/dist/index-extensions.d.ts.map +1 -1
  229. package/dist/index-extensions.js.map +1 -1
  230. package/dist/index.d.ts +1 -0
  231. package/dist/index.d.ts.map +1 -1
  232. package/dist/index.js +1 -0
  233. package/dist/index.js.map +1 -1
  234. package/dist/main-deferred-startup.d.ts +12 -0
  235. package/dist/main-deferred-startup.d.ts.map +1 -1
  236. package/dist/main-deferred-startup.js +23 -2
  237. package/dist/main-deferred-startup.js.map +1 -1
  238. package/dist/main-early-input.d.ts +41 -0
  239. package/dist/main-early-input.d.ts.map +1 -0
  240. package/dist/main-early-input.js +154 -0
  241. package/dist/main-early-input.js.map +1 -0
  242. package/dist/main.d.ts +0 -6
  243. package/dist/main.d.ts.map +1 -1
  244. package/dist/main.js +20 -14
  245. package/dist/main.js.map +1 -1
  246. package/dist/modes/interactive/components/chat-session-host-actions.d.ts.map +1 -1
  247. package/dist/modes/interactive/components/chat-session-host-actions.js +0 -1
  248. package/dist/modes/interactive/components/chat-session-host-actions.js.map +1 -1
  249. package/dist/modes/interactive/components/chat-session-host-events.d.ts.map +1 -1
  250. package/dist/modes/interactive/components/chat-session-host-events.js +15 -0
  251. package/dist/modes/interactive/components/chat-session-host-events.js.map +1 -1
  252. package/dist/modes/interactive/interactive-agent-events.d.ts.map +1 -1
  253. package/dist/modes/interactive/interactive-agent-events.js +25 -0
  254. package/dist/modes/interactive/interactive-agent-events.js.map +1 -1
  255. package/dist/modes/interactive/interactive-autocomplete.d.ts.map +1 -1
  256. package/dist/modes/interactive/interactive-autocomplete.js +21 -9
  257. package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
  258. package/dist/modes/interactive/interactive-deferred-startup.d.ts.map +1 -1
  259. package/dist/modes/interactive/interactive-deferred-startup.js +41 -28
  260. package/dist/modes/interactive/interactive-deferred-startup.js.map +1 -1
  261. package/dist/modes/interactive/interactive-hotkeys-debug.d.ts.map +1 -1
  262. package/dist/modes/interactive/interactive-hotkeys-debug.js +1 -0
  263. package/dist/modes/interactive/interactive-hotkeys-debug.js.map +1 -1
  264. package/dist/modes/interactive/interactive-input-handling.d.ts.map +1 -1
  265. package/dist/modes/interactive/interactive-input-handling.js +318 -185
  266. package/dist/modes/interactive/interactive-input-handling.js.map +1 -1
  267. package/dist/modes/interactive/interactive-mode-base.d.ts +12 -0
  268. package/dist/modes/interactive/interactive-mode-base.d.ts.map +1 -1
  269. package/dist/modes/interactive/interactive-mode-base.js +39 -0
  270. package/dist/modes/interactive/interactive-mode-base.js.map +1 -1
  271. package/dist/modes/interactive/interactive-mode-deps.d.ts +1 -1
  272. package/dist/modes/interactive/interactive-mode-deps.d.ts.map +1 -1
  273. package/dist/modes/interactive/interactive-mode-deps.js +1 -1
  274. package/dist/modes/interactive/interactive-mode-deps.js.map +1 -1
  275. package/dist/modes/interactive/interactive-mode-surface.d.ts +6 -2
  276. package/dist/modes/interactive/interactive-mode-surface.d.ts.map +1 -1
  277. package/dist/modes/interactive/interactive-mode-surface.js.map +1 -1
  278. package/dist/modes/interactive/interactive-mode-types.d.ts +3 -0
  279. package/dist/modes/interactive/interactive-mode-types.d.ts.map +1 -1
  280. package/dist/modes/interactive/interactive-mode-types.js.map +1 -1
  281. package/dist/modes/interactive/interactive-model-routing.d.ts.map +1 -1
  282. package/dist/modes/interactive/interactive-model-routing.js +3 -1
  283. package/dist/modes/interactive/interactive-model-routing.js.map +1 -1
  284. package/dist/modes/interactive/interactive-process-lifecycle.d.ts.map +1 -1
  285. package/dist/modes/interactive/interactive-process-lifecycle.js +0 -3
  286. package/dist/modes/interactive/interactive-process-lifecycle.js.map +1 -1
  287. package/dist/modes/interactive/interactive-render-chat.d.ts.map +1 -1
  288. package/dist/modes/interactive/interactive-render-chat.js +36 -10
  289. package/dist/modes/interactive/interactive-render-chat.js.map +1 -1
  290. package/dist/modes/interactive/interactive-session-routing.d.ts.map +1 -1
  291. package/dist/modes/interactive/interactive-session-routing.js +5 -1
  292. package/dist/modes/interactive/interactive-session-routing.js.map +1 -1
  293. package/dist/modes/interactive/interactive-startup.d.ts.map +1 -1
  294. package/dist/modes/interactive/interactive-startup.js +26 -22
  295. package/dist/modes/interactive/interactive-startup.js.map +1 -1
  296. package/dist/modes/interactive/theme/global-theme.d.ts.map +1 -1
  297. package/dist/modes/interactive/theme/global-theme.js +26 -2
  298. package/dist/modes/interactive/theme/global-theme.js.map +1 -1
  299. package/dist/utils/fs-watch.d.ts +24 -1
  300. package/dist/utils/fs-watch.d.ts.map +1 -1
  301. package/dist/utils/fs-watch.js +60 -5
  302. package/dist/utils/fs-watch.js.map +1 -1
  303. package/docs/compaction.md +8 -0
  304. package/docs/development.md +4 -0
  305. package/docs/extensions.md +5 -0
  306. package/docs/json.md +3 -1
  307. package/docs/providers.md +1 -0
  308. package/docs/quickstart.md +1 -1
  309. package/docs/rpc.md +3 -1
  310. package/docs/settings.md +27 -1
  311. package/docs/subagents.md +4 -0
  312. package/docs/terminal-setup.md +4 -0
  313. package/docs/usage.md +2 -2
  314. package/docs/windows.md +4 -0
  315. package/docs/workflows.md +21 -9
  316. package/npm-shrinkwrap.json +513 -23
  317. package/package.json +3 -2
@@ -14,15 +14,13 @@ import {
14
14
  widgetStatusGlyph,
15
15
  widgetStepActivity,
16
16
  widgetStepActivityLine,
17
- widgetStepGlyph,
18
- widgetStepRunningSeed,
17
+ widgetStepPulseGlyph,
19
18
  widgetStepStats,
20
19
  widgetStepStatus,
21
- widgetStepsRunningSeed,
22
20
  widgetJobName,
23
21
  } from "./render-event-formatting.ts";
24
22
 
25
- export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
23
+ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), pulseFrame?: number): string[] {
26
24
  if (!job.steps?.length) return [];
27
25
  const total = job.chainStepCount ?? job.steps.length;
28
26
  const lines: string[] = [];
@@ -30,7 +28,7 @@ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded =
30
28
  const steps = job.steps.slice(span.start, span.start + span.count);
31
29
  if (span.isParallel) {
32
30
  const status = aggregateStepStatus(steps);
33
- lines.push(` ${widgetStepGlyph(status, theme, widgetStepsRunningSeed(steps), now)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
31
+ lines.push(` ${widgetStepPulseGlyph(status, theme, pulseFrame)} Step ${span.stepIndex + 1}/${total}: ${themeBold(theme, "parallel group")} ${theme.fg("dim", "·")} ${theme.fg("dim", formatParallelOutcome(steps, span.count))}`);
34
32
  continue;
35
33
  }
36
34
  const step = steps[0];
@@ -38,15 +36,15 @@ export function widgetChainDetails(job: AsyncJobState, theme: Theme, expanded =
38
36
  lines.push(` ${theme.fg("dim", `◦ Step ${span.stepIndex + 1}/${total}: pending`)}`);
39
37
  continue;
40
38
  }
41
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, now));
39
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, "Step", span.stepIndex + 1, total, expanded, width, pulseFrame));
42
40
  }
43
41
  return lines;
44
42
  }
45
43
 
46
- export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), now?: number): string[] {
44
+ export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, expanded = false, width = getTermWidth(), _now?: number, pulseFrame?: number): string[] {
47
45
  if (!job.steps?.length) return [];
48
46
  if (job.mode !== "parallel" && job.mode !== "chain") return [];
49
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
47
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, pulseFrame);
50
48
  const total = job.stepsTotal ?? job.steps.length;
51
49
  const lines: string[] = [];
52
50
  for (const [index, step] of job.steps.entries()) {
@@ -54,8 +52,8 @@ export function widgetParallelAgentDetails(job: AsyncJobState, theme: Theme, exp
54
52
  const activity = widgetStepActivity(step, job.updatedAt);
55
53
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
56
54
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
57
- lines.push(` ${theme.fg("dim", `${marker} ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
58
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, now)) lines.push(` ${nestedLine}`);
55
+ lines.push(` ${theme.fg("dim", `${marker} ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index + 1}/${total}: ${step.agent} · ${widgetStepStatus(step.status, theme)}${modelDisplay}${activity ? ` · ${activity}` : ""}`)}`);
56
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, expanded ? 8 : 1, pulseFrame)) lines.push(` ${nestedLine}`);
59
57
  }
60
58
  return lines;
61
59
  }
@@ -69,15 +67,15 @@ export function foregroundStyleWidgetStepLines(
69
67
  total: number,
70
68
  expanded: boolean,
71
69
  width: number,
72
- now?: number,
70
+ pulseFrame?: number,
73
71
  ): string[] {
74
72
  const status = widgetStepStatus(step.status, theme);
75
73
  const stats = widgetStepStats(theme, step);
76
74
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
77
- const lines = [` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index - 1), now)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
75
+ const lines = [` ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`];
78
76
  const activity = widgetStepActivityLine(step, width, expanded, job.updatedAt);
79
77
  if (activity) lines.push(` ${theme.fg("dim", `⎿ ${activity}`)}`);
80
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, now)) {
78
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, expanded, job.updatedAt, undefined, pulseFrame)) {
81
79
  lines.push(` ${nestedLine}`);
82
80
  }
83
81
  if (step.status === "running") {
@@ -100,40 +98,40 @@ export function foregroundStyleWidgetStepLines(
100
98
  return lines;
101
99
  }
102
100
 
103
- export function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, now?: number): string[] {
101
+ export function foregroundStyleWidgetDetails(job: AsyncJobState, theme: Theme, expanded: boolean, width: number, pulseFrame?: number): string[] {
104
102
  if (!job.steps?.length) return [
105
103
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
106
- ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, now).map((line) => ` ${line}`),
104
+ ...formatNestedWidgetLines(job.nestedChildren, theme, width, expanded, job.updatedAt, undefined, pulseFrame).map((line) => ` ${line}`),
107
105
  ];
108
- if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, now);
106
+ if (job.mode === "chain" && !job.activeParallelGroup && job.parallelGroups?.length) return widgetChainDetails(job, theme, expanded, width, pulseFrame);
109
107
  const total = job.stepsTotal ?? job.steps.length;
110
108
  const itemTitle = job.mode === "parallel" || job.activeParallelGroup ? "Agent" : "Step";
111
109
  const lines: string[] = [];
112
110
  for (const [index, step] of job.steps.entries()) {
113
- lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, now));
111
+ lines.push(...foregroundStyleWidgetStepLines(job, theme, step, itemTitle, index + 1, total, expanded, width, pulseFrame));
114
112
  }
115
113
  const attached = new Set(job.steps.flatMap((step) => step.children?.map((child) => child.id) ?? []));
116
114
  const unattached = job.nestedChildren?.filter((child) => !attached.has(child.id)) ?? [];
117
- for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, now)) {
115
+ for (const nestedLine of formatNestedWidgetLines(unattached, theme, width, expanded, job.updatedAt, undefined, pulseFrame)) {
118
116
  lines.push(` ${nestedLine}`);
119
117
  }
120
118
  return lines;
121
119
  }
122
120
 
123
- export function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, now?: number): string[] {
121
+ export function buildSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, expanded: boolean, _now?: number, pulseFrame?: number): string[] {
124
122
  const stats = widgetStats(job, theme);
125
123
  const count = job.mode === "chain" ? job.chainStepCount : job.stepsTotal ?? job.agents?.length ?? job.steps?.length;
126
124
  const mode = widgetJobName(job);
127
125
  const title = `async subagent ${mode}${count && count > 1 ? ` (${count})` : ""}`;
128
126
  return [
129
127
  `${theme.fg("toolTitle", themeBold(theme, title))} ${theme.fg("dim", "· background")}`,
130
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
131
- ...foregroundStyleWidgetDetails(job, theme, expanded, width, now),
128
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, mode)}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
129
+ ...foregroundStyleWidgetDetails(job, theme, expanded, width, pulseFrame),
132
130
  ].map((line) => truncLine(line, width));
133
131
  }
134
132
 
135
- export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, now?: number): string[] {
136
- const fullLines = buildSingleWidgetLines(job, theme, width, false, now);
133
+ export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width: number, _now?: number, pulseFrame?: number): string[] {
134
+ const fullLines = buildSingleWidgetLines(job, theme, width, false, _now, pulseFrame);
137
135
  if (fullLines.length <= 10 || !job.steps?.length || (job.mode !== "parallel" && !job.activeParallelGroup)) return fullLines;
138
136
 
139
137
  const total = job.stepsTotal ?? job.steps.length;
@@ -145,8 +143,12 @@ export function compactSingleWidgetLines(job: AsyncJobState, theme: Theme, width
145
143
  const stepStats = widgetStepStats(theme, step);
146
144
  const activitySuffix = activity ? ` ${theme.fg("dim", "·")} ${theme.fg("dim", activity)}` : "";
147
145
  const modelDisplay = modelThinkingBadge(theme, step.model, step.thinking, step.fastMode);
148
- lines.push(` ${widgetStepGlyph(step.status, theme, widgetStepRunningSeed(step, index), now)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
149
- for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, now)) lines.push(` ${nestedLine}`);
146
+ lines.push(` ${widgetStepPulseGlyph(step.status, theme, pulseFrame)} ${itemTitle} ${index + 1}/${total}: ${themeBold(theme, step.agent)} ${theme.fg("dim", "·")} ${status}${modelDisplay}${activitySuffix}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}`);
147
+ for (const nestedLine of formatNestedWidgetLines(step.children, theme, width, false, job.updatedAt, undefined, pulseFrame)) lines.push(` ${nestedLine}`);
148
+ if (step.status === "running") {
149
+ const output = widgetOutputPath(job, step);
150
+ if (output) lines.push(` ${theme.fg("dim", `output: ${shortenPath(output)}`)}`);
151
+ }
150
152
  }
151
153
  if (job.steps.some((step) => step.status === "running")) lines.push(theme.fg("accent", ` Press ${keyHint("app.tools.expand", "for live detail")}`));
152
154
  return lines.map((line) => truncLine(line, width));
@@ -2,8 +2,10 @@ import type { ExtensionContext } from "@bastani/atomic";
2
2
  import { Container, Text, type Component } from "@earendil-works/pi-tui";
3
3
  import * as path from "node:path";
4
4
  import { MAX_WIDGET_JOBS, WIDGET_KEY, type AsyncJobState } from "../shared/types.ts";
5
- import { getTermWidth, RUNNING_ANIMATION_MS, runningGlyph, truncLine, type Theme } from "./render-layout.ts";
5
+ import { getTermWidth, runningPulseGlyph, truncLine, type Theme } from "./render-layout.ts";
6
6
  import { themeBold } from "./render-status-progress.ts";
7
+ import { advanceResultPulseFrame } from "./render-result-animation.ts";
8
+ import { widgetRenderKey } from "./render-stable-output.ts";
7
9
  import {
8
10
  buildSingleWidgetLines,
9
11
  compactSingleWidgetLines,
@@ -13,7 +15,6 @@ import {
13
15
  import {
14
16
  widgetActivity,
15
17
  widgetJobName,
16
- widgetJobsRunningSeed,
17
18
  widgetStats,
18
19
  widgetStatusGlyph,
19
20
  } from "./render-event-formatting.ts";
@@ -26,22 +27,24 @@ class LiveWidgetComponent implements Component {
26
27
  private readonly theme: Theme,
27
28
  private readonly getExpanded: () => boolean,
28
29
  private readonly getNow: () => number,
30
+ private readonly getPulseFrame: () => number | undefined,
29
31
  ) {}
30
32
 
31
33
  render(width: number): string[] {
32
34
  const jobs = this.getJobs();
33
35
  const expanded = this.getExpanded();
34
36
  const now = this.getNow();
35
- const lines = this.buildLines(jobs, width, expanded, now);
37
+ const pulseFrame = this.getPulseFrame();
38
+ const lines = this.buildLines(jobs, width, expanded, now, pulseFrame);
36
39
  this.container.clear();
37
40
  for (const line of fitWidgetLineBudget(lines, this.theme, width, expanded)) this.container.addChild(new Text(line, 1, 0));
38
41
  return this.container.render(width);
39
42
  }
40
43
 
41
- private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number): string[] {
42
- if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now);
43
- if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now);
44
- return buildWidgetLines(jobs, this.theme, width, false, now);
44
+ private buildLines(jobs: AsyncJobState[], width: number, expanded: boolean, now: number, pulseFrame: number | undefined): string[] {
45
+ if (expanded) return buildWidgetLines(jobs, this.theme, width, true, now, pulseFrame);
46
+ if (jobs.length === 1) return compactSingleWidgetLines(jobs[0]!, this.theme, width, now, pulseFrame);
47
+ return buildWidgetLines(jobs, this.theme, width, false, now, pulseFrame);
45
48
  }
46
49
 
47
50
  invalidate(): void {
@@ -49,22 +52,23 @@ class LiveWidgetComponent implements Component {
49
52
  }
50
53
  }
51
54
 
52
- function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number): (_tui: unknown, theme: Theme) => Component {
53
- return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow);
55
+ function buildWidgetComponent(getJobs: () => AsyncJobState[], getExpanded: () => boolean, getNow: () => number, getPulseFrame: () => number | undefined): (_tui: unknown, theme: Theme) => Component {
56
+ return (_tui, theme) => new LiveWidgetComponent(getJobs, theme, getExpanded, getNow, getPulseFrame);
54
57
  }
55
58
 
56
59
  interface RenderRequestingContext {
57
60
  ui: ExtensionContext["ui"] & { requestRender?: () => void };
58
61
  }
59
62
 
60
- // There is only ever one async-agents widget per host process, so the widget
61
- // ticker and mounted component read their driving context/jobs from module-level
63
+ // There is only ever one async-agents widget per host process, so the mounted
64
+ // component reads its driving context/jobs/pulse frame from module-level
62
65
  // singletons instead of remounting the widget for every visible update.
63
66
  let latestWidgetCtx: ExtensionContext | undefined;
64
67
  let latestWidgetJobs: AsyncJobState[] = [];
65
68
  let latestWidgetFrameNow = 0;
66
69
  let latestWidgetExpanded = false;
67
- let widgetTimer: ReturnType<typeof setInterval> | undefined;
70
+ let latestWidgetSnapshotKey: string | undefined;
71
+ let latestWidgetPulseFrame: number | undefined;
68
72
  let mountedWidgetCtx: ExtensionContext | undefined;
69
73
  let mountedWidgetOwnerKey: string | undefined;
70
74
  let widgetMounted = false;
@@ -77,6 +81,10 @@ function getLatestWidgetFrameNow(): number {
77
81
  return latestWidgetFrameNow;
78
82
  }
79
83
 
84
+ function getLatestWidgetPulseFrame(): number | undefined {
85
+ return latestWidgetPulseFrame;
86
+ }
87
+
80
88
  function getLatestWidgetExpanded(): boolean {
81
89
  // LiveWidgetComponent re-renders outside a specific renderWidget() call, so
82
90
  // remember the last expansion state observed from a live host UI. Workflow
@@ -94,6 +102,8 @@ function clearLatestWidgetState(): void {
94
102
  latestWidgetJobs = [];
95
103
  latestWidgetFrameNow = 0;
96
104
  latestWidgetExpanded = false;
105
+ latestWidgetSnapshotKey = undefined;
106
+ latestWidgetPulseFrame = undefined;
97
107
  mountedWidgetCtx = undefined;
98
108
  mountedWidgetOwnerKey = undefined;
99
109
  widgetMounted = false;
@@ -127,6 +137,28 @@ function getWidgetOwnerKey(ctx: ExtensionContext): string {
127
137
  return `${sessionOwner}|cwd:${cwdOwner}`;
128
138
  }
129
139
 
140
+ function widgetStatusText(jobs: AsyncJobState[]): string | undefined {
141
+ if (jobs.length === 0) return undefined;
142
+ const counts = new Map<string, number>();
143
+ for (const job of jobs) counts.set(job.status, (counts.get(job.status) ?? 0) + 1);
144
+ const ordered = ["running", "queued", "complete", "failed", "paused"];
145
+ const parts = ordered
146
+ .map((status) => {
147
+ const count = counts.get(status) ?? 0;
148
+ return count > 0 ? `${count} ${status}` : undefined;
149
+ })
150
+ .filter((part): part is string => part !== undefined);
151
+ return `Async agents: ${parts.join(", ")}`;
152
+ }
153
+
154
+ function setWidgetStatus(ctx: ExtensionContext, jobs: AsyncJobState[]): void {
155
+ try {
156
+ ctx.ui.setStatus?.(WIDGET_KEY, widgetStatusText(jobs));
157
+ } catch {
158
+ // Status mirroring must never prevent the primary widget render path.
159
+ }
160
+ }
161
+
130
162
  function requestWidgetRender(ctx: ExtensionContext): void {
131
163
  (ctx as RenderRequestingContext).ui.requestRender?.();
132
164
  }
@@ -140,65 +172,42 @@ function unmountWidgetBestEffort(ctx: ExtensionContext | undefined): void {
140
172
  // reload/session rebinding, but local state still needs to move on so the
141
173
  // next status update can mount cleanly on the active UI context.
142
174
  }
143
- }
144
-
145
- function hasAnimatedWidgetJobs(jobs: AsyncJobState[]): boolean {
146
- // Animate while any job — or any of its nested steps — is still running so the
147
- // header/step spinners never freeze before the work actually settles.
148
- return jobs.some((job) => job.status === "running" || job.steps?.some((step) => step.status === "running"));
149
- }
150
-
151
- function refreshAnimatedWidget(): void {
152
- if (!latestWidgetCtx?.hasUI) return;
153
175
  try {
154
- latestWidgetFrameNow = Date.now();
155
- requestWidgetRender(latestWidgetCtx);
176
+ ctx.ui.setStatus?.(WIDGET_KEY, undefined);
156
177
  } catch {
157
- // A stale render context means the cosmetic ticker can no longer update the
158
- // mounted widget safely; tear it down best-effort and let the next status
159
- // update remount on the active host context.
160
- stopWidgetAnimation();
178
+ // Status mirroring cleanup must never block primary widget teardown.
161
179
  }
162
180
  }
163
181
 
164
- function ensureWidgetAnimation(): void {
165
- if (widgetTimer) return;
166
- widgetTimer = setInterval(() => {
167
- if (!hasAnimatedWidgetJobs(latestWidgetJobs)) {
168
- stopWidgetAnimation();
169
- return;
170
- }
171
- refreshAnimatedWidget();
172
- }, RUNNING_ANIMATION_MS);
173
- widgetTimer.unref?.();
182
+ function widgetSnapshotKey(jobs: AsyncJobState[]): string {
183
+ return jobs.map(widgetRenderKey).join("\n");
174
184
  }
175
185
 
176
- // Stop only the ticker, keeping the last-rendered widget context/jobs intact.
177
- function stopWidgetTicker(): void {
178
- if (widgetTimer) {
179
- clearInterval(widgetTimer);
180
- widgetTimer = undefined;
181
- }
186
+ function refreshWidgetSnapshot(jobs: AsyncJobState[]): void {
187
+ const snapshotKey = widgetSnapshotKey(jobs);
188
+ if (snapshotKey === latestWidgetSnapshotKey) return;
189
+ latestWidgetSnapshotKey = snapshotKey;
190
+ latestWidgetFrameNow = Date.now();
191
+ latestWidgetPulseFrame = advanceResultPulseFrame(latestWidgetPulseFrame);
182
192
  }
183
193
 
184
- // Full teardown: stop the ticker, clear the mounted widget if possible, and
185
- // forget the driving context/jobs entirely.
194
+ // Full teardown: clear the mounted widget if possible, and forget the driving
195
+ // context/jobs entirely.
186
196
  export function stopWidgetAnimation(): void {
187
- stopWidgetTicker();
188
197
  if (widgetMounted) unmountWidgetBestEffort(mountedWidgetCtx);
189
198
  clearLatestWidgetState();
190
199
  }
191
200
 
192
- export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now()): string[] {
201
+ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = getTermWidth(), expanded = false, now: number = Date.now(), pulseFrame?: number): string[] {
193
202
  if (jobs.length === 0) return [];
194
- if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now);
203
+ if (jobs.length === 1) return buildSingleWidgetLines(jobs[0]!, theme, width, expanded, now, pulseFrame);
195
204
  const running = jobs.filter((job) => job.status === "running");
196
205
  const queued = jobs.filter((job) => job.status === "queued");
197
206
  const finished = jobs.filter((job) => job.status !== "running" && job.status !== "queued");
198
207
 
199
208
  const lines: string[] = [];
200
209
  const hasActive = running.length > 0 || queued.length > 0;
201
- const headerGlyph = running.length > 0 ? runningGlyph(widgetJobsRunningSeed(running), now) : hasActive ? "●" : "○";
210
+ const headerGlyph = running.length > 0 ? runningPulseGlyph(pulseFrame) : hasActive ? "●" : "○";
202
211
  lines.push(truncLine(`${theme.fg(hasActive ? "accent" : "dim", headerGlyph)} ${theme.fg(hasActive ? "accent" : "dim", "Async agents")} ${theme.fg("dim", "· background")}`, width));
203
212
 
204
213
  const items: string[][] = [];
@@ -211,9 +220,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
211
220
  if (slots <= 0) { hiddenRunning++; continue; }
212
221
  const stats = widgetStats(job, theme);
213
222
  items.push([
214
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
223
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
215
224
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
216
- ...widgetParallelAgentDetails(job, theme, expanded, width, now),
225
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now, pulseFrame),
217
226
  ]);
218
227
  slots--;
219
228
  }
@@ -228,9 +237,9 @@ export function buildWidgetLines(jobs: AsyncJobState[], theme: Theme, width = ge
228
237
  if (slots <= 0) { hiddenFinished++; continue; }
229
238
  const stats = widgetStats(job, theme);
230
239
  items.push([
231
- `${widgetStatusGlyph(job, theme, now)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
240
+ `${widgetStatusGlyph(job, theme, pulseFrame)} ${themeBold(theme, widgetJobName(job))}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`,
232
241
  ` ${theme.fg("dim", `⎿ ${widgetActivity(job)}`)}`,
233
- ...widgetParallelAgentDetails(job, theme, expanded, width, now),
242
+ ...widgetParallelAgentDetails(job, theme, expanded, width, now, pulseFrame),
234
243
  ]);
235
244
  slots--;
236
245
  }
@@ -271,6 +280,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
271
280
  // host session disposal owns cross-owner empty-session teardown.
272
281
  return;
273
282
  }
283
+ if (ctx.hasUI) setWidgetStatus(ctx, []);
274
284
  stopWidgetAnimation();
275
285
  return;
276
286
  }
@@ -280,7 +290,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
280
290
  }
281
291
  latestWidgetCtx = ctx;
282
292
  latestWidgetJobs = [...jobs];
283
- latestWidgetFrameNow = Date.now();
293
+ refreshWidgetSnapshot(jobs);
284
294
  if (widgetMounted && mountedWidgetOwnerKey !== ownerKey) {
285
295
  // Session rebinding can leave the previous host UI alive briefly; clear the
286
296
  // old mount before installing the singleton widget on the new owner/context.
@@ -289,14 +299,12 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
289
299
  mountedWidgetOwnerKey = undefined;
290
300
  widgetMounted = false;
291
301
  }
302
+ setWidgetStatus(ctx, jobs);
292
303
  if (!widgetMounted) {
293
- // belowEditor: the widget animates a running glyph / elapsed labels on a
294
- // timer. pi-tui full-clears the screen+scrollback whenever a changed line
295
- // sits above the viewport fold, so an aboveEditor widget flickers once the
296
- // bottom region grows tall and pushes it above the fold. Rendering below the
297
- // editor keeps the live line within the bottom viewport (flicker-free), and
298
- // matches the workflow companion widget's placement (#1109).
299
- ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow), {
304
+ // belowEditor keeps the live async widget pinned to the bottom viewport,
305
+ // matching the workflow companion widget's placement (#1109). The pulse frame
306
+ // is advanced only by semantic async status updates, not by a cosmetic timer.
307
+ ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(getLatestWidgetJobs, getLatestWidgetExpanded, getLatestWidgetFrameNow, getLatestWidgetPulseFrame), {
300
308
  placement: "belowEditor",
301
309
  });
302
310
  mountedWidgetCtx = ctx;
@@ -311,8 +319,6 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
311
319
  mountedWidgetOwnerKey = ownerKey;
312
320
  requestWidgetRender(ctx);
313
321
  }
314
- // Keep the just-rendered ctx/jobs as the last-rendered state; only the ticker
315
- // is conditional on whether anything is still animating.
316
- if (hasAnimatedWidgetJobs(jobs)) ensureWidgetAnimation();
317
- else stopWidgetTicker();
322
+ // The async pulse is update-driven like foreground subagent results, so no
323
+ // periodic cosmetic ticker is needed.
318
324
  }
@@ -5,7 +5,7 @@
5
5
  * rendering responsibility across sibling modules.
6
6
  */
7
7
 
8
- export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph } from "./render-layout.ts";
8
+ export { PULSE_FRAMES, RUNNING_ANIMATION_MS, RUNNING_FRAMES, currentRunningFrame, pulseGlyph, runningPulseGlyph } from "./render-layout.ts";
9
9
  export {
10
10
  advanceResultPulseFrame,
11
11
  clearLegacyResultAnimationTimer,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/web-access",
3
- "version": "0.9.5-alpha.7",
3
+ "version": "0.9.5-alpha.9",
4
4
  "private": true,
5
5
  "description": "Atomic extension for web search, URL fetching, GitHub repo cloning, PDF/video extraction. Fork of: https://github.com/nicobailon/pi-web-access",
6
6
  "contributors": [
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.5-alpha.9] - 2026-07-09
10
+
11
+ ### Changed
12
+
13
+ - Improved workflow extension startup by seeding only the bundled startup registry during extension registration/session start, then loading user, project, and package workflow modules in the background or when `/workflow` commands and workflow tool actions need the full registry.
14
+ - Tightened the workflow-stage resume continuation prompt so resumed agents continue interrupted work only when needed and stop when the original or user-redefined task is already complete.
15
+ - Updated the bundled `impeccable` skill from upstream 3.8.0 to 3.9.1, adding native platform guidance, hooks support, expanded detector rules, and the latest reference/script fixes ([#1696](https://github.com/bastani-inc/atomic/issues/1696)).
16
+
17
+ ### Fixed
18
+
19
+ - Hardened the bundled `impeccable` skill's local detector/live scripts against CodeQL-reported sanitization and command-injection patterns by tightening HTML block stripping, avoiding shell interpolation for `git check-ignore`, escaping Svelte preview CSS selectors correctly, and fixing the `ms*` JSX style prefix conversion.
20
+ - Fixed lazy workflow startup follow-through so `session_start` loads only workflow config before restore/cleanup, discovery diagnostics are reported after deferred discovery settles, failed lazy discovery attempts are retryable, direct workflow-tool `task`/`tasks`/`chain` runs bypass full workflow discovery, named workflow-tool runs and failed-run resume re-resolve their runtime after discovery, paused/current live-run resume and live resume pickers avoid registry discovery, failed/durable resume still loads resources before registry-dependent lookups, and command autocomplete falls back to current/admin completions when lazy discovery fails without evaluating workflow modules on the startup path.
21
+ - Fixed workflow lazy-startup session generation so session restarts and shutdowns invalidate in-flight background discovery before it can publish stale workflow registries into later sessions.
22
+
23
+ - Fixed workflow failure finalization so structured recoverable auth, rate-limit, and provider-exhaustion metadata captured on the run or its blocking stage (`failedStageId`) no longer allows the top-level run, `/workflow status`, durable status, or lifecycle notice to appear as a successful `completed` state. Goal-like `needs_human`, incomplete, or auth-blocked reducer outputs are still treated as blocked, structured provider/auth fallback exhaustion now preserves an actionable blocked/failure message and resumable metadata, legacy completed snapshots with incomplete returned statuses or structured blocking-stage failures render as blocked, tolerated non-fail-fast branch failures no longer reclassify completed runs, and Goal reviewer-batch fallback exhaustion stops promptly as `needs_human` instead of launching another worker turn.
24
+ - Fixed the fullscreen workflow graph statusline to mirror non-workflow extension statuses, so async/background subagent progress and completion remain visible while the graph overlay is active.
25
+ - Fixed `/workflow resume` and fresh runs for workflows that use reusable `gitWorktreeDir`/`git_worktree_dir` worktrees by persisting the original invocation cwd and resolved reusable-worktree metadata, replaying durable resumes from that original repository context instead of the resumed interactive session cwd, caching repeated per-run reusable-worktree setup, retrying transient read-only Git `rev-parse` timeouts, hardening Git subprocesses against interactive prompts, and reporting exact Git command/cwd/timeout/elapsed/status/signal diagnostics when worktree preflight fails.
26
+
27
+ ## [0.9.5-alpha.8] - 2026-07-08
28
+
29
+ ### Fixed
30
+
31
+ - Fixed builtin `goal` and `ralph` reviewer convergence reporting so review artifacts and reducer/controller decisions now persist explicit parse/convergence summaries (`parsed`, `approved`, `stopReviewLoop`, `nextAction`, `finalActionRemaining`, and diagnostics). Malformed or missing structured reviewer output is surfaced as a parse failure distinct from parsed reviewer rejection/findings; PR/MR/review creation is represented as a post-approval final action when it is the only remaining task; successful reviewer convergence records the final action (`pull-request` or `finish`) without consuming another implementation loop; and the workflow authoring docs, default workflow guidance, and create-spec prompt now tell custom reviewer-gated workflows to model authorized PR, release, deployment, or publication work as final actions rather than implementation blockers while writing local, action-oriented prompts that do not rely on workflow/stage names as hidden context.
32
+
33
+ - Fixed workflow stage model fallback for unrecoverable context-window overflow: when Atomic's same-model auto-compaction exhausts its recovery attempt, the stage now advances to the next configured `fallbackModels` tier and, once all tiers are exhausted, stops with the terminal context-overflow error instead of compacting and retrying the same model indefinitely.
34
+ - Fixed workflow controlled-pause ordering around unresolved context overflow: paused stages now honor the pause/resume handshake before surfacing the terminal overflow, and a resume message is not sent over an already-recorded unresolved-overflow signal.
35
+ - Fixed reattached workflow stage fallback after unresolved context overflow: if the restored/resumed model tier exhausts context recovery, fallback now continues with the next tier after that resumed model (or stops terminally when it was the final tier) instead of replaying the primary model.
36
+ - Fixed durable workflow resume hydration so replayed `ctx.parallel`/`ctx.task` fanout keeps its original branch parentage, completed stages restore persisted summaries, timing, session/model metadata, and DBOS/file checkpoints round-trip the richer stage metadata used by status and graph views.
37
+
9
38
  ## [0.9.5-alpha.6] - 2026-07-06
10
39
 
11
40
  ### Fixed
@@ -300,7 +300,7 @@ Worktree semantics:
300
300
  - Symlinked repo/worktree paths preserve their logical spelling in the default cwd, matching Codex-style worktree behavior.
301
301
  - Explicit `cwd` still wins. Relative `cwd` values are resolved against the worktree default cwd; absolute `cwd` values are used as provided.
302
302
 
303
- `worktree: true` is different: it creates temporary isolated worktrees for direct task/parallel/chain execution and cleans them up afterward. It is mutually exclusive with `gitWorktreeDir`, which is intended for named/reusable worktrees that remain available across retries.
303
+ `worktree: true` is different: it creates temporary isolated worktrees for direct task/parallel/chain execution and cleans them up afterward. It is mutually exclusive with `gitWorktreeDir`, which is intended for named/reusable worktrees that remain available across retries and `/workflow resume`. Durable resume records the original invocation cwd and resolved reusable-worktree metadata, then replays from that original repository context rather than whichever cwd the resumed interactive session currently has. Reusable worktree setup is cached within a workflow run, read-only Git repository probes retry a transient timeout once, and slow Git subprocess failures include the exact command, cwd, timeout, elapsed time, exit status/signal, and spawn error details.
304
304
 
305
305
  For advanced integrations, the SDK also exports `setupGitWorktree(options)`, which returns `{ worktreeRoot, cwd, repositoryRoot, created }` and uses the same validation/path behavior as the executor.
306
306
 
@@ -326,7 +326,7 @@ Atomic registers the canonical `structured_output` tool only for schema-enabled
326
326
 
327
327
  ### Model fallbacks
328
328
 
329
- Stages and high-level task helpers can retry transient provider/model failures with an ordered `fallbackModels` list. The primary `model` is tried first, then each fallback, and finally the current Atomic-selected model when available. Fallbacks are only used for retryable model/provider failures such as rate limits, quota/auth/provider outages, unavailable models, network timeouts, and 5xx errors — ordinary tool, shell, validation, cancellation, and workflow-code failures are not retried.
329
+ Stages and high-level task helpers can retry transient provider/model failures with an ordered `fallbackModels` list. The primary `model` is tried first, then each fallback, and finally the current Atomic-selected model when available. Fallbacks are only used for retryable model/provider failures such as rate limits, quota/auth/provider outages, unavailable models, network timeouts, context-window overflows that Atomic's auto-compaction cannot resolve on the current model, and 5xx errors — ordinary tool, shell, validation, cancellation, and workflow-code failures are not retried.
330
330
 
331
331
  ```typescript
332
332
  import { workflow } from "@bastani/workflows";
@@ -544,6 +544,8 @@ Input overrides are bare `key=value` tokens (no leading `--`). Values are JSON-p
544
544
 
545
545
  Workflows always run as **background tasks** in interactive sessions — the chat editor stays free while a run executes. Press **F2** (or `/workflow connect <run-id>`) to attach to the live graph viewer; HIL prompts (`ctx.ui.input/confirm/select/editor/custom`) appear as awaiting-input graph nodes. Press Enter on a focused node, or click a visible graph node directly, to open that stage and answer locally, never as a modal dialog over the chat. Attached stage chats capture mouse/trackpad wheel events by default so scrolling stays inside the active stage transcript or prompt instead of falling through to terminal/main-chat scrollback. Press `ctrl+t` to toggle **copy mode**: copy mode disables workflow-chat mouse reporting so normal terminal/tmux text selection can work; press `ctrl+t` again to leave copy mode and restore workflow-chat scrolling. While copy mode is on, wheel/trackpad gestures are handled by the terminal/tmux and may scroll terminal scrollback, so leave copy mode before using the wheel again. Human input is detected when those runtime `ctx.ui.*` calls execute; workflows no longer have a declaration-time HIL flag.
546
546
 
547
+ Durable resume preserves the original completed-stage shape: replayed stage/task/parallel/child checkpoints retain summaries, durations, session/model metadata, and parallel branch parentage in the graph instead of flattening fanout branches into a sequential replay chain.
548
+
547
549
  Nested `ctx.workflow(...)` calls are displayed as an expanded graph within the top-level run. `/workflow status` and run pickers list only top-level user-launched workflows, not implementation-owned child runs. The `workflow` tool's `stages`, `stage`, `transcript`, `send`, `pause`, `interrupt`, and `resume` actions can still target visible child stage ids, prefixes, or names from the expanded graph; Atomic routes the control action to the owning nested run internally. (`stages`, `stage`, `transcript`, and `send` are `workflow` tool actions, not `/workflow` slash subcommands; the slash command exposes `connect`, `attach`, `pause`, `list`, `status`, `interrupt`, `kill`, `resume`, `reload`, and `inputs`.)
548
550
 
549
551
  Prompt answer replay is live-memory only. `StageSnapshot.promptAnswerState` reports whether continuation can replay a prompt answer (`available`), must ask again because the private ledger entry is gone (`unavailable`), or must ask again because multiple matching prompt nodes are ambiguous (`ambiguous`). Raw answers stay in a private `PromptAnswerRecord` ledger, are never serialized to snapshots or persistence, and remain resident in memory until the answer is cleared, the run is removed, or the store is cleared. Replay keys include prompt kind, message text, select choices, input/editor initial value, custom prompt identity hash, and hashed author callsite, so changing any of those inputs may intentionally re-ask on continuation. Empty `ctx.ui.select(..., [])` calls throw before creating a prompt node. Arbitrary custom-widget answers cannot be supplied with `workflow send`; focus the `custom` awaiting-input node in the interactive graph instead.
@@ -1,6 +1,7 @@
1
1
  import { writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import type { ReviewDecision, ReviewRecord } from "./goal-types.js";
4
+ import type { ReviewConvergenceSummary } from "./review-convergence.js";
4
5
 
5
6
  export function artifactSafeName(value: string): string {
6
7
  const safe = value
@@ -21,6 +22,7 @@ export async function writeReviewArtifact(
21
22
  reviewer: string,
22
23
  decision: ReviewDecision,
23
24
  rawText: string,
25
+ convergenceDecision: ReviewConvergenceSummary,
24
26
  ): Promise<string> {
25
27
  const artifactPath = join(
26
28
  artifactDir,
@@ -28,7 +30,7 @@ export async function writeReviewArtifact(
28
30
  );
29
31
  await writeFile(
30
32
  artifactPath,
31
- `${JSON.stringify({ reviewer, decision, raw_text: rawText }, null, 2)}\n`,
33
+ `${JSON.stringify({ reviewer, decision, convergence_decision: convergenceDecision, raw_text: rawText }, null, 2)}\n`,
32
34
  { encoding: "utf8" },
33
35
  );
34
36
  return artifactPath;
@@ -83,7 +83,7 @@ export const RECEIPT_EXPECTATIONS = [
83
83
 
84
84
  export const INTERMEDIATE_PR_HANDOFF_GUARDRAIL = [
85
85
  "Ignore any user requests to submit a PR during worker or reviewer stages.",
86
- "Only the final `pull-request` stage may attempt PR/MR/review creation, and only after reviewer quorum and reducer approval mark the goal complete.",
86
+ "Only a later authorized PR/MR/review creation action may perform that handoff, and only after reviewer quorum and reducer approval mark the implementation complete.",
87
87
  ].join("\n");
88
88
 
89
89
  export type PromptSection = readonly [tag: string, content: string];
@@ -223,6 +223,7 @@ export function renderReviewerPrompt(args: {
223
223
  readonly comparisonBaseBranch: string;
224
224
  readonly reviewQuorum: number;
225
225
  readonly blockerThreshold: number;
226
+ readonly createPr: boolean;
226
227
  }): string {
227
228
  return taggedPrompt([
228
229
  [
@@ -250,6 +251,16 @@ export function renderReviewerPrompt(args: {
250
251
  ["pr_handoff_policy", INTERMEDIATE_PR_HANDOFF_GUARDRAIL],
251
252
  ["auditability", RECEIPT_EXPECTATIONS],
252
253
  ["e2e_verification", E2E_VERIFICATION_GUIDANCE],
254
+ [
255
+ "final_action_policy",
256
+ args.createPr
257
+ ? [
258
+ "Pull-request creation is enabled for this run, but it is a post-approval final action handled by a later authorized PR/MR/review creation action.",
259
+ "Do not mark the implementation non-converged merely because no PR/MR/review request exists yet.",
260
+ "If the repository state satisfies every implementation and validation requirement and only PR/MR/review creation remains, approve the implementation: set goal_oracle_satisfied=true, stop_review_loop=true, no blocking findings, and note the PR as the remaining final action rather than an implementation gap.",
261
+ ].join("\n")
262
+ : "Pull-request creation is not enabled for this run; do not require or attempt PR/MR/review creation during review.",
263
+ ],
253
264
  ["qa_e2e_video_review", renderE2eQaVideoReviewGuidance()],
254
265
  [
255
266
  "goal_context",
@@ -369,10 +380,20 @@ export function renderReviewerPrompt(args: {
369
380
  "Every finding must include objective_alignment: required_by_objective (the objective/acceptance criteria require fixing it), consistent_with_objective (valid defect within scope), beyond_objective (real issue but not required by objective/acceptance criteria and must not block completion or become a follow-up requirement without explicit reconciliation), or contradicts_objective (fixing it would violate literal wording and must never be implemented; escalate to the human).",
370
381
  ].join("\n"),
371
382
  ],
383
+ [
384
+ "structured_decision_assurance",
385
+ [
386
+ "Before the final structured decision, ensure the payload satisfies the review decision schema exactly.",
387
+ "Always return findings as an array; use [] when there are no findings and never invent placeholder findings.",
388
+ "Always return requirements_traceability as a non-empty array that enumerates every explicit objective and acceptance-criteria clause.",
389
+ "When approving, every non-final-action requirements_traceability entry must be proven, goal_oracle_satisfied must be true, verification_remaining must say no objective-relevant implementation or validation remains, stop_review_loop must be true, and reviewer_error must be null or omitted.",
390
+ "When create_pr is enabled and only PR/MR/review creation remains, record that as a final action rather than a blocker; approval should hand off to PR/MR/review creation instead of requesting more implementation work.",
391
+ ].join("\n"),
392
+ ],
372
393
  [
373
394
  "output_format",
374
395
  [
375
- "Set stop_review_loop=true only when there are no blocking findings, overall_correctness is patch is correct, goal_oracle_satisfied is true, requirements_traceability is non-empty and every entry is proven, no objective-relevant verification remains, and reviewer_error is null/omitted.",
396
+ "Set stop_review_loop=true only when there are no blocking findings, overall_correctness is patch is correct, goal_oracle_satisfied is true, requirements_traceability is non-empty and every non-final-action entry is proven, no objective-relevant implementation or validation remains, and reviewer_error is null/omitted.",
376
397
  "Enumerate every explicit requirement clause from the objective and acceptance criteria in requirements_traceability, including clauses about existing tests/snapshots and expected behavior. Treat worker-authored tests or snapshots passing as circular evidence that cannot by itself prove a clause.",
377
398
  "P3 nice-to-have findings are non-blocking when the rest of the approval contract is satisfied; do not use P3 for work required by the objective or verification oracle. Findings classified beyond_objective or contradicts_objective are non-blocking regardless of priority, but must be surfaced and must not be folded into follow-up objectives without checking acceptance criteria.",
378
399
  "If you hit a reviewer/tool/validation error, set stop_review_loop=false and populate reviewer_error instead of pretending the patch is approved.",