@bastani/atomic 0.8.13 → 0.8.14-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 (355) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/builtin/intercom/package.json +1 -1
  3. package/dist/builtin/mcp/host-html-template.ts +1 -1
  4. package/dist/builtin/mcp/init.ts +15 -2
  5. package/dist/builtin/mcp/mcp-callback-server.ts +10 -9
  6. package/dist/builtin/mcp/package.json +1 -1
  7. package/dist/builtin/mcp/ui-session.ts +9 -6
  8. package/dist/builtin/subagents/CHANGELOG.md +8 -1
  9. package/dist/builtin/subagents/README.md +39 -32
  10. package/dist/builtin/subagents/package.json +1 -1
  11. package/dist/builtin/subagents/skills/subagent/SKILL.md +11 -11
  12. package/dist/builtin/subagents/src/agents/agent-management.ts +6 -1
  13. package/dist/builtin/subagents/src/agents/agent-serializer.ts +2 -0
  14. package/dist/builtin/subagents/src/agents/agents.ts +44 -19
  15. package/dist/builtin/subagents/src/extension/config.ts +16 -0
  16. package/dist/builtin/subagents/src/extension/fanout-child.ts +246 -0
  17. package/dist/builtin/subagents/src/extension/index.ts +466 -603
  18. package/dist/builtin/subagents/src/intercom/intercom-bridge.ts +6 -4
  19. package/dist/builtin/subagents/src/intercom/result-intercom.ts +109 -1
  20. package/dist/builtin/subagents/src/runs/background/async-execution.ts +124 -19
  21. package/dist/builtin/subagents/src/runs/background/async-job-tracker.ts +41 -6
  22. package/dist/builtin/subagents/src/runs/background/async-resume.ts +28 -15
  23. package/dist/builtin/subagents/src/runs/background/async-status.ts +60 -30
  24. package/dist/builtin/subagents/src/runs/background/result-watcher.ts +111 -54
  25. package/dist/builtin/subagents/src/runs/background/run-id-resolver.ts +83 -0
  26. package/dist/builtin/subagents/src/runs/background/run-status.ts +79 -3
  27. package/dist/builtin/subagents/src/runs/background/stale-run-reconciler.ts +46 -1
  28. package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +66 -14
  29. package/dist/builtin/subagents/src/runs/foreground/chain-execution.ts +10 -3
  30. package/dist/builtin/subagents/src/runs/foreground/execution.ts +14 -2
  31. package/dist/builtin/subagents/src/runs/foreground/subagent-executor.ts +320 -23
  32. package/dist/builtin/subagents/src/runs/shared/completion-guard.ts +23 -1
  33. package/dist/builtin/subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +369 -0
  34. package/dist/builtin/subagents/src/runs/shared/nested-events.ts +935 -0
  35. package/dist/builtin/subagents/src/runs/shared/nested-path.ts +52 -0
  36. package/dist/builtin/subagents/src/runs/shared/nested-render.ts +115 -0
  37. package/dist/builtin/subagents/src/runs/shared/parallel-utils.ts +1 -0
  38. package/dist/builtin/subagents/src/runs/shared/pi-args.ts +82 -9
  39. package/dist/builtin/subagents/src/runs/shared/pi-spawn.ts +1 -1
  40. package/dist/builtin/subagents/src/runs/shared/single-output.ts +12 -2
  41. package/dist/builtin/subagents/src/runs/shared/subagent-prompt-runtime.ts +32 -10
  42. package/dist/builtin/subagents/src/runs/shared/worktree.ts +3 -2
  43. package/dist/builtin/subagents/src/shared/artifacts.ts +0 -1
  44. package/dist/builtin/subagents/src/shared/types.ts +96 -1
  45. package/dist/builtin/subagents/src/shared/utils.ts +10 -2
  46. package/dist/builtin/subagents/src/slash/slash-commands.ts +468 -625
  47. package/dist/builtin/subagents/src/tui/render.ts +1227 -2093
  48. package/dist/builtin/web-access/package.json +1 -1
  49. package/dist/builtin/workflows/CHANGELOG.md +24 -0
  50. package/dist/builtin/workflows/README.md +28 -11
  51. package/dist/builtin/workflows/builtin/deep-research-codebase.ts +323 -40
  52. package/dist/builtin/workflows/builtin/ralph.ts +362 -176
  53. package/dist/builtin/workflows/package.json +2 -5
  54. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  55. package/dist/builtin/workflows/skills/skill-creator/LICENSE.txt +202 -0
  56. package/dist/builtin/workflows/skills/skill-creator/SKILL.md +489 -0
  57. package/dist/builtin/workflows/skills/skill-creator/agents/analyzer.md +274 -0
  58. package/dist/builtin/workflows/skills/skill-creator/agents/comparator.md +202 -0
  59. package/dist/builtin/workflows/skills/skill-creator/agents/grader.md +223 -0
  60. package/dist/builtin/workflows/skills/skill-creator/assets/eval_review.html +146 -0
  61. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  62. package/dist/builtin/workflows/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  63. package/dist/builtin/workflows/skills/skill-creator/references/schemas.md +430 -0
  64. package/dist/builtin/workflows/skills/skill-creator/scripts/__init__.py +0 -0
  65. package/dist/builtin/workflows/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  66. package/dist/builtin/workflows/skills/skill-creator/scripts/generate_report.py +326 -0
  67. package/dist/builtin/workflows/skills/skill-creator/scripts/improve_description.py +247 -0
  68. package/dist/builtin/workflows/skills/skill-creator/scripts/package_skill.py +136 -0
  69. package/dist/builtin/workflows/skills/skill-creator/scripts/quick_validate.py +103 -0
  70. package/dist/builtin/workflows/skills/skill-creator/scripts/run_eval.py +310 -0
  71. package/dist/builtin/workflows/skills/skill-creator/scripts/run_loop.py +328 -0
  72. package/dist/builtin/workflows/skills/skill-creator/scripts/utils.py +47 -0
  73. package/dist/builtin/workflows/src/extension/index.ts +869 -93
  74. package/dist/builtin/workflows/src/extension/render-call.ts +34 -1
  75. package/dist/builtin/workflows/src/extension/render-result.ts +126 -21
  76. package/dist/builtin/workflows/src/extension/runtime.ts +91 -3
  77. package/dist/builtin/workflows/src/extension/wiring.ts +38 -12
  78. package/dist/builtin/workflows/src/extension/workflow-schema.ts +62 -5
  79. package/dist/builtin/workflows/src/runs/background/runner.ts +3 -3
  80. package/dist/builtin/workflows/src/runs/background/status.ts +42 -8
  81. package/dist/builtin/workflows/src/runs/foreground/executor.ts +410 -95
  82. package/dist/builtin/workflows/src/runs/foreground/stage-control-registry.ts +5 -2
  83. package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +8 -0
  84. package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +6 -4
  85. package/dist/builtin/workflows/src/runs/shared/worktree.ts +3 -2
  86. package/dist/builtin/workflows/src/shared/persistence-restore.ts +138 -5
  87. package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +30 -0
  88. package/dist/builtin/workflows/src/shared/render-inputs-schema.ts +78 -120
  89. package/dist/builtin/workflows/src/shared/stage-ui-broker.ts +193 -0
  90. package/dist/builtin/workflows/src/shared/store-types.ts +26 -1
  91. package/dist/builtin/workflows/src/shared/store.ts +145 -17
  92. package/dist/builtin/workflows/src/shared/timing.ts +6 -2
  93. package/dist/builtin/workflows/src/shared/workflow-failures.ts +375 -0
  94. package/dist/builtin/workflows/src/tui/chat-surface.ts +68 -17
  95. package/dist/builtin/workflows/src/tui/connectors.ts +2 -2
  96. package/dist/builtin/workflows/src/tui/dispatch-confirm.ts +24 -26
  97. package/dist/builtin/workflows/src/tui/graph-canvas.ts +4 -8
  98. package/dist/builtin/workflows/src/tui/graph-view.ts +17 -14
  99. package/dist/builtin/workflows/src/tui/header.ts +38 -0
  100. package/dist/builtin/workflows/src/tui/inline-form-card.ts +161 -238
  101. package/dist/builtin/workflows/src/tui/inline-form-editor.ts +68 -73
  102. package/dist/builtin/workflows/src/tui/inline-form-overlay.ts +2 -3
  103. package/dist/builtin/workflows/src/tui/inline-form-store.ts +2 -1
  104. package/dist/builtin/workflows/src/tui/inputs-overlay.ts +1 -3
  105. package/dist/builtin/workflows/src/tui/inputs-picker.ts +286 -399
  106. package/dist/builtin/workflows/src/tui/keybindings-adapter.ts +11 -0
  107. package/dist/builtin/workflows/src/tui/node-card.ts +2 -1
  108. package/dist/builtin/workflows/src/tui/overlay-adapter.ts +9 -1
  109. package/dist/builtin/workflows/src/tui/prompt-card.ts +46 -19
  110. package/dist/builtin/workflows/src/tui/run-detail.ts +63 -80
  111. package/dist/builtin/workflows/src/tui/session-confirm.ts +9 -3
  112. package/dist/builtin/workflows/src/tui/session-picker.ts +19 -16
  113. package/dist/builtin/workflows/src/tui/stage-chat-layout.ts +88 -0
  114. package/dist/builtin/workflows/src/tui/stage-chat-view.ts +368 -879
  115. package/dist/builtin/workflows/src/tui/status-helpers.ts +4 -0
  116. package/dist/builtin/workflows/src/tui/status-list.ts +67 -75
  117. package/dist/builtin/workflows/src/tui/store-widget-installer.ts +50 -12
  118. package/dist/builtin/workflows/src/tui/submit-pane.ts +164 -0
  119. package/dist/builtin/workflows/src/tui/switcher.ts +27 -4
  120. package/dist/builtin/workflows/src/tui/text-helpers.ts +98 -4
  121. package/dist/builtin/workflows/src/tui/widget.ts +90 -68
  122. package/dist/builtin/workflows/src/tui/workflow-attach-pane.ts +23 -2
  123. package/dist/builtin/workflows/src/tui/workflow-list.ts +44 -68
  124. package/dist/cli/file-processor.d.ts.map +1 -1
  125. package/dist/cli/file-processor.js +2 -3
  126. package/dist/cli/file-processor.js.map +1 -1
  127. package/dist/config.d.ts.map +1 -1
  128. package/dist/config.js +3 -10
  129. package/dist/config.js.map +1 -1
  130. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  131. package/dist/core/agent-session-runtime.js +2 -1
  132. package/dist/core/agent-session-runtime.js.map +1 -1
  133. package/dist/core/agent-session-services.d.ts.map +1 -1
  134. package/dist/core/agent-session-services.js +3 -2
  135. package/dist/core/agent-session-services.js.map +1 -1
  136. package/dist/core/agent-session.d.ts +6 -0
  137. package/dist/core/agent-session.d.ts.map +1 -1
  138. package/dist/core/agent-session.js +16 -2
  139. package/dist/core/agent-session.js.map +1 -1
  140. package/dist/core/atomic-guide-command.d.ts.map +1 -1
  141. package/dist/core/atomic-guide-command.js +8 -9
  142. package/dist/core/atomic-guide-command.js.map +1 -1
  143. package/dist/core/auth-storage.d.ts.map +1 -1
  144. package/dist/core/auth-storage.js +3 -2
  145. package/dist/core/auth-storage.js.map +1 -1
  146. package/dist/core/bash-executor.d.ts.map +1 -1
  147. package/dist/core/bash-executor.js +2 -1
  148. package/dist/core/bash-executor.js.map +1 -1
  149. package/dist/core/export-html/index.d.ts.map +1 -1
  150. package/dist/core/export-html/index.js +8 -6
  151. package/dist/core/export-html/index.js.map +1 -1
  152. package/dist/core/export-html/template.js +6 -3
  153. package/dist/core/extensions/loader.d.ts.map +1 -1
  154. package/dist/core/extensions/loader.js +12 -29
  155. package/dist/core/extensions/loader.js.map +1 -1
  156. package/dist/core/model-registry.d.ts.map +1 -1
  157. package/dist/core/model-registry.js +5 -1
  158. package/dist/core/model-registry.js.map +1 -1
  159. package/dist/core/package-manager.d.ts +8 -0
  160. package/dist/core/package-manager.d.ts.map +1 -1
  161. package/dist/core/package-manager.js +145 -58
  162. package/dist/core/package-manager.js.map +1 -1
  163. package/dist/core/prompt-templates.d.ts.map +1 -1
  164. package/dist/core/prompt-templates.js +6 -20
  165. package/dist/core/prompt-templates.js.map +1 -1
  166. package/dist/core/resource-loader.d.ts.map +1 -1
  167. package/dist/core/resource-loader.js +38 -31
  168. package/dist/core/resource-loader.js.map +1 -1
  169. package/dist/core/sdk.d.ts.map +1 -1
  170. package/dist/core/sdk.js +9 -4
  171. package/dist/core/sdk.js.map +1 -1
  172. package/dist/core/session-manager.d.ts.map +1 -1
  173. package/dist/core/session-manager.js +32 -24
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/settings-manager.d.ts.map +1 -1
  176. package/dist/core/settings-manager.js +8 -15
  177. package/dist/core/settings-manager.js.map +1 -1
  178. package/dist/core/skills.d.ts.map +1 -1
  179. package/dist/core/skills.js +8 -22
  180. package/dist/core/skills.js.map +1 -1
  181. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts +5 -4
  182. package/dist/core/tools/ask-user-question/state/questionnaire-session.d.ts.map +1 -1
  183. package/dist/core/tools/ask-user-question/state/questionnaire-session.js +34 -11
  184. package/dist/core/tools/ask-user-question/state/questionnaire-session.js.map +1 -1
  185. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts +1 -0
  186. package/dist/core/tools/ask-user-question/state/selectors/contract.d.ts.map +1 -1
  187. package/dist/core/tools/ask-user-question/state/selectors/contract.js.map +1 -1
  188. package/dist/core/tools/ask-user-question/state/selectors/projections.d.ts.map +1 -1
  189. package/dist/core/tools/ask-user-question/state/selectors/projections.js +1 -0
  190. package/dist/core/tools/ask-user-question/state/selectors/projections.js.map +1 -1
  191. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts +1 -2
  192. package/dist/core/tools/ask-user-question/state/state-reducer.d.ts.map +1 -1
  193. package/dist/core/tools/ask-user-question/state/state-reducer.js +26 -9
  194. package/dist/core/tools/ask-user-question/state/state-reducer.js.map +1 -1
  195. package/dist/core/tools/ask-user-question/state/state.d.ts +4 -0
  196. package/dist/core/tools/ask-user-question/state/state.d.ts.map +1 -1
  197. package/dist/core/tools/ask-user-question/state/state.js.map +1 -1
  198. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts +1 -0
  199. package/dist/core/tools/ask-user-question/view/components/option-list-view.d.ts.map +1 -1
  200. package/dist/core/tools/ask-user-question/view/components/option-list-view.js +1 -0
  201. package/dist/core/tools/ask-user-question/view/components/option-list-view.js.map +1 -1
  202. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts +9 -6
  203. package/dist/core/tools/ask-user-question/view/components/wrapping-select.d.ts.map +1 -1
  204. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js +28 -7
  205. package/dist/core/tools/ask-user-question/view/components/wrapping-select.js.map +1 -1
  206. package/dist/core/tools/ask-user-question/view/props-adapter.d.ts.map +1 -1
  207. package/dist/core/tools/ask-user-question/view/props-adapter.js +4 -1
  208. package/dist/core/tools/ask-user-question/view/props-adapter.js.map +1 -1
  209. package/dist/core/tools/bash.d.ts.map +1 -1
  210. package/dist/core/tools/bash.js +56 -53
  211. package/dist/core/tools/bash.js.map +1 -1
  212. package/dist/core/tools/edit-diff.d.ts +3 -1
  213. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  214. package/dist/core/tools/edit-diff.js +8 -1
  215. package/dist/core/tools/edit-diff.js.map +1 -1
  216. package/dist/core/tools/edit.d.ts +3 -1
  217. package/dist/core/tools/edit.d.ts.map +1 -1
  218. package/dist/core/tools/edit.js +44 -81
  219. package/dist/core/tools/edit.js.map +1 -1
  220. package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
  221. package/dist/core/tools/file-mutation-queue.js +27 -12
  222. package/dist/core/tools/file-mutation-queue.js.map +1 -1
  223. package/dist/core/tools/find.d.ts.map +1 -1
  224. package/dist/core/tools/find.js +2 -3
  225. package/dist/core/tools/find.js.map +1 -1
  226. package/dist/core/tools/grep.d.ts.map +1 -1
  227. package/dist/core/tools/grep.js +3 -3
  228. package/dist/core/tools/grep.js.map +1 -1
  229. package/dist/core/tools/ls.d.ts.map +1 -1
  230. package/dist/core/tools/ls.js +5 -5
  231. package/dist/core/tools/ls.js.map +1 -1
  232. package/dist/core/tools/output-accumulator.d.ts +2 -0
  233. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  234. package/dist/core/tools/output-accumulator.js +11 -4
  235. package/dist/core/tools/output-accumulator.js.map +1 -1
  236. package/dist/core/tools/path-utils.d.ts +2 -0
  237. package/dist/core/tools/path-utils.d.ts.map +1 -1
  238. package/dist/core/tools/path-utils.js +39 -21
  239. package/dist/core/tools/path-utils.js.map +1 -1
  240. package/dist/core/tools/read.d.ts.map +1 -1
  241. package/dist/core/tools/read.js +9 -8
  242. package/dist/core/tools/read.js.map +1 -1
  243. package/dist/core/tools/truncate.d.ts.map +1 -1
  244. package/dist/core/tools/truncate.js +12 -2
  245. package/dist/core/tools/truncate.js.map +1 -1
  246. package/dist/core/tools/write.d.ts.map +1 -1
  247. package/dist/core/tools/write.js +20 -35
  248. package/dist/core/tools/write.js.map +1 -1
  249. package/dist/index.d.ts +2 -1
  250. package/dist/index.d.ts.map +1 -1
  251. package/dist/index.js +4 -1
  252. package/dist/index.js.map +1 -1
  253. package/dist/main.d.ts.map +1 -1
  254. package/dist/main.js +5 -6
  255. package/dist/main.js.map +1 -1
  256. package/dist/modes/interactive/chat-input-actions.d.ts +24 -0
  257. package/dist/modes/interactive/chat-input-actions.d.ts.map +1 -0
  258. package/dist/modes/interactive/chat-input-actions.js +179 -0
  259. package/dist/modes/interactive/chat-input-actions.js.map +1 -0
  260. package/dist/modes/interactive/components/chat-message-renderer.d.ts +1 -0
  261. package/dist/modes/interactive/components/chat-message-renderer.d.ts.map +1 -1
  262. package/dist/modes/interactive/components/chat-message-renderer.js +14 -3
  263. package/dist/modes/interactive/components/chat-message-renderer.js.map +1 -1
  264. package/dist/modes/interactive/components/chat-session-host.d.ts +157 -0
  265. package/dist/modes/interactive/components/chat-session-host.d.ts.map +1 -0
  266. package/dist/modes/interactive/components/chat-session-host.js +1007 -0
  267. package/dist/modes/interactive/components/chat-session-host.js.map +1 -0
  268. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  269. package/dist/modes/interactive/components/config-selector.js +1 -1
  270. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  271. package/dist/modes/interactive/components/footer.d.ts +1 -0
  272. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  273. package/dist/modes/interactive/components/footer.js +14 -5
  274. package/dist/modes/interactive/components/footer.js.map +1 -1
  275. package/dist/modes/interactive/components/index.d.ts +1 -0
  276. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  277. package/dist/modes/interactive/components/index.js +1 -0
  278. package/dist/modes/interactive/components/index.js.map +1 -1
  279. package/dist/modes/interactive/components/login-dialog.d.ts +9 -1
  280. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/login-dialog.js +29 -4
  282. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  283. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  284. package/dist/modes/interactive/interactive-mode.js +18 -67
  285. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  286. package/dist/utils/child-process.d.ts +1 -0
  287. package/dist/utils/child-process.d.ts.map +1 -1
  288. package/dist/utils/child-process.js +8 -0
  289. package/dist/utils/child-process.js.map +1 -1
  290. package/dist/utils/clipboard-native.d.ts +3 -1
  291. package/dist/utils/clipboard-native.d.ts.map +1 -1
  292. package/dist/utils/clipboard-native.js +14 -8
  293. package/dist/utils/clipboard-native.js.map +1 -1
  294. package/dist/utils/image-resize-core.d.ts +30 -0
  295. package/dist/utils/image-resize-core.d.ts.map +1 -0
  296. package/dist/utils/image-resize-core.js +124 -0
  297. package/dist/utils/image-resize-core.js.map +1 -0
  298. package/dist/utils/image-resize-worker.d.ts +2 -0
  299. package/dist/utils/image-resize-worker.d.ts.map +1 -0
  300. package/dist/utils/image-resize-worker.js +31 -0
  301. package/dist/utils/image-resize-worker.js.map +1 -0
  302. package/dist/utils/image-resize.d.ts +7 -27
  303. package/dist/utils/image-resize.d.ts.map +1 -1
  304. package/dist/utils/image-resize.js +75 -115
  305. package/dist/utils/image-resize.js.map +1 -1
  306. package/dist/utils/paths.d.ts +16 -1
  307. package/dist/utils/paths.d.ts.map +1 -1
  308. package/dist/utils/paths.js +49 -7
  309. package/dist/utils/paths.js.map +1 -1
  310. package/docs/changelog.mdx +29 -0
  311. package/docs/compaction.md +1 -1
  312. package/docs/custom-provider.md +2 -2
  313. package/docs/development.md +1 -1
  314. package/docs/docs.json +98 -143
  315. package/docs/extensions.md +29 -16
  316. package/docs/favicon.svg +29 -0
  317. package/docs/images/interactive-mode.png +0 -0
  318. package/docs/images/tree-view.png +0 -0
  319. package/docs/images/workflow-command.png +0 -0
  320. package/docs/images/workflow-graph.png +0 -0
  321. package/docs/images/workflow-input-picker.png +0 -0
  322. package/docs/images/workflow-list.png +0 -0
  323. package/docs/index.md +10 -1
  324. package/docs/logo.svg +59 -0
  325. package/docs/packages.md +3 -3
  326. package/docs/providers.md +1 -1
  327. package/docs/quickstart.md +98 -2
  328. package/docs/rpc.md +8 -8
  329. package/docs/sdk.md +23 -12
  330. package/docs/sessions.md +1 -1
  331. package/docs/skills.md +15 -1
  332. package/docs/termux.md +11 -1
  333. package/docs/themes.md +6 -6
  334. package/docs/tui.md +18 -18
  335. package/docs/usage.md +1 -1
  336. package/docs/workflows.md +172 -2
  337. package/examples/extensions/subagent/index.ts +2 -1
  338. package/package.json +6 -6
  339. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/SKILL.md +0 -0
  340. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/element-attributes.md +0 -0
  341. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/playwright-tests.md +0 -0
  342. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/request-mocking.md +0 -0
  343. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/running-code.md +0 -0
  344. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/session-management.md +0 -0
  345. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/spec-driven-testing.md +0 -0
  346. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/storage-state.md +0 -0
  347. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/test-generation.md +0 -0
  348. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/tracing.md +0 -0
  349. /package/dist/builtin/{workflows → subagents}/skills/playwright-cli/references/video-recording.md +0 -0
  350. /package/dist/builtin/{workflows → subagents}/skills/tdd/SKILL.md +0 -0
  351. /package/dist/builtin/{workflows → subagents}/skills/tdd/deep-modules.md +0 -0
  352. /package/dist/builtin/{workflows → subagents}/skills/tdd/interface-design.md +0 -0
  353. /package/dist/builtin/{workflows → subagents}/skills/tdd/mocking.md +0 -0
  354. /package/dist/builtin/{workflows → subagents}/skills/tdd/refactoring.md +0 -0
  355. /package/dist/builtin/{workflows → subagents}/skills/tdd/tests.md +0 -0
