@selesai/code 0.5.19 → 0.5.21

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 (265) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -2,16 +2,20 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { formatDuration, formatModelThinking, formatTokens, shortenPath } from "../../shared/formatters.ts";
4
4
  import { formatActivityLabel, formatParallelOutcome } from "../../shared/status-format.ts";
5
- import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
5
+ import { type ActivityState, type AsyncJobStep, type AsyncParallelGroupStatus, type AsyncStatus, type CostSummary, type NestedRunSummary, type SteeringStatus, type SubagentRunMode, type TokenUsage, type TurnBudgetState } from "../../shared/types.ts";
6
+ import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../shared/capability-ceiling.ts";
6
7
  import { readStatus } from "../../shared/utils.ts";
7
8
  import { attachRootChildrenToSteps, buildNestedRouteIndex, type NestedRoute, projectNestedEvents } from "../shared/nested-events.ts";
8
9
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
9
10
  import { flatToLogicalStepIndex, normalizeParallelGroups } from "./parallel-groups.ts";
11
+ import { contextModeLabel, summarizeContextModes, type ContextMode, type ContextSummary } from "../shared/context-mode.ts";
10
12
  import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-reconciler.ts";
13
+ import { readProcessTerminal, sanitizeProcessTerminal } from "./process-terminal.ts";
11
14
 
