@selesai/code 0.2.1 → 0.3.1

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 (284) hide show
  1. package/dist/config.d.ts +4 -15
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -45
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +8 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/diagnostics.d.ts +1 -0
  10. package/dist/core/diagnostics.d.ts.map +1 -1
  11. package/dist/core/diagnostics.js.map +1 -1
  12. package/dist/core/model-registry.d.ts +4 -1
  13. package/dist/core/model-registry.d.ts.map +1 -1
  14. package/dist/core/model-registry.js +101 -51
  15. package/dist/core/model-registry.js.map +1 -1
  16. package/dist/core/package-manager.d.ts +10 -0
  17. package/dist/core/package-manager.d.ts.map +1 -1
  18. package/dist/core/package-manager.js +93 -8
  19. package/dist/core/package-manager.js.map +1 -1
  20. package/dist/core/package-manager.test.d.ts +2 -0
  21. package/dist/core/package-manager.test.d.ts.map +1 -0
  22. package/dist/core/package-manager.test.js +20 -0
  23. package/dist/core/package-manager.test.js.map +1 -0
  24. package/dist/core/resource-loader.d.ts +3 -0
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +10 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +1 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/defaults/models.json +64 -113
  32. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  33. package/dist/extensions/caveman/index.js +116 -0
  34. package/dist/extensions/caveman/package.json +8 -0
  35. package/dist/extensions/caveman/test/extension.test.js +185 -0
  36. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  37. package/dist/extensions/copy-turn.ts +2 -2
  38. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  39. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  40. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  41. package/dist/extensions/pi-intercom/config.ts +2 -2
  42. package/dist/extensions/pi-intercom/index.ts +2 -2
  43. package/dist/extensions/pi-intercom/package.json +2 -2
  44. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  45. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  46. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  47. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
  48. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  49. package/dist/extensions/pi-powerline-footer/index.ts +40 -47
  50. package/dist/extensions/pi-powerline-footer/package.json +2 -2
  51. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
  52. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
  53. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
  54. package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
  55. package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
  56. package/dist/extensions/pi-powerline-footer/types.ts +7 -7
  57. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  58. package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
  59. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  60. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  61. package/dist/extensions/pi-subagents/README.md +224 -52
  62. package/dist/extensions/pi-subagents/agents/architect.md +188 -0
  63. package/dist/extensions/pi-subagents/agents/builder.md +120 -0
  64. package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
  65. package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
  66. package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
  67. package/dist/extensions/pi-subagents/install.mjs +93 -0
  68. package/dist/extensions/pi-subagents/package.json +4 -5
  69. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  70. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  71. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  72. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  73. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  74. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  75. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  76. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  77. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  78. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  79. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  80. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  81. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  82. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  83. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  84. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  85. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  86. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  87. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  88. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  89. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  90. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  91. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  92. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  93. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  94. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  95. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  96. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  97. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  98. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  100. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  101. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  103. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  105. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  106. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  107. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  109. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  112. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  113. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  114. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  116. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  117. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  118. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  119. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  120. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  121. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  123. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
  126. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  128. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  129. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  133. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  134. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  135. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  136. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  137. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  138. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  139. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  140. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  141. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  142. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  143. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  144. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  145. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  146. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  147. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  148. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  149. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  150. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  151. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  152. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  153. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  154. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  155. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  156. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  157. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  158. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  159. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  160. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  161. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  162. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  163. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  164. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  165. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  166. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  167. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  168. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  169. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  170. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  171. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  172. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  173. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  174. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  175. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  176. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  177. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  178. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  179. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  180. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  181. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  182. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  183. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  184. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  185. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  186. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  187. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  188. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  189. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  190. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  191. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  192. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  193. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  194. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  195. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  196. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  197. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  198. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  199. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  200. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  201. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  202. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  203. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  204. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  205. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  206. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  207. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  208. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  209. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  210. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  211. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  212. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  213. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  215. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  216. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  217. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  218. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  222. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  223. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  224. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  225. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  226. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  227. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  228. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  229. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  230. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  231. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  232. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  233. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  234. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  235. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  236. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  237. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  238. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  239. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  240. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  241. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  242. package/dist/extensions/pi-web-agent/package.json +2 -2
  243. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
  244. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  245. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
  246. package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
  247. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  248. package/dist/extensions/question/helpers.ts +3 -3
  249. package/dist/extensions/question/index.ts +2 -2
  250. package/dist/extensions/question/navigation.ts +2 -2
  251. package/dist/extensions/question/package.json +2 -2
  252. package/dist/extensions/question/question-list.ts +2 -2
  253. package/dist/extensions/question/tests/question-list.test.ts +2 -2
  254. package/dist/extensions/question/tui-adapter.ts +2 -2
  255. package/dist/extensions/question/types.ts +2 -2
  256. package/dist/extensions/question/ui-protocol.ts +2 -2
  257. package/dist/extensions/rtk.ts +2 -2
  258. package/dist/extensions/tokenin-onboarding.ts +2 -2
  259. package/dist/extensions/undo.ts +1 -1
  260. package/dist/extensions/web-agent-onboarding.ts +3 -3
  261. package/dist/extensions/workflow/adapter.ts +16 -8
  262. package/dist/extensions/workflow/extension.ts +1 -1
  263. package/dist/extensions/workflow/package.json +1 -1
  264. package/dist/main.d.ts.map +1 -1
  265. package/dist/main.js +4 -0
  266. package/dist/main.js.map +1 -1
  267. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/assistant-message.js +2 -0
  269. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  270. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  271. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  272. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  273. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  274. package/dist/skills/caveman/SKILL.md +49 -0
  275. package/dist/skills/handoff-text/SKILL.md +13 -0
  276. package/dist/utils/thinking-tags.d.ts +7 -0
  277. package/dist/utils/thinking-tags.d.ts.map +1 -0
  278. package/dist/utils/thinking-tags.js +63 -0
  279. package/dist/utils/thinking-tags.js.map +1 -0
  280. package/docs/docs.json +4 -0
  281. package/docs/extensions.md +2 -0
  282. package/docs/settings.md +15 -0
  283. package/docs/shared-host-extensions.md +109 -0
  284. package/package.json +2 -2