@@ -0,0 +1,193 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Component, Focusable, TUI } from "@earendil-works/pi-tui";
3
+ import type { Store } from "./store.js";
4
+ import { store as defaultStore } from "./store.js";
5
+ import type { PiCustomOverlayFactory, PiCustomOverlayOptions, PiKeybindings, PiTheme } from "../extension/wiring.js";
6
+
7
+ export interface StageCustomUiRequest<T = unknown> {
8
+ readonly id: string;
9
+ readonly runId: string;
10
+ readonly stageId: string;
11
+ readonly factory: PiCustomOverlayFactory<T>;
12
+ readonly options?: PiCustomOverlayOptions;
13
+ readonly createdAt: number;
14
+ resolve(value: T): void;
15
+ reject(reason: unknown): void;
16
+ }
17
+
18
+ export interface StageCustomUiHost {
19
+ showCustomUi(request: StageCustomUiRequest): void;
20
+ hideCustomUi?(request: StageCustomUiRequest, reason: unknown): void;
21
+ }
22
+
23
+ function key(runId: string, stageId: string): string {
24
+ return `${runId}\0${stageId}`;
25
+ }
26
+
27
+ function nextRequestId(): string {
28
+ return `stage-ui-${randomUUID()}`;
29
+ }
30
+
31
+ export class StageUiBroker {
32
+ private readonly store: Store;
33
+ private readonly pending = new Map<string, StageCustomUiRequest>();
34
+ private readonly hosts = new Map<string, StageCustomUiHost>();
35
+
36
+ constructor(store: Store = defaultStore) {
37
+ this.store = store;
38
+ }
39
+
40
+ private hideHost(host: StageCustomUiHost | undefined, request: StageCustomUiRequest, reason: unknown): void {
41
+ try {
42
+ host?.hideCustomUi?.(request, reason);
43
+ } catch {
44
+ // Host teardown is best-effort; request settlement must still continue.
45
+ }
46
+ }
47
+
48
+ private showHostOrReject(host: StageCustomUiHost, request: StageCustomUiRequest): void {
49
+ try {
50
+ host.showCustomUi(request);
51
+ } catch (error) {
52
+ this.reject(request, error);
53
+ }
54
+ }
55
+
56
+ registerHost(runId: string, stageId: string, host: StageCustomUiHost): () => void {
57
+ const hostKey = key(runId, stageId);
58
+ const previousHost = this.hosts.get(hostKey);
59
+ const request = this.pending.get(hostKey);
60
+ if (previousHost && previousHost !== host && request) {
61
+ this.hideHost(
62
+ previousHost,
63
+ request,
64
+ new Error(`pi-workflows: stage ${stageId} custom UI host replaced`),
65
+ );
66
+ }
67
+ this.hosts.set(hostKey, host);
68
+ const activeRequest = this.pending.get(hostKey);
69
+ if (activeRequest) this.showHostOrReject(host, activeRequest);
70
+ return () => {
71
+ if (this.hosts.get(hostKey) !== host) return;
72
+ this.hosts.delete(hostKey);
73
+ const pendingRequest = this.pending.get(hostKey);
74
+ if (pendingRequest) {
75
+ this.reject(
76
+ pendingRequest,
77
+ new Error(`pi-workflows: stage ${stageId} custom UI host unregistered`),
78
+ );
79
+ }
80
+ };
81
+ }
82
+
83
+ requestCustomUi<T>(
84
+ runId: string,
85
+ stageId: string,
86
+ factory: PiCustomOverlayFactory<T>,
87
+ options?: PiCustomOverlayOptions,
88
+ signal?: AbortSignal,
89
+ ): Promise<T> {
90
+ if (signal?.aborted) {
91
+ return Promise.reject(signal.reason ?? new Error("pi-workflows: stage UI request aborted"));
92
+ }
93
+ const hostKey = key(runId, stageId);
94
+ const existing = this.pending.get(hostKey);
95
+ if (existing) {
96
+ return Promise.reject(new Error(`pi-workflows: stage ${stageId} already has a pending custom UI request`));
97
+ }
98
+
99
+ let request!: StageCustomUiRequest<T>;
100
+ const promise = new Promise<T>((resolve, reject) => {
101
+ request = {
102
+ id: nextRequestId(),
103
+ runId,
104
+ stageId,
105
+ factory,
106
+ ...(options !== undefined ? { options } : {}),
107
+ createdAt: Date.now(),
108
+ resolve,
109
+ reject,
110
+ };
111
+ });
112
+
113
+ const onAbort = (): void => {
114
+ this.reject(request, signal?.reason ?? new Error("pi-workflows: stage UI request aborted"));
115
+ };
116
+ signal?.addEventListener("abort", onAbort, { once: true });
117
+
118
+ this.pending.set(hostKey, request);
119
+ this.store.recordStageAwaitingInput(runId, stageId, true, request.createdAt);
120
+ const host = this.hosts.get(hostKey);
121
+ if (host) this.showHostOrReject(host, request);
122
+ // Re-check after listener registration and host display; AbortSignal does
123
+ // not replay an already-fired abort event for listeners added later.
124
+ if (signal?.aborted) onAbort();
125
+
126
+ return promise.finally(() => {
127
+ signal?.removeEventListener("abort", onAbort);
128
+ });
129
+ }
130
+
131
+ resolve<T>(request: StageCustomUiRequest<T>, value: T): void {
132
+ const hostKey = key(request.runId, request.stageId);
133
+ if (this.pending.get(hostKey)?.id !== request.id) return;
134
+ this.pending.delete(hostKey);
135
+ this.store.recordStageAwaitingInput(request.runId, request.stageId, false);
136
+ this.hideHost(this.hosts.get(hostKey), request, undefined);
137
+ request.resolve(value);
138
+ }
139
+
140
+ reject(request: StageCustomUiRequest, reason: unknown): void {
141
+ const hostKey = key(request.runId, request.stageId);
142
+ if (this.pending.get(hostKey)?.id !== request.id) return;
143
+ this.pending.delete(hostKey);
144
+ this.store.recordStageAwaitingInput(request.runId, request.stageId, false);
145
+ this.hideHost(this.hosts.get(hostKey), request, reason);
146
+ request.reject(reason);
147
+ }
148
+ }
149
+
150
+ export interface MountedStageCustomUi {
151
+ readonly request: StageCustomUiRequest;
152
+ readonly component: Component & { dispose?(): void };
153
+ }
154
+
155
+ export async function mountStageCustomUi(
156
+ request: StageCustomUiRequest,
157
+ tui: TUI,
158
+ theme: PiTheme,
159
+ keybindings: PiKeybindings,
160
+ broker: StageUiBroker,
161
+ onDone?: () => void,
162
+ ): Promise<MountedStageCustomUi> {
163
+ const rawComponent = await request.factory(
164
+ tui as unknown as Parameters<StageCustomUiRequest["factory"]>[0],
165
+ theme,
166
+ keybindings,
167
+ (result: unknown) => {
168
+ broker.resolve(request, result);
169
+ onDone?.();
170
+ },
171
+ );
172
+ const component: Component & { dispose?(): void } & Partial<Focusable> = {
173
+ render: (width) => rawComponent.render(width),
174
+ ...(rawComponent.handleInput !== undefined
175
+ ? { handleInput: (data: string) => rawComponent.handleInput?.(data) }
176
+ : {}),
177
+ invalidate: () => rawComponent.invalidate?.(),
178
+ ...(rawComponent.dispose !== undefined ? { dispose: () => rawComponent.dispose?.() } : {}),
179
+ };
180
+ if ("focused" in rawComponent) {
181
+ Object.defineProperty(component, "focused", {
182
+ get: () => (rawComponent as Component & Partial<Focusable>).focused,
183
+ set: (value: boolean) => {
184
+ (rawComponent as Component & Partial<Focusable>).focused = value;
185
+ },
186
+ enumerable: true,
187
+ configurable: true,
188
+ });
189
+ }
190
+ return { request, component };
191
+ }
192
+
193
+ export const stageUiBroker = new StageUiBroker();
@@ -11,7 +11,10 @@ export type StageStatus =
11
11
  | "paused"