12
15
  interface AsyncRunStepSummary {
13
16
  index: number;
14
17
  agent: string;
18
+ context?: ContextMode;
15
19
  label?: string;
16
20
  phase?: string;
17
21
  outputName?: string;
@@ -27,8 +31,7 @@ interface AsyncRunStepSummary {
27
31
  recentOutput?: string[];
28
32
  turnCount?: number;
29
33
  toolCount?: number;
30
- steerCount?: number;
31
- lastSteerAt?: number;
34
+ steering?: SteeringStatus;
32
35
  durationMs?: number;
33
36
  tokens?: TokenUsage;
34
37
  totalCost?: CostSummary;
@@ -36,11 +39,22 @@ interface AsyncRunStepSummary {
36
39
  model?: string;
37
40
  thinking?: string;
38
41
  attemptedModels?: string[];
42
+ sessionFile?: string;
43
+ transcriptPath?: string;
39
44
  error?: string;
40
45
  timedOut?: boolean;
46
+ stopped?: boolean;
41
47
  turnBudget?: TurnBudgetState;
42
48
  turnBudgetExceeded?: boolean;
43
49
  wrapUpRequested?: boolean;
50
+ acceptance?: AsyncJobStep["acceptance"];
51
+ agentContract?: AsyncJobStep["agentContract"];
52
+ execution?: AsyncJobStep["execution"];
53
+ review?: AsyncJobStep["review"];
54
+ effects?: AsyncJobStep["effects"];
55
+ processTerminal?: AsyncJobStep["processTerminal"];
56
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
57
+ capabilityAudit?: SubagentCapabilityAudit;
44
58
  children?: NestedRunSummary[];
45
59
  }
46
60
 
@@ -48,7 +62,7 @@ export interface AsyncRunSummary {
48
62
  id: string;
49
63
  asyncDir: string;
50
64
  sessionId?: string;
51
- state: "queued" | "running" | "complete" | "failed" | "paused";
65
+ state: "queued" | "running" | "complete" | "failed" | "paused" | "stopped";
52
66
  error?: string;
53
67
  activityState?: ActivityState;
54
68
  lastActivityAt?: number;
@@ -57,9 +71,9 @@ export interface AsyncRunSummary {
57
71
  currentPath?: string;
58
72
  turnCount?: number;
59
73
  toolCount?: number;
60
- steerCount?: number;
61
- lastSteerAt?: number;
74
+ steering?: SteeringStatus;
62
75
  mode: SubagentRunMode;
76
+ context?: ContextSummary;
63
77
  cwd?: string;
64
78
  startedAt: number;
65
79
  lastUpdate?: number;
@@ -67,6 +81,7 @@ export interface AsyncRunSummary {
67
81
  timeoutMs?: number;
68
82
  deadlineAt?: number;
69
83
  timedOut?: boolean;
84
+ stopped?: boolean;
70
85
  turnBudget?: TurnBudgetState;
71
86
  turnBudgetExceeded?: boolean;
72
87
  wrapUpRequested?: boolean;
@@ -82,6 +97,9 @@ export interface AsyncRunSummary {
82
97
  sessionFile?: string;
83
98
  nestedChildren?: NestedRunSummary[];
84
99
  nestedWarnings?: string[];
100
+ processTerminal?: AsyncStatus["processTerminal"];
101
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
102
+ capabilityAudit?: SubagentCapabilityAudit;
85
103
  }
86
104
 
87
105
  interface AsyncRunListOptions {
@@ -92,6 +110,7 @@ interface AsyncRunListOptions {
92
110
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
93
111
  now?: () => number;
94
112
  reconcile?: boolean;
113
+ runId?: string;
95
114
  }
96
115
 
97
116
  function getErrorMessage(error: unknown): string {
@@ -117,6 +136,45 @@ function isAsyncRunDir(root: string, entry: string): boolean {
117
136
  }
118
137
  }
119
138
 
139
+ type TargetedAsyncRunResolution =
140
+ | { kind: "exact"; id: string }
141
+ | { kind: "scan" }
142
+ | { kind: "reject" };
143
+
144
+ /**
145
+ * Resolve an exact targeted run without following a run-directory symlink or
146
+ * accepting a path whose canonical location escaped the async root.
147
+ */
148
+ export function resolveTargetedAsyncRun(asyncDirRoot: string, id: string, sessionId?: string): TargetedAsyncRunResolution {
149
+ if (!id || id === "." || id === ".." || path.basename(id) !== id) return { kind: "reject" };
150
+ const asyncDir = path.join(asyncDirRoot, id);
151
+ let entryStat: fs.Stats;
152
+ try {
153
+ entryStat = fs.lstatSync(asyncDir);
154
+ } catch (error) {
155
+ if (isNotFoundError(error)) return { kind: "scan" };
156
+ throw new Error(`Failed to inspect async run path '${asyncDir}': ${getErrorMessage(error)}`, {
157
+ cause: error instanceof Error ? error : undefined,
158
+ });
159
+ }
160
+ if (!entryStat.isDirectory() || entryStat.isSymbolicLink()) return { kind: "reject" };
161
+ try {
162
+ const canonicalRoot = fs.realpathSync(asyncDirRoot);
163
+ const canonicalDir = fs.realpathSync(asyncDir);
164
+ if (canonicalDir !== canonicalRoot && !canonicalDir.startsWith(`${canonicalRoot}${path.sep}`)) return { kind: "reject" };
165
+ } catch (error) {
166
+ if (isNotFoundError(error)) return { kind: "reject" };
167
+ throw new Error(`Failed to resolve async run path '${asyncDir}': ${getErrorMessage(error)}`, {
168
+ cause: error instanceof Error ? error : undefined,
169
+ });
170
+ }
171
+ if (sessionId !== undefined) {
172
+ const status = readStatus(asyncDir);
173
+ if (status?.sessionId !== sessionId) return { kind: "scan" };
174
+ }
175
+ return { kind: "exact", id };
176
+ }
177
+
120
178
  function outputFileMtime(outputFile: string | undefined): number | undefined {
121
179
  if (!outputFile) return undefined;
122
180
  try {
@@ -144,6 +202,8 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
144
202
  throw new Error(`Invalid async status '${path.join(asyncDir, "status.json")}': sessionId must be a string.`);
145
203
  }
146
204
  const { activityState, lastActivityAt } = deriveAsyncActivityState(asyncDir, status);
205
+ const processTerminal = readProcessTerminal(asyncDir, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId })
206
+ ?? sanitizeProcessTerminal(status.processTerminal, { runId: status.runId, runnerProcessInstanceId: status.processTerminal?.runnerProcessInstanceId }, path.join(asyncDir, "status.json"));
147
207
  const steps = status.steps ?? [];
148
208
  const chainStepCount = status.chainStepCount ?? steps.length;
149
209
  const parallelGroups = normalizeParallelGroups(status.parallelGroups, steps.length, chainStepCount);
@@ -163,6 +223,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
163
223
  return {
164
224
  index,
165
225
  agent: step.agent,
226
+ ...(step.context ? { context: step.context } : {}),
166
227
  ...(step.label ? { label: step.label } : {}),
167
228
  ...(step.phase ? { phase: step.phase } : {}),
168
229
  ...(step.outputName ? { outputName: step.outputName } : {}),
@@ -178,8 +239,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
178
239
  ...(step.recentOutput ? { recentOutput: [...step.recentOutput] } : {}),
179
240
  ...(step.turnCount !== undefined ? { turnCount: step.turnCount } : {}),
180
241
  ...(step.toolCount !== undefined ? { toolCount: step.toolCount } : {}),
181
- ...(step.steerCount !== undefined ? { steerCount: step.steerCount } : {}),
182
- ...(step.lastSteerAt !== undefined ? { lastSteerAt: step.lastSteerAt } : {}),
242
+ ...(step.steering ? { steering: step.steering } : {}),
183
243
  ...(step.durationMs !== undefined ? { durationMs: step.durationMs } : {}),
184
244
  ...(step.tokens ? { tokens: step.tokens } : {}),
185
245
  ...(step.totalCost ? { totalCost: step.totalCost } : {}),
@@ -187,11 +247,23 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
187
247
  ...(step.model ? { model: step.model } : {}),
188
248
  ...(step.thinking ? { thinking: step.thinking } : {}),
189
249
  ...(step.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
250
+ ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
251
+ ...(step.transcriptPath ? { transcriptPath: step.transcriptPath } : {}),
190
252
  ...(step.error ? { error: step.error } : {}),
191
253
  ...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
254
+ ...(step.stopped !== undefined ? { stopped: step.stopped } : {}),
192
255
  ...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
193
256
  ...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
194
257
  ...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
258
+ ...(step.acceptance ? { acceptance: step.acceptance } : {}),
259
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
260
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
261
+ ...(step.execution ? { execution: step.execution } : {}),
262
+ ...(step.review ? { review: step.review } : {}),
263
+ ...(step.effects ? { effects: step.effects } : {}),
264
+ ...(step.processTerminal ? { processTerminal: sanitizeProcessTerminal(step.processTerminal, { runId: status.runId, runnerProcessInstanceId: step.processTerminal.runnerProcessInstanceId }, `${path.join(asyncDir, "status.json")} step ${index}`) } : {}),
265
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
266
+ ...(step.capabilityAudit ? { capabilityAudit: step.capabilityAudit } : {}),
195
267
  ...(step.children?.length ? { children: step.children } : {}),
196
268
  };
197
269
  });
@@ -209,9 +281,9 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
209
281
  currentPath: status.currentPath,
210
282
  turnCount: status.turnCount,
211
283
  toolCount: status.toolCount,
212
- steerCount: status.steerCount,
213
- lastSteerAt: status.lastSteerAt,
284
+ steering: status.steering,
214
285
  mode: status.mode,
286
+ ...(summarizeContextModes(summarizedSteps.map((step) => step.context)) ? { context: summarizeContextModes(summarizedSteps.map((step) => step.context)) } : {}),
215
287
  cwd: status.cwd,
216
288
  startedAt: status.startedAt,
217
289
  lastUpdate: status.lastUpdate,
@@ -219,6 +291,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
219
291
  ...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
220
292
  ...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
221
293
  ...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
294
+ ...(status.stopped !== undefined ? { stopped: status.stopped } : {}),
222
295
  ...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
223
296
  ...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
224
297
  ...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
@@ -229,6 +302,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
229
302
  steps: summarizedSteps,
230
303
  ...(nestedChildren.length ? { nestedChildren } : {}),
231
304
  ...(nestedWarnings.length ? { nestedWarnings } : {}),
305
+ ...(processTerminal ? { processTerminal } : {}),
306
+ ...(status.launchContractDigest ? { launchContractDigest: status.launchContractDigest } : {}),
307
+ ...(status.capabilityCeiling ? { capabilityCeiling: status.capabilityCeiling } : {}),
308
+ ...(status.capabilityAudit ? { capabilityAudit: status.capabilityAudit } : {}),
232
309
  ...(status.sessionDir ? { sessionDir: status.sessionDir } : {}),
233
310
  ...(status.outputFile ? { outputFile: status.outputFile } : {}),
234
311
  ...(status.totalTokens ? { totalTokens: status.totalTokens } : {}),
@@ -237,12 +314,17 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
237
314
  };
238
315
  }
239
316
 
317
+ export function summarizeAsyncStatus(asyncDir: string, status: AsyncStatus & { cwd?: string }): AsyncRunSummary {
318
+ return statusToSummary(asyncDir, status);
319
+ }
320
+
240
321
  function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
241
322
  const rank = (state: AsyncRunSummary["state"]): number => {
242
323
  switch (state) {
243
324
  case "running": return 0;
244
325
  case "queued": return 1;
245
326
  case "failed": return 2;
327
+ case "stopped": return 2;
246
328
  case "paused": return 2;
247
329
  case "complete": return 3;
248
330
  }
@@ -259,7 +341,19 @@ function sortRuns(runs: AsyncRunSummary[]): AsyncRunSummary[] {
259
341
  export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions = {}): AsyncRunSummary[] {
260
342
  let entries: string[];
261
343
  try {
262
- entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
344
+ if (options.runId !== undefined) {
345
+ const resolution = resolveTargetedAsyncRun(asyncDirRoot, options.runId, options.sessionId);
346
+ entries = resolution.kind === "exact"
347
+ ? [resolution.id]
348
+ : resolution.kind === "scan"
349
+ ? fs.readdirSync(asyncDirRoot).filter((entry) =>
350
+ (entry === options.runId || entry.startsWith(options.runId!))
351
+ && resolveTargetedAsyncRun(asyncDirRoot, entry, options.sessionId).kind === "exact"
352
+ )
353
+ : [];
354
+ } else {
355
+ entries = fs.readdirSync(asyncDirRoot).filter((entry) => isAsyncRunDir(asyncDirRoot, entry));
356
+ }
263
357
  } catch (error) {
264
358
  if (isNotFoundError(error)) return [];
265
359
  throw new Error(`Failed to list async runs in '${asyncDirRoot}': ${getErrorMessage(error)}`, {
@@ -294,11 +388,13 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
294
388
  if (options.sessionId && status.sessionId !== options.sessionId) continue;
295
389
  const nestedWarnings: string[] = [];
296
390
  let nestedRoute: NestedRoute | undefined;
297
- try {
298
- nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
299
- if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
300
- } catch (error) {
301
- nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
391
+ if (options.reconcile !== false) {
392
+ try {
393
+ nestedRoute = resolveNestedRoute(status.runId || path.basename(asyncDir));
394
+ if (nestedRoute) reconcileNestedAsyncDescendants(nestedRoute, { resultsDir: options.resultsDir, kill: options.kill, now: options.now });
395
+ } catch (error) {
396
+ nestedWarnings.push(`Nested status unavailable: ${getErrorMessage(error)}`);
397
+ }
302
398
  }
303
399
  const summary = statusToSummary(asyncDir, status, nestedWarnings, nestedRoute);
304
400
  runs.push(summary);
@@ -308,26 +404,27 @@ export function listAsyncRuns(asyncDirRoot: string, options: AsyncRunListOptions
308
404
  return options.limit !== undefined ? sorted.slice(0, options.limit) : sorted;
309
405
  }
310
406
 
311
- function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steerCount?: number; lastSteerAt?: number; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
407
+ function formatActivityFacts(input: { activityState?: ActivityState; lastActivityAt?: number; currentTool?: string; currentToolStartedAt?: number; currentPath?: string; turnCount?: number; toolCount?: number; steering?: SteeringStatus; turnBudget?: TurnBudgetState; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean }): string | undefined {
312
408
  const facts: string[] = [];
313
409
  if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
314
410
  else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
315
411
  if (input.currentPath) facts.push(shortenPath(input.currentPath));
316
412
  if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
317
413
  if (input.turnBudgetExceeded && input.turnBudget) facts.push(`turn budget exceeded ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
414
+ else if (input.turnBudget?.outcome === "termination-deferred") facts.push(`turn-budget termination deferred ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
318
415
  else if (input.wrapUpRequested && input.turnBudget) facts.push(`wrap-up requested ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}`);
319
416
  else if (input.turnBudget) facts.push(`turn budget ${input.turnBudget.turnCount}/${input.turnBudget.maxTurns}+${input.turnBudget.graceTurns}`);
320
417
  if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
321
- if (input.steerCount !== undefined) facts.push(`${input.steerCount} steers`);
322
- if (typeof input.lastSteerAt === "number" && Number.isFinite(input.lastSteerAt)) facts.push(`last steer ${new Date(input.lastSteerAt).toISOString()}`);
418
+ if (input.steering) facts.push(`steering ${input.steering.scheduled} scheduled, ${input.steering.pending} pending, ${input.steering.delivered} delivered, ${input.steering.failed} failed, ${input.steering.recovered} recovered`);
323
419
  const activity = formatActivityLabel(input.lastActivityAt, input.activityState);
324
420
  return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
325
421
  }
326
422
 
327
423
  function formatStepLine(step: AsyncRunStepSummary): string {
328
424
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
425
+ const context = contextModeLabel(step.context);
329
426
  const phase = step.phase ? `[${step.phase}] ` : "";
330
- const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
427
+ const parts = [`${step.index + 1}. ${phase}${display}${context ? ` ${context}` : ""}`, step.status];
331
428
  const activity = formatActivityFacts(step);
332
429
  if (activity) parts.push(activity);
333
430
  const modelThinking = formatModelThinking(step.model, step.thinking);
@@ -368,7 +465,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
368
465
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
369
466
  const activity = formatActivityFacts(run);
370
467
  const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
371
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel}${pending} | ${cwd}`;
468
+ const context = contextModeLabel(run.context);
469
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode}${context ? ` ${context}` : ""} | ${stepLabel}${pending} | ${cwd}`;
372
470
  }
373
471
 
374
472
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {
@@ -0,0 +1,67 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { snapshotBackgroundWork } from "../../api/background-work.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type Details, type SubagentState } from "../../shared/types.ts";
4
+ import { listAsyncRuns } from "./async-status.ts";
5
+ import { waitForSubagents, type SubagentWaitDeps, type SubagentWaitParams, type WaitEventBus } from "./subagent-wait.ts";
6
+
7
+ export const DEFAULT_AUTO_DRAIN_TIMEOUT_MS = 30 * 60 * 1000;
8
+
9
+ export interface AutoDrainDeps {
10
+ state: SubagentState;
11
+ events?: WaitEventBus;
12
+ timeoutMs?: number;
13
+ now?: () => number;
14
+ wait?: (
15
+ params: SubagentWaitParams,
16
+ signal: AbortSignal | undefined,
17
+ deps: SubagentWaitDeps,
18
+ ) => Promise<AgentToolResult<Details>>;
19
+ hasWork?: (sessionId: string, nowMs: number) => boolean;
20
+ }
21
+
22
+ function resultText(value: AgentToolResult<Details>): string {
23
+ return value.content.map((part) => part.type === "text" ? part.text : "").join(" ").trim();
24
+ }
25
+
26
+ function hasOutstandingWork(sessionId: string, nowMs: number): boolean {
27
+ const asyncRuns = listAsyncRuns(ASYNC_DIR, {
28
+ states: ["queued", "running"],
29
+ sessionId,
30
+ resultsDir: RESULTS_DIR,
31
+ now: () => nowMs,
32
+ });
33
+ return asyncRuns.length > 0 || snapshotBackgroundWork(sessionId, nowMs).items.length > 0;
34
+ }
35
+
36
+ /** Drain all work owned by the current headless session, including work added while draining. */
37
+ export async function drainOutstandingWork(deps: AutoDrainDeps): Promise<void> {
38
+ const sessionId = deps.state.currentSessionId;
39
+ if (!sessionId) throw new Error("Cannot auto-drain background work without an active session identity.");
40
+ const now = deps.now ?? Date.now;
41
+ const timeoutMs = deps.timeoutMs ?? DEFAULT_AUTO_DRAIN_TIMEOUT_MS;
42
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Auto-drain timeoutMs must be a positive finite number.");
43
+ const deadlineAt = now() + timeoutMs;
44
+ const hasWork = deps.hasWork ?? hasOutstandingWork;
45
+ const wait = deps.wait ?? waitForSubagents;
46
+
47
+ while (hasWork(sessionId, now())) {
48
+ const remainingMs = deadlineAt - now();
49
+ if (remainingMs <= 0) {
50
+ throw new Error(`Auto-drain timed out after ${timeoutMs}ms with background work still active in session '${sessionId}'.`);
51
+ }
52
+ const waitResult = await wait(
53
+ { all: true, timeoutMs: remainingMs },
54
+ undefined,
55
+ {
56
+ state: deps.state,
57
+ events: deps.events,
58
+ now,
59
+ stopOnAttention: false,
60
+ failOnFailedRuns: true,
61
+ },
62
+ );
63
+ if (waitResult.isError) {
64
+ throw new Error(`Auto-drain failed for session '${sessionId}': ${resultText(waitResult) || "subagent_wait returned an error without details"}.`);
65
+ }
66
+ }
67
+ }
@@ -131,6 +131,7 @@ export function consumeChainAppendRequests(asyncDir: string): ChainAppendRequest
131
131
  function statusStepForTask(task: RunnerSubagentStep): StatusStep {
132
132
  return {
133
133
  agent: task.agent,
134
+ ...(task.context ? { context: task.context } : {}),
134
135
  phase: task.phase,
135
136
  label: task.label,
136
137
  outputName: task.outputName,
@@ -151,6 +152,7 @@ function statusStepsForRunnerStep(step: RunnerStep): StatusStep[] {
151
152
  if (isDynamicRunnerGroup(step)) {
152
153
  return [{
153
154
  agent: `expand:${step.parallel.agent}`,
155
+ ...(step.parallel.context ? { context: step.parallel.context } : {}),
154
156
  phase: step.phase ?? step.parallel.phase,
155
157
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
156
158
  outputName: step.collect.as,
@@ -27,6 +27,7 @@ export interface ImportedAsyncRootResult {
27
27
  structuredOutputSchemaPath?: string;
28
28
  acceptance?: AcceptanceLedger;
29
29
  timedOut?: boolean;
30
+ stopped?: boolean;
30
31
  }
31
32
 
32
33
  interface AsyncResultFile {
@@ -35,12 +36,14 @@ interface AsyncResultFile {
35
36
  summary?: string;
36
37
  error?: string;
37
38
  timedOut?: boolean;
39
+ stopped?: boolean;
38
40
  results?: Array<{
39
41
  agent?: string;
40
42
  output?: string;
41
43
  error?: string;
42
44
  success?: boolean;
43
45
  timedOut?: boolean;
46
+ stopped?: boolean;
44
47
  sessionFile?: string;
45
48
  intercomTarget?: string;
46
49
  model?: string;
@@ -54,8 +57,8 @@ interface AsyncResultFile {
54
57
  }>;
55
58
  }
56
59
 
57
- const TERMINAL_STATES = new Set(["complete", "failed", "paused"]);
58
- const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused"]);
60
+ const TERMINAL_STATES = new Set(["complete", "failed", "paused", "stopped"]);
61
+ const TERMINAL_STEP_STATUSES = new Set(["complete", "completed", "failed", "paused", "stopped"]);
59
62
 
60
63
  function readResultFile(resultPath: string): AsyncResultFile | undefined {
61
64
  try {
@@ -79,11 +82,12 @@ function isTerminalStatus(status: AsyncStatus | null, index: number): boolean {
79
82
  return TERMINAL_STATES.has(status.state);
80
83
  }
81
84
 
82
- function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | undefined {
85
+ function resultState(result: AsyncResultFile | undefined, child: NonNullable<AsyncResultFile["results"]>[number] | undefined): "complete" | "failed" | "paused" | "stopped" | undefined {
83
86
  if (!result) return undefined;
87
+ if (child?.stopped === true) return "stopped";
84
88
  if (child?.success === true) return "complete";
85
- if (child?.success === false) return result.state === "paused" ? "paused" : "failed";
86
- if (result.state === "complete" || result.state === "failed" || result.state === "paused") return result.state;
89
+ if (child?.success === false) return result.state === "stopped" ? "stopped" : result.state === "paused" ? "paused" : "failed";
90
+ if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped") return result.state;
87
91
  if (result.success === true) return "complete";
88
92
  if (result.success === false) return "failed";
89
93
  return undefined;
@@ -92,7 +96,8 @@ function resultState(result: AsyncResultFile | undefined, child: NonNullable<Asy
92
96
  function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus, step: NonNullable<AsyncStatus["steps"]>[number] | undefined): ImportedAsyncRootResult {
93
97
  const agent = step?.agent ?? status.steps?.[root.index]?.agent ?? "subagent";
94
98
  const timedOut = step?.timedOut === true || status.timedOut === true;
95
- const message = step?.error ?? status.error ?? `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`;
99
+ const stopped = step?.stopped === true || status.stopped === true || status.state === "stopped";
100
+ const message = step?.error ?? status.error ?? (stopped ? "Subagent stopped by user." : `Attached async root ${root.runId} ended without a result file at ${root.resultPath}.`);
96
101
  return {
97
102
  agent,
98
103
  output: message,
@@ -100,6 +105,7 @@ function outputFromTerminalStatus(root: ImportedAsyncRoot, status: AsyncStatus,
100
105
  exitCode: 1,
101
106
  error: message,
102
107
  ...(timedOut ? { timedOut: true } : {}),
108
+ ...(stopped ? { stopped: true } : {}),
103
109
  ...(step?.sessionFile ?? status.sessionFile ? { sessionFile: step?.sessionFile ?? status.sessionFile } : {}),
104
110
  ...(step?.model ? { model: step.model } : {}),
105
111
  ...(step?.attemptedModels ? { attemptedModels: step.attemptedModels } : {}),
@@ -136,8 +142,9 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
136
142
  const agent = child?.agent ?? step?.agent ?? status?.steps?.[root.index]?.agent ?? "subagent";
137
143
  const output = child?.output ?? result.summary ?? "";
138
144
  const timedOut = child?.timedOut === true || step?.timedOut === true || result.timedOut === true || status?.timedOut === true;
139
- const success = state === "complete" && !timedOut;
140
- const error = child?.error ?? (success ? undefined : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
145
+ const stopped = child?.stopped === true || step?.stopped === true || result.stopped === true || status?.stopped === true || state === "stopped";
146
+ const success = state === "complete" && !timedOut && !stopped;
147
+ const error = child?.error ?? (success ? undefined : stopped ? "Subagent stopped by user." : result.error ?? result.summary ?? status?.error ?? `Attached async root ${root.runId} did not complete successfully.`);
141
148
  return {
142
149
  agent,
143
150
  output: success ? output : (output || error || ""),
@@ -145,6 +152,7 @@ function buildImportedResult(root: ImportedAsyncRoot, status: AsyncStatus | null
145
152
  exitCode: success ? 0 : 1,
146
153
  ...(error ? { error } : {}),
147
154
  ...(timedOut ? { timedOut: true } : {}),
155
+ ...(stopped ? { stopped: true } : {}),
148
156
  ...(child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile ? { sessionFile: child?.sessionFile ?? step?.sessionFile ?? status?.sessionFile } : {}),
149
157
  ...(child?.intercomTarget ? { intercomTarget: child.intercomTarget } : {}),
150
158
  ...(child?.model ?? step?.model ? { model: child?.model ?? step?.model } : {}),
@@ -89,8 +89,8 @@ export interface CompletionBatcher<T> {
89
89
  push(item: T): void;
90
90
  /** Emit any held items immediately as a single group. */
91
91
  flush(): void;
92
- /** Clear timers without emitting. */
93
- dispose(): void;
92
+ /** Clear timers and return items that were never emitted. */
93
+ dispose(): T[];
94
94
  }
95
95
 
96
96
  /**
@@ -109,7 +109,7 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
109
109
  options.emit([item]);
110
110
  },
111
111
  flush() {},
112
- dispose() {},
112
+ dispose() { return []; },
113
113
  };
114
114
  }
115
115
 
@@ -160,7 +160,9 @@ export function createCompletionBatcher<T>(options: CompletionBatcherOptions<T>)
160
160
  flush: emitGroup,
161
161
  dispose() {
162
162
  clearTimers();
163
+ const abandoned = pending;
163
164
  pending = [];
164
- },
165
+ return abandoned;
166
+ }
165
167
  };
166
168
  }
@@ -20,9 +20,9 @@ function asFiniteNumber(value: unknown): number | undefined {
20
20
  }
21
21
 
22
22
  export function buildCompletionKey(data: CompletionDataLike, fallback: string): string {
23
- const id = asNonEmptyString(data.id);
24
- if (id) return `id:${id}`;
25
23
  const sessionId = asNonEmptyString(data.sessionId) ?? "no-session";
24
+ const id = asNonEmptyString(data.id);
25
+ if (id) return `session:${sessionId}:id:${id}`;
26
26
  const agent = asNonEmptyString(data.agent) ?? "unknown";
27
27
  const timestamp = asFiniteNumber(data.timestamp);
28
28
  const taskIndex = asFiniteNumber(data.taskIndex);
@@ -52,12 +52,3 @@ export function markSeenWithTtl(seen: Map<string, number>, key: string, now: num
52
52
  seen.set(key, now);
53
53
  return false;
54
54
  }
55
-
56
- export function getGlobalSeenMap(storeKey: string): Map<string, number> {
57
- const globalStore = globalThis as Record<string, unknown>;
58
- const existing = globalStore[storeKey];
59
- if (existing instanceof Map) return existing as Map<string, number>;
60
- const map = new Map<string, number>();
61
- globalStore[storeKey] = map;
62
- return map;
63
- }