@@ -0,0 +1,398 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { describe, it } from "node:test";
6
+ import { waitForSubagents, type WaitDeps } from "../../src/runs/background/wait.ts";
7
+ import type { SubagentState } from "../../src/shared/types.ts";
8
+
9
+ function writeStatus(asyncRoot: string, runId: string, state: string, extra: object = {}): void {
10
+ const dir = path.join(asyncRoot, runId);
11
+ fs.mkdirSync(dir, { recursive: true });
12
+ // Use a recent timestamp so the stale-run reconciler doesn't mark a live
13
+ // "running" fixture as failed for having a stale heartbeat.
14
+ const nowMs = Date.now();
15
+ fs.writeFileSync(
16
+ path.join(dir, "status.json"),
17
+ JSON.stringify({
18
+ runId,
19
+ mode: "single",
20
+ state,
21
+ startedAt: nowMs,
22
+ lastUpdate: nowMs,
23
+ steps: [{ agent: "worker", status: state }],
24
+ ...extra,
25
+ }),
26
+ "utf-8",
27
+ );
28
+ }
29
+
30
+ function makeState(sessionId: string | null): SubagentState {
31
+ return {
32
+ baseCwd: "",
33
+ currentSessionId: sessionId,
34
+ asyncJobs: new Map(),
35
+ foregroundControls: new Map(),
36
+ lastForegroundControlId: null,
37
+ cleanupTimers: new Map(),
38
+ lastUiContext: null,
39
+ poller: null,
40
+ completionSeen: new Map(),
41
+ watcher: null,
42
+ watcherRestartTimer: null,
43
+ resultFileCoalescer: { schedule: () => false, clear: () => {} },
44
+ } as SubagentState;
45
+ }
46
+
47
+ function textOf(result: { content: Array<{ type: string; text?: string }> }): string {
48
+ return result.content.map((c) => c.text ?? "").join("");
49
+ }
50
+
51
+ function baseDeps(root: string, state: SubagentState, overrides: Partial<WaitDeps> = {}): WaitDeps {
52
+ return {
53
+ state,
54
+ asyncDirRoot: path.join(root, "runs"),
55
+ resultsDir: path.join(root, "results"),
56
+ // Never probe real PIDs in tests — treat every recorded pid as alive so
57
+ // reconciliation doesn't flip a "running" fixture to failed.
58
+ kill: () => true,
59
+ pollIntervalMs: 250,
60
+ ...overrides,
61
+ };
62
+ }
63
+
64
+ describe("wait tool", () => {
65
+ it("returns immediately when there is nothing to wait for", async () => {
66
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-empty-"));
67
+ try {
68
+ const state = makeState("sess-1");
69
+ const result = await waitForSubagents({}, undefined, baseDeps(root, state));
70
+ assert.equal(result.isError, undefined);
71
+ assert.match(textOf(result), /nothing to wait for/i);
72
+ } finally {
73
+ fs.rmSync(root, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ it("with all:true, resolves once every active run reaches a terminal state", async () => {
78
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-resolve-"));
79
+ try {
80
+ const asyncRoot = path.join(root, "runs");
81
+ const state = makeState("sess-1");
82
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
83
+ writeStatus(asyncRoot, "run-b", "queued", { sessionId: "sess-1", pid: 999998 });
84
+
85
+ // Flip one run terminal on the first poll, the other on the second — so
86
+ // all:true must keep waiting past the first completion.
87
+ let polls = 0;
88
+ const sleep = async () => {
89
+ polls += 1;
90
+ if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
91
+ if (polls === 2) writeStatus(asyncRoot, "run-b", "failed", { sessionId: "sess-1" });
92
+ };
93
+
94
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
95
+ assert.equal(result.isError, undefined);
96
+ const text = textOf(result);
97
+ assert.match(text, /done/i);
98
+ assert.match(text, /1 complete/);
99
+ assert.match(text, /1 failed/);
100
+ assert.ok(polls >= 2, "all:true should wait for both completions");
101
+ } finally {
102
+ fs.rmSync(root, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ it("wakes when a run needs attention, not only on completion", async () => {
107
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-attn-"));
108
+ try {
109
+ const asyncRoot = path.join(root, "runs");
110
+ const state = makeState("sess-1");
111
+ // Two runs, all:true so it would normally block until both finish.
112
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
113
+ writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
114
+
115
+ // Neither completes; run-a flags needs_attention (blocked for a decision).
116
+ let polls = 0;
117
+ const sleep = async () => {
118
+ polls += 1;
119
+ if (polls === 1) {
120
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
121
+ }
122
+ };
123
+
124
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
125
+ assert.equal(result.isError, undefined);
126
+ const text = textOf(result);
127
+ assert.match(text, /need attention/i, "should report the attention run");
128
+ assert.match(text, /run-a/, "should name the attention run");
129
+ assert.ok(polls <= 2, `should break on attention promptly, polled ${polls}`);
130
+ } finally {
131
+ fs.rmSync(root, { recursive: true, force: true });
132
+ }
133
+ });
134
+
135
+ it("reports runs that already need attention before waiting starts", async () => {
136
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-initial-attn-"));
137
+ try {
138
+ const asyncRoot = path.join(root, "runs");
139
+ const state = makeState("sess-1");
140
+ writeStatus(asyncRoot, "run-blocked", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
141
+
142
+ let polls = 0;
143
+ const result = await waitForSubagents({}, undefined, baseDeps(root, state, {
144
+ sleep: async () => { polls += 1; },
145
+ }));
146
+
147
+ assert.equal(result.isError, undefined);
148
+ const text = textOf(result);
149
+ assert.doesNotMatch(text, /nothing to wait for/i);
150
+ assert.match(text, /need attention/i);
151
+ assert.match(text, /run-blocked/);
152
+ assert.equal(polls, 0, "initial attention should return without polling");
153
+ } finally {
154
+ fs.rmSync(root, { recursive: true, force: true });
155
+ }
156
+ });
157
+
158
+ it("by default returns as soon as the FIRST run finishes, leaving the rest in flight", async () => {
159
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-first-"));
160
+ try {
161
+ const asyncRoot = path.join(root, "runs");
162
+ const state = makeState("sess-1");
163
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
164
+ writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
165
+ writeStatus(asyncRoot, "run-c", "running", { sessionId: "sess-1", pid: 999997 });
166
+
167
+ // Only run-a finishes; b and c stay running forever.
168
+ let polls = 0;
169
+ const sleep = async () => {
170
+ polls += 1;
171
+ if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
172
+ };
173
+
174
+ const result = await waitForSubagents({}, undefined, baseDeps(root, state, { sleep }));
175
+ assert.equal(result.isError, undefined);
176
+ const text = textOf(result);
177
+ assert.match(text, /1 of 3 run\(s\) finished/);
178
+ assert.match(text, /1 complete/);
179
+ assert.match(text, /2 run\(s\) still in flight/);
180
+ // Must not have blocked on b and c: a bounded number of polls.
181
+ assert.ok(polls <= 2, `first-completion should return promptly, polled ${polls}`);
182
+ } finally {
183
+ fs.rmSync(root, { recursive: true, force: true });
184
+ }
185
+ });
186
+
187
+ it("only waits for runs belonging to the current session", async () => {
188
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-session-"));
189
+ try {
190
+ const asyncRoot = path.join(root, "runs");
191
+ const state = makeState("sess-1");
192
+ // A run from another session must be ignored.
193
+ writeStatus(asyncRoot, "run-other", "running", { sessionId: "sess-2", pid: 999999 });
194
+ const result = await waitForSubagents({}, undefined, baseDeps(root, state));
195
+ assert.equal(result.isError, undefined);
196
+ assert.match(textOf(result), /nothing to wait for/i);
197
+ } finally {
198
+ fs.rmSync(root, { recursive: true, force: true });
199
+ }
200
+ });
201
+
202
+ it("summarizes only runs that were active when waiting began", async () => {
203
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-summary-scope-"));
204
+ try {
205
+ const asyncRoot = path.join(root, "runs");
206
+ const state = makeState("sess-1");
207
+ writeStatus(asyncRoot, "old-complete", "complete", { sessionId: "sess-1" });
208
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
209
+
210
+ const sleep = async () => {
211
+ writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
212
+ };
213
+
214
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
215
+ assert.equal(result.isError, undefined);
216
+ const text = textOf(result);
217
+ assert.match(text, /1 complete/);
218
+ assert.doesNotMatch(text, /2 complete/);
219
+ } finally {
220
+ fs.rmSync(root, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ it("can target a single run by id prefix", async () => {
225
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-id-"));
226
+ try {
227
+ const asyncRoot = path.join(root, "runs");
228
+ const state = makeState("sess-1");
229
+ writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999999 });
230
+ writeStatus(asyncRoot, "run-beta", "running", { sessionId: "sess-1", pid: 999998 });
231
+
232
+ let polls = 0;
233
+ const sleep = async () => {
234
+ polls += 1;
235
+ // Only alpha finishes; beta stays running but we're not waiting on it.
236
+ if (polls === 1) writeStatus(asyncRoot, "run-alpha", "complete", { sessionId: "sess-1" });
237
+ };
238
+
239
+ const result = await waitForSubagents({ id: "run-al" }, undefined, baseDeps(root, state, { sleep }));
240
+ assert.equal(result.isError, undefined);
241
+ assert.match(textOf(result), /run "run-al".*done/is);
242
+ } finally {
243
+ fs.rmSync(root, { recursive: true, force: true });
244
+ }
245
+ });
246
+
247
+ it("rejects ambiguous id prefixes but lets exact ids win", async () => {
248
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-ambiguous-id-"));
249
+ try {
250
+ const asyncRoot = path.join(root, "runs");
251
+ const state = makeState("sess-1");
252
+ writeStatus(asyncRoot, "run", "running", { sessionId: "sess-1", pid: 999999 });
253
+ writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999998 });
254
+
255
+ const ambiguous = await waitForSubagents({ id: "ru" }, undefined, baseDeps(root, state));
256
+ assert.equal(ambiguous.isError, true);
257
+ assert.match(textOf(ambiguous), /Ambiguous async run id prefix "ru"/);
258
+ assert.match(textOf(ambiguous), /run-alpha/);
259
+
260
+ let polls = 0;
261
+ const exact = await waitForSubagents({ id: "run" }, undefined, baseDeps(root, state, {
262
+ sleep: async () => {
263
+ polls += 1;
264
+ writeStatus(asyncRoot, "run", "complete", { sessionId: "sess-1" });
265
+ },
266
+ }));
267
+
268
+ assert.equal(exact.isError, undefined);
269
+ assert.match(textOf(exact), /run "run".*done/is);
270
+ assert.equal(polls, 1);
271
+ } finally {
272
+ fs.rmSync(root, { recursive: true, force: true });
273
+ }
274
+ });
275
+
276
+ it("times out while runs are still active and reports them", async () => {
277
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-timeout-"));
278
+ try {
279
+ const asyncRoot = path.join(root, "runs");
280
+ const state = makeState("sess-1");
281
+ writeStatus(asyncRoot, "run-stuck", "running", { sessionId: "sess-1", pid: 999999 });
282
+
283
+ // Virtual clock that jumps past the timeout on the first sleep.
284
+ let clock = 0;
285
+ const now = () => clock;
286
+ const sleep = async (ms: number) => {
287
+ clock += ms + 10_000;
288
+ };
289
+
290
+ const result = await waitForSubagents({ timeoutMs: 5_000 }, undefined, baseDeps(root, state, { now, sleep }));
291
+ assert.equal(result.isError, true);
292
+ const text = textOf(result);
293
+ assert.match(text, /timed out/i);
294
+ assert.match(text, /run-stuck \(running\)/);
295
+ } finally {
296
+ fs.rmSync(root, { recursive: true, force: true });
297
+ }
298
+ });
299
+
300
+ it("resolves early when the turn is aborted", async () => {
301
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-abort-"));
302
+ try {
303
+ const asyncRoot = path.join(root, "runs");
304
+ const state = makeState("sess-1");
305
+ writeStatus(asyncRoot, "run-x", "running", { sessionId: "sess-1", pid: 999999 });
306
+
307
+ const controller = new AbortController();
308
+ const sleep = async () => {
309
+ controller.abort();
310
+ };
311
+
312
+ const result = await waitForSubagents({}, controller.signal, baseDeps(root, state, { sleep }));
313
+ assert.equal(result.isError, true);
314
+ assert.match(textOf(result), /aborted/i);
315
+ assert.match(textOf(result), /run-x \(running\)/);
316
+ } finally {
317
+ fs.rmSync(root, { recursive: true, force: true });
318
+ }
319
+ });
320
+
321
+ it("wakes immediately on an event bus emission instead of waiting the poll interval", async () => {
322
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-event-"));
323
+ try {
324
+ const asyncRoot = path.join(root, "runs");
325
+ const state = makeState("sess-1");
326
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
327
+
328
+ // Fake bus. Emitting on a wake channel should end wait's sleep early.
329
+ const handlers = new Map<string, Array<(d: unknown) => void>>();
330
+ const events = {
331
+ on(channel: string, handler: (d: unknown) => void) {
332
+ const list = handlers.get(channel) ?? [];
333
+ list.push(handler);
334
+ handlers.set(channel, list);
335
+ return () => {
336
+ const l = handlers.get(channel) ?? [];
337
+ handlers.set(channel, l.filter((h) => h !== handler));
338
+ };
339
+ },
340
+ emit(channel: string, data: unknown) {
341
+ for (const h of handlers.get(channel) ?? []) h(data);
342
+ },
343
+ };
344
+
345
+ // A real timer-based sleep with a LONG poll interval; if wait waited for
346
+ // the poll it would take ~10s. The event should wake it in ~10ms.
347
+ let sleepCalls = 0;
348
+ const realSleep = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
349
+ sleepCalls += 1;
350
+ const t = setTimeout(resolve, ms);
351
+ signal?.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
352
+ });
353
+
354
+ const startedAt = Date.now();
355
+ const p = waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
356
+ events,
357
+ pollIntervalMs: 10_000,
358
+ sleep: realSleep,
359
+ }));
360
+
361
+ // After a short delay, flip the run terminal and emit a completion event.
362
+ setTimeout(() => {
363
+ writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
364
+ events.emit("subagent:async-complete", { id: "run-a" });
365
+ }, 15);
366
+
367
+ const result = await p;
368
+ const elapsed = Date.now() - startedAt;
369
+ assert.equal(result.isError, undefined);
370
+ assert.match(textOf(result), /done/i);
371
+ assert.ok(elapsed < 5_000, `should wake via event, not the 10s poll; took ${elapsed}ms`);
372
+ assert.ok(sleepCalls >= 1, "poll-interval sleep still armed as fallback");
373
+ } finally {
374
+ fs.rmSync(root, { recursive: true, force: true });
375
+ }
376
+ });
377
+
378
+ it("still resolves via poll when no event bus is provided (fallback)", async () => {
379
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-nobus-"));
380
+ try {
381
+ const asyncRoot = path.join(root, "runs");
382
+ const state = makeState("sess-1");
383
+ writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
384
+ let polls = 0;
385
+ const sleep = async () => {
386
+ polls += 1;
387
+ if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
388
+ };
389
+ // No `events` in deps → pure poll path.
390
+ const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
391
+ assert.equal(result.isError, undefined);
392
+ assert.match(textOf(result), /done/i);
393
+ assert.ok(polls >= 1);
394
+ } finally {
395
+ fs.rmSync(root, { recursive: true, force: true });
396
+ }
397
+ });
398
+ });
@@ -0,0 +1,88 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { buildWidgetLines, widgetRenderKey } from "../../src/tui/render.ts";
4
+ import type { AsyncJobState, NestedRunSummary } from "../../src/shared/types.ts";
5
+
6
+ const theme = {
7
+ fg(_name: string, text: string): string { return text; },
8
+ bold(text: string): string { return text; },
9
+ };
10
+
11
+ function nested(id: string, parentRunId: string, state: NestedRunSummary["state"] = "running", extra: Partial<NestedRunSummary> = {}): NestedRunSummary {
12
+ return {
13
+ id,
14
+ parentRunId,
15
+ parentStepIndex: 0,
16
+ depth: 1,
17
+ path: [{ runId: parentRunId, stepIndex: 0 }],
18
+ state,
19
+ agent: id,
20
+ lastUpdate: 1_000,
21
+ ...extra,
22
+ };
23
+ }
24
+
25
+ function job(child: NestedRunSummary): AsyncJobState {
26
+ return {
27
+ asyncId: "root-run",
28
+ asyncDir: "/tmp/root-run",
29
+ status: "running",
30
+ mode: "single",
31
+ agents: ["owner"],
32
+ startedAt: 0,
33
+ updatedAt: 1_500,
34
+ steps: [{ index: 0, agent: "owner", status: "running", children: [child] }],
35
+ stepsTotal: 1,
36
+ nestedChildren: [child],
37
+ };
38
+ }
39
+
40
+ describe("nested widget rendering", () => {
41
+ it("uses aggregate lines when collapsed and full child rows when expanded", () => {
42
+ const child = nested("nested-reviewer", "root-run", "running", { currentTool: "read" });
43
+ const collapsed = buildWidgetLines([job(child)], theme as any, 120, false).join("\n");
44
+ assert.match(collapsed, /↳ \+1 nested run \(1 running\)/);
45
+ assert.doesNotMatch(collapsed, /nested-reviewer · running/);
46
+
47
+ const expanded = buildWidgetLines([job(child)], theme as any, 120, true).join("\n");
48
+ assert.match(expanded, /↳ . nested-reviewer · running · read/);
49
+ });
50
+
51
+ it("collapses descendants beyond the nested depth budget", () => {
52
+ const root = nested("nested-root", "root-run", "running", {
53
+ children: [nested("nested-child", "nested-root", "running", {
54
+ parentStepIndex: undefined,
55
+ children: [nested("nested-grandchild", "nested-child", "running", {
56
+ parentStepIndex: undefined,
57
+ children: [nested("nested-great-grandchild", "nested-grandchild")],
58
+ })],
59
+ })],
60
+ });
61
+ const expanded = buildWidgetLines([job(root)], theme as any, 160, true).join("\n");
62
+ assert.match(expanded, /nested-grandchild/);
63
+ assert.match(expanded, /\+1 nested run \(1 running\)/);
64
+ assert.doesNotMatch(expanded, /nested-great-grandchild · running/);
65
+ });
66
+
67
+ it("shows running descendants even after the parent step is complete", () => {
68
+ const child = nested("still-running", "root-run", "running");
69
+ const state = job(child);
70
+ state.status = "complete";
71
+ state.steps![0]!.status = "complete";
72
+ const expanded = buildWidgetLines([state], theme as any, 120, true).join("\n");
73
+ assert.match(expanded, /✓ Step 1\/1: owner · complete/);
74
+ assert.match(expanded, /↳ . still-running · running/);
75
+ });
76
+
77
+ it("degrades stale child summaries to id and state", () => {
78
+ const child = nested("missing-metadata", "root-run", "failed", { agent: undefined, error: "owner gone" });
79
+ const expanded = buildWidgetLines([job(child)], theme as any, 120, true).join("\n");
80
+ assert.match(expanded, /missing-metadata · failed · Failed · owner gone/);
81
+ });
82
+
83
+ it("rerenders when only nested state changes", () => {
84
+ const first = job(nested("nested-reviewer", "root-run", "running"));
85
+ const second = job(nested("nested-reviewer", "root-run", "complete"));
86
+ assert.notEqual(widgetRenderKey(first), widgetRenderKey(second));
87
+ });
88
+ });
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { describe, it } from "node:test";
6
+
7
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
8
+
9
+ function assertNestedPiSpawnHidesWindows(sourcePath: string): void {
10
+ const source = fs.readFileSync(path.join(projectRoot, sourcePath), "utf-8");
11
+ assert.match(
12
+ source,
13
+ /spawn\(spawnSpec\.command,\s*spawnSpec\.args,\s*\{[^}]*windowsHide:\s*true/s,
14
+ `${sourcePath} nested Pi spawn should set windowsHide: true`,
15
+ );
16
+ }
17
+
18
+ describe("nested child Pi process visibility", () => {
19
+ it("hides foreground child Pi process windows on Windows", () => {
20
+ assertNestedPiSpawnHidesWindows("src/runs/foreground/execution.ts");
21
+ });
22
+
23
+ it("hides background child Pi process windows on Windows", () => {
24
+ assertNestedPiSpawnHidesWindows("src/runs/background/subagent-runner.ts");
25
+ });
26
+ });
@@ -0,0 +1,134 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { buildWorkflowGraphSnapshot } from "../../src/runs/shared/workflow-graph.ts";
4
+
5
+ describe("workflow graph snapshots", () => {
6
+ it("maps sequential chains with phase, label, output name, and stable flat indexes", () => {
7
+ const graph = buildWorkflowGraphSnapshot({
8
+ runId: "run-1",
9
+ steps: [
10
+ { agent: "scout", task: "Scan", phase: "Research", label: "Find context", as: "context" },
11
+ { agent: "writer", task: "Use {outputs.context}", phase: "Synthesis", outputSchema: { type: "object" } },
12
+ ],
13
+ results: [{ exitCode: 0 }, { exitCode: 1, error: "bad output" }],
14
+ });
15
+
16
+ assert.equal(graph.nodes[0]?.id, "step-0");
17
+ assert.equal(graph.nodes[0]?.label, "Find context");
18
+ assert.equal(graph.nodes[0]?.flatIndex, 0);
19
+ assert.equal(graph.nodes[0]?.outputName, "context");
20
+ assert.equal(graph.nodes[1]?.structured, true);
21
+ assert.equal(graph.nodes[1]?.status, "failed");
22
+ assert.deepEqual(graph.phases, [
23
+ { title: "Research", nodeIds: ["step-0"] },
24
+ { title: "Synthesis", nodeIds: ["step-1"] },
25
+ ]);
26
+ });
27
+
28
+ it("maps a long alternating worker and reviewer chain without losing indexes", () => {
29
+ const steps = Array.from({ length: 40 }, (_, index) => ({
30
+ agent: index % 2 === 0 ? "worker" : "reviewer",
31
+ task: index === 0 ? "Start" : "{previous}",
32
+ }));
33
+ const results = steps.map(() => ({ exitCode: 0 }));
34
+ const graph = buildWorkflowGraphSnapshot({
35
+ runId: "run-long",
36
+ steps,
37
+ results,
38
+ currentFlatIndex: 37,
39
+ currentStepIndex: 37,
40
+ });
41
+
42
+ assert.equal(graph.nodes.length, 40);
43
+ assert.equal(graph.nodes[0]?.id, "step-0");
44
+ assert.equal(graph.nodes[0]?.flatIndex, 0);
45
+ assert.equal(graph.nodes[37]?.id, "step-37");
46
+ assert.equal(graph.nodes[37]?.agent, "reviewer");
47
+ assert.equal(graph.nodes[37]?.flatIndex, 37);
48
+ assert.equal(graph.nodes[39]?.id, "step-39");
49
+ assert.equal(graph.nodes[39]?.agent, "reviewer");
50
+ assert.equal(graph.nodes[39]?.flatIndex, 39);
51
+ assert.equal(graph.currentNodeId, "step-37");
52
+ });
53
+
54
+ it("maps parallel chain steps with stable group and child indexes", () => {
55
+ const graph = buildWorkflowGraphSnapshot({
56
+ runId: "run-2",
57
+ steps: [
58
+ { agent: "setup", task: "Setup" },
59
+ {
60
+ parallel: [
61
+ { agent: "reviewer", label: "Correctness", phase: "Review", as: "correctness" },
62
+ { agent: "security", label: "Security", phase: "Review", as: "security" },
63
+ ],
64
+ },
65
+ ],
66
+ currentFlatIndex: 2,
67
+ });
68
+
69
+ const group = graph.nodes[1];
70
+ assert.equal(group?.kind, "parallel-group");
71
+ assert.equal(group?.children?.[0]?.id, "step-1-agent-0");
72
+ assert.equal(group?.children?.[0]?.flatIndex, 1);
73
+ assert.equal(group?.children?.[1]?.flatIndex, 2);
74
+ assert.equal(group?.children?.[1]?.status, "running");
75
+ assert.equal(graph.currentNodeId, "step-1-agent-1");
76
+ assert.deepEqual(graph.phases, [{ title: "Review", nodeIds: ["step-1-agent-0", "step-1-agent-1"] }]);
77
+ });
78
+
79
+ it("marks partially completed parallel groups as running", () => {
80
+ const graph = buildWorkflowGraphSnapshot({
81
+ runId: "run-3",
82
+ steps: [{ parallel: [{ agent: "first" }, { agent: "second" }] }],
83
+ results: [{ exitCode: 0 }],
84
+ });
85
+
86
+ assert.equal(graph.nodes[0]?.status, "running");
87
+ assert.equal(graph.nodes[0]?.children?.[0]?.status, "completed");
88
+ assert.equal(graph.nodes[0]?.children?.[1]?.status, "pending");
89
+ });
90
+
91
+ it("summarizes mixed terminal parallel group statuses with explicit precedence", () => {
92
+ const failedThenPaused = buildWorkflowGraphSnapshot({
93
+ runId: "run-4",
94
+ steps: [{ parallel: [{ agent: "first" }, { agent: "second" }] }],
95
+ results: [{ exitCode: 1 }, { exitCode: 1, interrupted: true }],
96
+ });
97
+ const pausedThenFailed = buildWorkflowGraphSnapshot({
98
+ runId: "run-5",
99
+ steps: [{ parallel: [{ agent: "first" }, { agent: "second" }] }],
100
+ results: [{ exitCode: 1, interrupted: true }, { exitCode: 1 }],
101
+ });
102
+
103
+ assert.equal(failedThenPaused.nodes[0]?.status, "failed");
104
+ assert.equal(pausedThenFailed.nodes[0]?.status, "failed");
105
+ });
106
+
107
+ it("uses dynamic group status overrides for empty or aggregate-failure fanout states", () => {
108
+ const steps = [{
109
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
110
+ parallel: { agent: "reviewer", task: "Review {item}" },
111
+ collect: { as: "reviews" },
112
+ }];
113
+
114
+ const emptySkip = buildWorkflowGraphSnapshot({
115
+ runId: "run-dynamic-empty",
116
+ steps,
117
+ dynamicGroupStatuses: { 0: { status: "completed" } },
118
+ });
119
+ assert.equal(emptySkip.nodes[0]?.kind, "dynamic-parallel-group");
120
+ assert.equal(emptySkip.nodes[0]?.status, "completed");
121
+ assert.deepEqual(emptySkip.nodes[0]?.children, []);
122
+
123
+ const collectFailure = buildWorkflowGraphSnapshot({
124
+ runId: "run-dynamic-collect-fail",
125
+ steps,
126
+ dynamicChildren: { 0: [{ agent: "reviewer", flatIndex: 0, itemKey: "a" }] },
127
+ results: [{ exitCode: 0 }],
128
+ dynamicGroupStatuses: { 0: { status: "failed", error: "Collected output validation failed" } },
129
+ });
130
+ assert.equal(collectFailure.nodes[0]?.status, "failed");
131
+ assert.match(collectFailure.nodes[0]?.error ?? "", /Collected output validation failed/);
132
+ assert.equal(collectFailure.nodes[0]?.children?.[0]?.status, "completed");
133
+ });
134
+ });