12
12
  | "blocked"
13
13
  | "completed"
14
- | "failed";
14
+ | "failed"
15
+ | "skipped";
16
+
17
+ export type WorkflowFailureKind = "auth" | "rate_limit" | "provider" | "cancelled" | "unknown";
15
18
 
16
19
  /**
17
20
  * Human-in-the-loop prompt kind. Mirrors the four `WorkflowUIContext` methods.
@@ -66,9 +69,21 @@ export interface StageSnapshot {
66
69
  durationMs?: number;
67
70
  result?: string;
68
71
  error?: string;
72
+ /** Structured workflow failure category for failed stages. */
73
+ failureKind?: WorkflowFailureKind;
74
+ /** Original unsanitized error text when different from `error`. */
75
+ failureMessage?: string;
76
+ /** Reason for stages skipped by fail-fast/cascade handling. */
77
+ skippedReason?: string;
78
+ /** Source stage id when this stage was replayed during failed-run continuation. */
79
+ replayedFromStageId?: string;
80
+ /** True when provider work was skipped by continuation replay. */
81
+ replayed?: boolean;
69
82
  readonly toolEvents: ToolEvent[];
70
83
  /** True while an in-stage ask_user_question tool is waiting on the user. */
71
84
  awaitingInputSince?: number;
85
+ /** Pending human-in-the-loop prompt owned by this workflow stage/node. */
86
+ pendingPrompt?: PendingPrompt;
72
87
  blockedByStageId?: string;
73
88
  notices?: StageNotice[];
74
89
  /**
@@ -125,6 +140,16 @@ export interface RunSnapshot {
125
140
  resumedAt?: number;
126
141
  result?: Record<string, unknown>;
127
142
  error?: string;
143
+ /** Structured workflow failure category for failed runs. */
144
+ failureKind?: WorkflowFailureKind;
145
+ /** Original unsanitized error text when different from `error`. */
146
+ failureMessage?: string;
147
+ failedStageId?: string;
148
+ resumable?: boolean;
149
+ /** Source failed run when this run is a continuation. */
150
+ resumedFromRunId?: string;
151
+ /** Source stage id where continuation resumes real execution. */
152
+ resumeFromStageId?: string;
128
153
  /**
129
154
  * Pending human-in-the-loop prompt. Set when a background workflow calls
130
155
  * `ctx.ui.input/confirm/select/editor`; cleared when the user responds via
@@ -11,6 +11,8 @@ import type {
11
11
  StoreSnapshot,
12
12
  ToolEvent,
13
13
  RunStatus,
14
+ StageStatus,
15
+ WorkflowFailureKind,
14
16
  WorkflowNotice,
15
17
  } from "./store-types.js";
16
18
  import { accumulatePausedDurationMs, elapsedRunMs } from "./timing.js";
@@ -18,6 +20,29 @@ import { accumulatePausedDurationMs, elapsedRunMs } from "./timing.js";
18
20
  /** Statuses that represent a terminal run state — cannot be overwritten. */
19
21
  const TERMINAL_STATUSES: ReadonlySet<RunStatus> = new Set(["completed", "failed", "killed"]);
20
22
 
23
+ function isTerminalStageStatus(status: StageStatus): boolean {
24
+ return status === "completed" || status === "failed" || status === "skipped";
25
+ }
26
+
27
+ function cannotAwaitInput(status: StageStatus): boolean {
28
+ return isTerminalStageStatus(status) || status === "paused" || status === "blocked";
29
+ }
30
+
31
+ function cannotBlock(status: StageStatus): boolean {
32
+ return isTerminalStageStatus(status) || status === "paused";
33
+ }
34
+
35
+ function cannotPause(status: StageStatus): boolean {
36
+ return isTerminalStageStatus(status) || status === "paused" || status === "blocked";
37
+ }
38
+
39
+ export interface RunEndMetadata {
40
+ readonly failureKind?: WorkflowFailureKind;
41
+ readonly failureMessage?: string;
42
+ readonly failedStageId?: string;
43
+ readonly resumable?: boolean;
44
+ }
45
+
21
46
  export interface Store {
22
47
  runs(): readonly RunSnapshot[];
23
48
  notices(): readonly WorkflowNotice[];
@@ -39,6 +64,7 @@ export interface Store {
39
64
  status: RunStatus,
40
65
  result?: Record<string, unknown>,
41
66
  error?: string,
67
+ metadata?: RunEndMetadata,
42
68
  ): boolean;
43
69
  /**
44
70
  * Remove a run from live workflow history/status. Any pending HIL prompt
@@ -84,6 +110,17 @@ export interface Store {
84
110
  * overlay-driven response. Foreground runs never call this.
85
111
  */
86
112
  awaitPendingPrompt(runId: string, promptId: string): Promise<unknown>;
113
+ /** Record a pending HIL prompt for a specific workflow stage/node. */
114
+ recordStagePendingPrompt(runId: string, stageId: string, prompt: PendingPrompt): boolean;
115
+ /** Resolve a pending HIL prompt on a specific workflow stage/node. */
116
+ resolveStagePendingPrompt(
117
+ runId: string,
118
+ stageId: string,
119
+ promptId: string,
120
+ response: unknown,
121
+ ): boolean;
122
+ /** Wait for a stage/node-scoped HIL prompt to resolve. */
123
+ awaitStagePendingPrompt(runId: string, stageId: string, promptId: string): Promise<unknown>;
87
124
  /**
88
125
  * Record Pi/pi SDK session metadata for a stage after lazy
89
126
  * attach. The serializable snapshot tracks this so post-mortem reopen
@@ -186,6 +223,26 @@ export function createStore(): Store {
186
223
  return run.stages.find((s) => s.id === stageId);
187
224
  }
188
225
 
226
+ function rejectPrompt(promptId: string, reason: string): void {
227
+ const entry = _resolvers.get(promptId);
228
+ if (!entry) return;
229
+ _resolvers.delete(promptId);
230
+ entry.reject(new Error(reason));
231
+ }
232
+
233
+ function rejectStagePrompt(stage: StageSnapshot, reason: string): void {
234
+ const prompt = stage.pendingPrompt;
235
+ if (!prompt) return;
236
+ stage.pendingPrompt = undefined;
237
+ rejectPrompt(prompt.id, reason);
238
+ }
239
+
240
+ function rejectAllStagePrompts(run: RunSnapshot, reason: string): void {
241
+ for (const stage of run.stages) {
242
+ rejectStagePrompt(stage, reason);
243
+ }
244
+ }
245
+
189
246
  return {
190
247
  runs(): readonly RunSnapshot[] {
191
248
  return _runs;
@@ -274,7 +331,13 @@ export function createStore(): Store {
274
331
  existing.durationMs = stage.durationMs;
275
332
  existing.result = stage.result;
276
333
  existing.error = stage.error;
334
+ existing.failureKind = stage.failureKind;
335
+ existing.failureMessage = stage.failureMessage;
336
+ existing.skippedReason = stage.skippedReason;
337
+ existing.replayedFromStageId = stage.replayedFromStageId;
338
+ existing.replayed = stage.replayed;
277
339
  delete existing.awaitingInputSince;
340
+ rejectStagePrompt(existing, `pi-workflows: stage ${stage.id} ended before prompt resolved`);
278
341
  _version++;
279
342
  notify();
280
343
  },
@@ -284,6 +347,7 @@ export function createStore(): Store {
284
347
  status: RunStatus,
285
348
  result?: Record<string, unknown>,
286
349
  error?: string,
350
+ metadata?: RunEndMetadata,
287
351
  ): boolean {
288
352
  const run = findRun(runId);
289
353
  if (!run) return false;
@@ -306,20 +370,21 @@ export function createStore(): Store {
306
370
  if ((status === "failed" || status === "killed") && error !== undefined) {
307
371
  run.error = error;
308
372
  }
373
+ if (metadata !== undefined) {
374
+ if (metadata.failureKind !== undefined) run.failureKind = metadata.failureKind;
375
+ if (metadata.failureMessage !== undefined) run.failureMessage = metadata.failureMessage;
376
+ if (metadata.failedStageId !== undefined) run.failedStageId = metadata.failedStageId;
377
+ if (metadata.resumable !== undefined) run.resumable = metadata.resumable;
378
+ }
309
379
  // Abandon any waiting HIL prompt — workflow body never resumed past
310
380
  // it, but the awaiter promise must reject so the executor's catch
311
381
  // can finalise the run state cleanly.
312
382
  const pending = run.pendingPrompt;
313
383
  if (pending) {
314
384
  run.pendingPrompt = undefined;
315
- const entry = _resolvers.get(pending.id);
316
- if (entry) {
317
- _resolvers.delete(pending.id);
318
- entry.reject(
319
- new Error(`pi-workflows: run ${runId} ended before prompt resolved`),
320
- );
321
- }
385
+ rejectPrompt(pending.id, `pi-workflows: run ${runId} ended before prompt resolved`);
322
386
  }
387
+ rejectAllStagePrompts(run, `pi-workflows: run ${runId} ended before prompt resolved`);
323
388
  _version++;
324
389
  notify();
325
390
  return true;
@@ -331,14 +396,9 @@ export function createStore(): Store {
331
396
  const run = _runs[index]!;
332
397
  const pending = run.pendingPrompt;
333
398
  if (pending) {
334
- const entry = _resolvers.get(pending.id);
335
- if (entry) {
336
- _resolvers.delete(pending.id);
337
- entry.reject(
338
- new Error(`pi-workflows: run ${runId} was removed before prompt resolved`),
339
- );
340
- }
399
+ rejectPrompt(pending.id, `pi-workflows: run ${runId} was removed before prompt resolved`);
341
400
  }
401
+ rejectAllStagePrompts(run, `pi-workflows: run ${runId} was removed before prompt resolved`);
342
402
  _runs.splice(index, 1);
343
403
  for (let i = _notices.length - 1; i >= 0; i--) {
344
404
  if (_notices[i]?.runId === runId) _notices.splice(i, 1);
@@ -416,6 +476,74 @@ export function createStore(): Store {
416
476
  });
417
477
  },
418
478
 
479
+ recordStagePendingPrompt(runId: string, stageId: string, prompt: PendingPrompt): boolean {
480
+ const run = findRun(runId);
481
+ if (!run) return false;
482
+ if (TERMINAL_STATUSES.has(run.status)) return false;
483
+ const stage = findStage(run, stageId);
484
+ if (!stage) return false;
485
+ if (isTerminalStageStatus(stage.status)) return false;
486
+ if (stage.pendingPrompt !== undefined) return false;
487
+ stage.pendingPrompt = { ...prompt };
488
+ stage.status = "awaiting_input";
489
+ stage.awaitingInputSince = prompt.createdAt;
490
+ _version++;
491
+ notify();
492
+ return true;
493
+ },
494
+
495
+ resolveStagePendingPrompt(
496
+ runId: string,
497
+ stageId: string,
498
+ promptId: string,
499
+ response: unknown,
500
+ ): boolean {
501
+ const run = findRun(runId);
502
+ if (!run) return false;
503
+ const stage = findStage(run, stageId);
504
+ if (!stage) return false;
505
+ const pending = stage.pendingPrompt;
506
+ if (!pending || pending.id !== promptId) return false;
507
+ stage.pendingPrompt = undefined;
508
+ if (stage.status === "awaiting_input") {
509
+ stage.status = "running";
510
+ delete stage.awaitingInputSince;
511
+ }
512
+ _version++;
513
+ notify();
514
+ const entry = _resolvers.get(promptId);
515
+ if (entry) {
516
+ _resolvers.delete(promptId);
517
+ entry.resolve(response);
518
+ }
519
+ return true;
520
+ },
521
+
522
+ awaitStagePendingPrompt(runId: string, stageId: string, promptId: string): Promise<unknown> {
523
+ return new Promise<unknown>((resolve, reject) => {
524
+ const run = findRun(runId);
525
+ if (!run) {
526
+ reject(new Error(`pi-workflows: run "${runId}" not found`));
527
+ return;
528
+ }
529
+ const stage = findStage(run, stageId);
530
+ if (!stage) {
531
+ reject(new Error(`pi-workflows: stage "${stageId}" not found on run "${runId}"`));
532
+ return;
533
+ }
534
+ const pending = stage.pendingPrompt;
535
+ if (!pending || pending.id !== promptId) {
536
+ reject(
537
+ new Error(
538
+ `pi-workflows: pending prompt "${promptId}" not registered on stage "${stageId}" in run "${runId}"`,
539
+ ),
540
+ );
541
+ return;
542
+ }
543
+ _resolvers.set(promptId, { promptId, resolve, reject });
544
+ });
545
+ },
546
+
419
547
  recordStageSession(
420
548
  runId: string,
421
549
  stageId: string,
@@ -480,7 +608,7 @@ export function createStore(): Store {
480
608
  if (TERMINAL_STATUSES.has(run.status)) return false;
481
609
  const stage = findStage(run, stageId);
482
610
  if (!stage) return false;
483
- if (stage.status === "completed" || stage.status === "failed" || stage.status === "paused" || stage.status === "blocked") return false;
611
+ if (cannotAwaitInput(stage.status)) return false;
484
612
 
485
613
  if (awaiting) {
486
614
  if (stage.status === "awaiting_input") return false;
@@ -502,7 +630,7 @@ export function createStore(): Store {
502
630
  if (TERMINAL_STATUSES.has(run.status)) return false;
503
631
  const stage = findStage(run, stageId);
504
632
  if (!stage) return false;
505
- if (stage.status === "completed" || stage.status === "failed" || stage.status === "paused") return false;
633
+ if (cannotBlock(stage.status)) return false;
506
634
  if (stage.status === "blocked") {
507
635
  if (stage.blockedByStageId === blockedBy) return false;
508
636
  stage.blockedByStageId = blockedBy;
@@ -547,7 +675,7 @@ export function createStore(): Store {
547
675
  if (TERMINAL_STATUSES.has(run.status)) return false;
548
676
  const stage = findStage(run, stageId);
549
677
  if (!stage) return false;
550
- if (stage.status === "paused" || stage.status === "blocked" || stage.status === "completed" || stage.status === "failed") return false;
678
+ if (cannotPause(stage.status)) return false;
551
679
  stage.status = "paused";
552
680
  stage.pausedAt = pausedAt ?? Date.now();
553
681
  stage.resumedAt = undefined;
@@ -1,5 +1,6 @@
1
1
  interface StageTimerSnapshot {
2
2
  readonly startedAt?: number;
3
+ readonly endedAt?: number;
3
4
  readonly durationMs?: number;
4
5
  readonly pausedDurationMs?: number;
5
6
  readonly pausedAt?: number;
@@ -7,6 +8,7 @@ interface StageTimerSnapshot {
7
8
 
8
9
  interface RunTimerSnapshot {
9
10
  readonly startedAt: number;
11
+ readonly endedAt?: number;
10
12
  readonly durationMs?: number;
11
13
  readonly pausedDurationMs?: number;
12
14
  readonly pausedAt?: number;
@@ -39,10 +41,12 @@ export function accumulatePausedDurationMs(
39
41
  export function elapsedStageMs(stage: StageTimerSnapshot, now = Date.now()): number | undefined {
40
42
  if (stage.durationMs !== undefined) return nonNegative(stage.durationMs);
41
43
  if (stage.startedAt === undefined) return undefined;
42
- return elapsedFromStart(stage.startedAt, now, stage.pausedDurationMs, stage.pausedAt);
44
+ const effectiveNow = stage.endedAt ?? now;
45
+ return elapsedFromStart(stage.startedAt, effectiveNow, stage.pausedDurationMs, stage.pausedAt);
43
46
  }
44
47
 
45
48
  export function elapsedRunMs(run: RunTimerSnapshot, now = Date.now()): number {
46
49
  if (run.durationMs !== undefined) return nonNegative(run.durationMs);
47
- return elapsedFromStart(run.startedAt, now, run.pausedDurationMs, run.pausedAt);
50
+ const effectiveNow = run.endedAt ?? now;
51
+ return elapsedFromStart(run.startedAt, effectiveNow, run.pausedDurationMs, run.pausedAt);
48
52
  }