@selesai/code 0.5.20 → 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 (263) 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-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -7,11 +7,13 @@ import {
7
7
  type AsyncJobState,
8
8
  type AsyncStartedEvent,
9
9
  type ControlEvent,
10
+ type SteeringNotice,
10
11
  type SubagentState,
11
12
  POLL_INTERVAL_MS,
12
13
  RESULTS_DIR,
13
14
  SUBAGENT_CONTROL_EVENT,
14
15
  SUBAGENT_CONTROL_INTERCOM_EVENT,
16
+ SUBAGENT_STEERING_NOTICE_EVENT,
15
17
  } from "../../shared/types.ts";
16
18
  import { readStatus } from "../../shared/utils.ts";
17
19
  import { normalizeParallelGroups } from "./parallel-groups.ts";
@@ -23,6 +25,7 @@ interface AsyncJobTrackerOptions {
23
25
  completionRetentionMs?: number;
24
26
  pollIntervalMs?: number;
25
27
  resultsDir?: string;
28
+ widgetEnabled?: boolean;
26
29
  kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
27
30
  now?: () => number;
28
31
  }
@@ -30,9 +33,20 @@ interface AsyncJobTrackerOptions {
30
33
  const CONTROL_EVENT_READ_CHUNK_BYTES = 64 * 1024;
31
34
  const MAX_CONTROL_EVENT_LINE_BYTES = 1024 * 1024;
32
35
  const CONTROL_EVENT_SCAN_WINDOW_BYTES = 2 * 1024 * 1024;
36
+ const MAX_RECENT_FLEET_JOBS = 20;
37
+
38
+ function rememberFleetJob(state: SubagentState, job: AsyncJobState): void {
39
+ state.fleetJobs ??= new Map();
40
+ state.fleetJobs.set(job.asyncId, job);
41
+ const terminal = [...state.fleetJobs.values()]
42
+ .filter((candidate) => candidate.status === "complete" || candidate.status === "failed" || candidate.status === "paused" || candidate.status === "stopped")
43
+ .sort((left, right) => (right.updatedAt ?? right.startedAt ?? 0) - (left.updatedAt ?? left.startedAt ?? 0));
44
+ for (const stale of terminal.slice(MAX_RECENT_FLEET_JOBS)) state.fleetJobs.delete(stale.asyncId);
45
+ }
33
46
 
34
47
  export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
35
48
  ensurePoller: () => void;
49
+ refreshWidget: (ctx: ExtensionContext) => void;
36
50
  handleStarted: (data: unknown) => void;
37
51
  handleComplete: (data: unknown) => void;
38
52
  resetJobs: (ctx?: ExtensionContext) => void;
@@ -41,10 +55,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
41
55
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
42
56
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
43
57
  const resultsDir = options.resultsDir ?? RESULTS_DIR;
58
+ const steeringNoticeSeen = new Map<string, number>();
44
59
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
45
- renderWidget(ctx, jobs);
60
+ renderWidget(ctx, options.widgetEnabled === false ? [] : jobs);
46
61
  ctx.ui.requestRender?.();
47
62
  };
63
+ const refreshWidget = (ctx: ExtensionContext) => rerenderWidget(ctx);
48
64
  const restoredControlEventCursor = (asyncDir: string) => {
49
65
  try {
50
66
  return fs.statSync(path.join(asyncDir, "events.jsonl")).size;
@@ -73,7 +89,10 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
73
89
  currentPath: run.currentPath,
74
90
  turnCount: run.turnCount,
75
91
  toolCount: run.toolCount,
92
+ steering: run.steering,
76
93
  mode: run.mode,
94
+ context: run.context,
95
+ cwd: run.cwd,
77
96
  agents: visibleSteps.map((step) => step.agent),
78
97
  currentStep: run.currentStep,
79
98
  chainStepCount: run.chainStepCount,
@@ -89,6 +108,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
89
108
  timeoutMs: run.timeoutMs,
90
109
  deadlineAt: run.deadlineAt,
91
110
  timedOut: run.timedOut,
111
+ stopped: run.stopped,
92
112
  turnBudget: run.turnBudget,
93
113
  turnBudgetExceeded: run.turnBudgetExceeded,
94
114
  wrapUpRequested: run.wrapUpRequested,
@@ -144,7 +164,24 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
144
164
  console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
145
165
  return;
146
166
  }
147
- if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") return;
167
+ if (!parsed || typeof parsed !== "object") return;
168
+ if ((parsed as { type?: unknown }).type === "subagent.steering.notice") {
169
+ const notice = parsed as Partial<SteeringNotice>;
170
+ if (typeof notice.requestId !== "string" || typeof notice.runId !== "string" || (notice.state !== "failed" && notice.state !== "partial" && notice.state !== "recovered") || typeof notice.message !== "string") return;
171
+ if (typeof state.currentSessionId === "string" && notice.currentSessionId !== state.currentSessionId) return;
172
+ const key = `${notice.runId}:${notice.requestId}:${notice.state}`;
173
+ if (steeringNoticeSeen.has(key)) return;
174
+ const now = Date.now();
175
+ steeringNoticeSeen.set(key, now);
176
+ if (steeringNoticeSeen.size > 200) {
177
+ for (const [seenKey, seenAt] of steeringNoticeSeen) {
178
+ if (now - seenAt > 10 * 60 * 1000 || steeringNoticeSeen.size > 200) steeringNoticeSeen.delete(seenKey);
179
+ }
180
+ }
181
+ pi.events.emit(SUBAGENT_STEERING_NOTICE_EVENT, { ...notice, source: "async", asyncDir: job.asyncDir, noticeText: notice.message });
182
+ return;
183
+ }
184
+ if ((parsed as { type?: unknown }).type !== "subagent.control") return;
148
185
  const record = parsed as { event?: ControlEvent; channels?: string[]; childIntercomTarget?: string; noticeText?: string; intercom?: { to?: string; message?: string } };
149
186
  if (!record.event || !Array.isArray(record.channels)) return;
150
187
  const payload = {
@@ -269,7 +306,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
269
306
  if (status) {
270
307
  const previousStatus = job.status;
271
308
  job.status = status.state;
272
- if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused") cancelCleanup(job.asyncId);
309
+ if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
273
310
  job.sessionId = status.sessionId ?? job.sessionId;
274
311
  job.activityState = status.activityState;
275
312
  job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
@@ -278,6 +315,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
278
315
  job.currentPath = status.currentPath;
279
316
  job.turnCount = status.turnCount ?? job.turnCount;
280
317
  job.toolCount = status.toolCount ?? job.toolCount;
318
+ job.steering = status.steering ?? job.steering;
281
319
  job.mode = status.mode;
282
320
  job.currentStep = status.currentStep ?? job.currentStep;
283
321
  job.chainStepCount = status.chainStepCount ?? job.chainStepCount;
@@ -308,12 +346,16 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
308
346
  job.timeoutMs = status.timeoutMs ?? job.timeoutMs;
309
347
  job.deadlineAt = status.deadlineAt ?? job.deadlineAt;
310
348
  job.timedOut = status.timedOut ?? job.timedOut;
349
+ job.stopped = status.stopped ?? job.stopped;
311
350
  job.turnBudget = status.turnBudget ?? job.turnBudget;
312
351
  job.turnBudgetExceeded = status.turnBudgetExceeded ?? job.turnBudgetExceeded;
313
352
  job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
314
353
  job.sessionFile = status.sessionFile ?? job.sessionFile;
315
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
316
- scheduleCleanup(job.asyncId);
354
+ if (job.status === "complete" || job.status === "failed" || job.status === "paused" || job.status === "stopped") {
355
+ rememberFleetJob(state, job);
356
+ if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
357
+ scheduleCleanup(job.asyncId);
358
+ }
317
359
  }
318
360
  if (widgetRenderKey(job) !== widgetStateBefore) widgetChanged = true;
319
361
  continue;
@@ -328,6 +370,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
328
370
  job.status = "failed";
329
371
  job.updatedAt = Date.now();
330
372
  }
373
+ rememberFleetJob(state, job);
331
374
  if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) {
332
375
  scheduleCleanup(job.asyncId);
333
376
  }
@@ -356,10 +399,12 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
356
399
  state.asyncJobs.set(info.id, {
357
400
  asyncId: info.id,
358
401
  asyncDir,
402
+ ...(typeof info.cwd === "string" ? { cwd: path.resolve(info.cwd) } : {}),
359
403
  status: "queued",
360
404
  pid: typeof info.pid === "number" ? info.pid : undefined,
361
405
  ...(typeof info.sessionId === "string" ? { sessionId: info.sessionId } : {}),
362
406
  mode: info.mode ?? (info.chain ? "chain" : "single"),
407
+ description: info.goal ?? info.task,
363
408
  agents,
364
409
  chainStepCount: info.chainStepCount,
365
410
  parallelGroups: validParallelGroups,
@@ -374,6 +419,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
374
419
  turnBudget: info.turnBudget,
375
420
  controlEventCursor: 0,
376
421
  });
422
+ rememberFleetJob(state, state.asyncJobs.get(info.id)!);
377
423
  ensurePoller();
378
424
  if (state.lastUiContext) {
379
425
  rerenderWidget(state.lastUiContext);
@@ -381,14 +427,15 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
381
427
  };
382
428
 
383
429
  const handleComplete = (data: unknown) => {
384
- const result = data as { id?: string; success?: boolean; asyncDir?: string; sessionId?: string };
430
+ const result = data as { id?: string; success?: boolean; state?: AsyncJobState["status"]; asyncDir?: string; sessionId?: string; stopped?: boolean };
385
431
  if (typeof state.currentSessionId === "string" && result.sessionId !== state.currentSessionId) return;
386
432
  const asyncId = result.id;
387
433
  if (!asyncId) return;
388
434
  const job = state.asyncJobs.get(asyncId);
389
435
  let nestedRefreshFailed = false;
390
436
  if (job) {
391
- job.status = result.success ? "complete" : "failed";
437
+ job.status = result.state ?? (result.success ? "complete" : "failed");
438
+ job.stopped = result.stopped ?? job.stopped;
392
439
  job.updatedAt = Date.now();
393
440
  if (result.asyncDir) job.asyncDir = result.asyncDir;
394
441
  try {
@@ -398,6 +445,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
398
445
  console.error(`Failed to refresh nested async descendants for '${job.asyncDir}':`, error);
399
446
  }
400
447
  }
448
+ if (job) rememberFleetJob(state, job);
401
449
  if (state.lastUiContext) {
402
450
  rerenderWidget(state.lastUiContext);
403
451
  }
@@ -410,6 +458,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
410
458
  }
411
459
  state.cleanupTimers.clear();
412
460
  state.asyncJobs.clear();
461
+ state.fleetJobs?.clear();
413
462
  state.foregroundControls?.clear();
414
463
  state.lastForegroundControlId = null;
415
464
  state.resultFileCoalescer.clear();
@@ -430,12 +479,14 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
430
479
  return;
431
480
  }
432
481
  for (const run of runs) {
433
- state.asyncJobs.set(run.id, summaryToJob(run));
482
+ const job = summaryToJob(run);
483
+ state.asyncJobs.set(run.id, job);
484
+ rememberFleetJob(state, job);
434
485
  }
435
486
  if (runs.length === 0) return;
436
487
  ensurePoller();
437
488
  if (state.lastUiContext?.hasUI) rerenderWidget(state.lastUiContext);
438
489
  };
439
490
 
440
- return { ensurePoller, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
491
+ return { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs };
441
492
  }
@@ -1,12 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SubagentState } from "../../shared/types.ts";
4
- import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
5
- import { deliverInterruptRequest } from "./control-channel.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type AcceptanceInput, type AsyncStatus, type SteeringRecoveryDescriptor } from "../../shared/types.ts";
4
+ import type { AgentConfig } from "../../agents/agents.ts";
5
+ import { validateAcceptanceInput } from "../shared/acceptance.ts";
6
+ import { validateToolBudgetConfig } from "../shared/tool-budget.ts";
7
+ import { intersectSubagentCapabilityCeilings, parseSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
8
+ import { resolveTurnBudgetConfig } from "../shared/turn-budget.ts";
6
9
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
7
10
 
8
- export const ASYNC_RESUME_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
9
-
10
11
  export interface AsyncResumeParams {
11
12
  id?: string;
12
13
  runId?: string;
@@ -23,6 +24,7 @@ export interface AsyncResumeDeps {
23
24
 
24
25
  export interface AsyncResumeOptions {
25
26
  requireSessionFile?: boolean;
27
+ sessionId?: string;
26
28
  }
27
29
 
28
30
  export type AsyncResumeTarget = {
@@ -32,49 +34,15 @@ export type AsyncResumeTarget = {
32
34
  state: AsyncStatus["state"];
33
35
  agent: string;
34
36
  index: number;
35
- intercomTarget: string;
36
37
  cwd?: string;
37
38
  sessionFile?: string;
39
+ model?: string;
40
+ thinking?: string;
41
+ recoveryDescriptor?: SteeringRecoveryDescriptor;
42
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
43
+ launchContractDigest?: string;
38
44
  };
39
45
 
40
- type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
41
-
42
- export function interruptLiveAsyncResumeTarget(input: {
43
- target: AsyncResumeTarget & { kind: "live" };
44
- state?: Pick<SubagentState, "asyncJobs">;
45
- kill?: KillFn;
46
- now?: () => number;
47
- resultsDir?: string;
48
- }): { ok: true; asyncId: string } | { ok: false; message: string } {
49
- const asyncId = input.target.runId;
50
- if (!input.target.asyncDir) {
51
- return { ok: false, message: `Async run ${asyncId} is live but does not have an async directory to interrupt.` };
52
- }
53
- const status = reconcileAsyncRun(input.target.asyncDir, { resultsDir: input.resultsDir, kill: input.kill, now: input.now }).status;
54
- if (!status || status.state !== "running" || typeof status.pid !== "number") {
55
- return { ok: false, message: `Async run ${asyncId} is live but no interrupt-capable runner pid was found.` };
56
- }
57
- try {
58
- deliverInterruptRequest({
59
- asyncDir: input.target.asyncDir,
60
- pid: status.pid,
61
- kill: input.kill,
62
- signal: ASYNC_RESUME_INTERRUPT_SIGNAL,
63
- now: input.now,
64
- source: "async-resume",
65
- });
66
- const tracked = input.state?.asyncJobs.get(asyncId);
67
- if (tracked) {
68
- tracked.activityState = undefined;
69
- tracked.updatedAt = input.now?.() ?? Date.now();
70
- }
71
- return { ok: true, asyncId };
72
- } catch (error) {
73
- const message = error instanceof Error ? error.message : String(error);
74
- return { ok: false, message: `Failed to interrupt async run ${asyncId}: ${message}` };
75
- }
76
- }
77
-
78
46
  interface AsyncResultFile {
79
47
  id?: string;
80
48
  runId?: string;
@@ -83,8 +51,13 @@ interface AsyncResultFile {
83
51
  state?: string;
84
52
  success?: boolean;
85
53
  cwd?: string;
54
+ sessionId?: string;
86
55
  sessionFile?: string;
87
- results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string }>;
56
+ model?: string;
57
+ thinking?: string;
58
+ launchContractDigest?: string;
59
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
60
+ results?: Array<{ agent?: string; success?: boolean; sessionFile?: string; intercomTarget?: string; model?: string; thinking?: string; launchContractDigest?: string; capabilityCeiling?: ResolvedSubagentCapabilityCeiling }>;
88
61
  }
89
62
 
90
63
  export interface AsyncRunLocation {
@@ -122,9 +95,13 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
122
95
  const agent = validateOptionalString(child, "agent", resultPath, `results[${index}].agent`);
123
96
  const sessionFile = validateOptionalString(child, "sessionFile", resultPath, `results[${index}].sessionFile`);
124
97
  const intercomTarget = validateOptionalString(child, "intercomTarget", resultPath, `results[${index}].intercomTarget`);
98
+ const model = validateOptionalString(child, "model", resultPath, `results[${index}].model`);
99
+ const thinking = validateOptionalString(child, "thinking", resultPath, `results[${index}].thinking`);
100
+ const launchContractDigest = validateOptionalString(child, "launchContractDigest", resultPath, `results[${index}].launchContractDigest`);
101
+ const capabilityCeiling = child.capabilityCeiling === undefined ? undefined : parseSubagentCapabilityCeiling(child.capabilityCeiling, `async result file '${resultPath}' results[${index}].capabilityCeiling`);
125
102
  const success = child.success;
126
103
  if (success !== undefined && typeof success !== "boolean") throw new Error(`Invalid async result file '${resultPath}': results[${index}].success must be a boolean.`);
127
- return { agent, sessionFile, intercomTarget, ...(typeof success === "boolean" ? { success } : {}) };
104
+ return { agent, sessionFile, intercomTarget, model, thinking, launchContractDigest, ...(capabilityCeiling ? { capabilityCeiling } : {}), ...(typeof success === "boolean" ? { success } : {}) };
128
105
  });
129
106
  }
130
107
  const success = data.success;
@@ -136,7 +113,12 @@ function validateResultFile(value: unknown, resultPath: string): AsyncResultFile
136
113
  mode: validateOptionalString(data, "mode", resultPath),
137
114
  state: validateOptionalString(data, "state", resultPath),
138
115
  cwd: validateOptionalString(data, "cwd", resultPath),
116
+ sessionId: validateOptionalString(data, "sessionId", resultPath),
139
117
  sessionFile: validateOptionalString(data, "sessionFile", resultPath),
118
+ model: validateOptionalString(data, "model", resultPath),
119
+ thinking: validateOptionalString(data, "thinking", resultPath),
120
+ launchContractDigest: validateOptionalString(data, "launchContractDigest", resultPath),
121
+ ...(data.capabilityCeiling === undefined ? {} : { capabilityCeiling: parseSubagentCapabilityCeiling(data.capabilityCeiling, `async result file '${resultPath}' capabilityCeiling`) }),
140
122
  ...(typeof success === "boolean" ? { success } : {}),
141
123
  ...(results ? { results } : {}),
142
124
  };
@@ -252,7 +234,7 @@ export function resolveAsyncRunLocation(params: AsyncResumeParams, asyncDirRoot:
252
234
  }
253
235
 
254
236
  function resultState(result: AsyncResultFile): AsyncStatus["state"] {
255
- if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "running" || result.state === "queued") {
237
+ if (result.state === "complete" || result.state === "failed" || result.state === "paused" || result.state === "stopped" || result.state === "running" || result.state === "queued") {
256
238
  return result.state;
257
239
  }
258
240
  return result.success ? "complete" : "failed";
@@ -264,16 +246,131 @@ function validateStatusForResume(status: AsyncStatus | null, source: string): vo
264
246
  if (status.sessionId !== undefined && typeof status.sessionId !== "string") throw new Error(`Invalid async status '${source}': sessionId must be a string.`);
265
247
  if (status.cwd !== undefined && typeof status.cwd !== "string") throw new Error(`Invalid async status '${source}': cwd must be a string.`);
266
248
  if (status.sessionFile !== undefined && typeof status.sessionFile !== "string") throw new Error(`Invalid async status '${source}': sessionFile must be a string.`);
249
+ if (status.capabilityCeiling !== undefined) status.capabilityCeiling = parseSubagentCapabilityCeiling(status.capabilityCeiling, `async status '${source}' capabilityCeiling`);
267
250
  if (status.steps !== undefined) {
268
251
  if (!Array.isArray(status.steps)) throw new Error(`Invalid async status '${source}': steps must be an array.`);
269
252
  status.steps.forEach((step, index) => {
270
253
  if (!step || typeof step !== "object" || Array.isArray(step)) throw new Error(`Invalid async status '${source}': steps[${index}] must be an object.`);
271
- if (typeof step.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
272
- if (step.sessionFile !== undefined && typeof step.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
254
+ const stepRecord = step as Record<string, unknown>;
255
+ if (typeof stepRecord.agent !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].agent must be a string.`);
256
+ if (stepRecord.sessionFile !== undefined && typeof stepRecord.sessionFile !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].sessionFile must be a string.`);
257
+ if (stepRecord.model !== undefined && typeof stepRecord.model !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].model must be a string.`);
258
+ if (stepRecord.thinking !== undefined && typeof stepRecord.thinking !== "string") throw new Error(`Invalid async status '${source}': steps[${index}].thinking must be a string.`);
259
+ if (stepRecord.capabilityCeiling !== undefined) stepRecord.capabilityCeiling = parseSubagentCapabilityCeiling(stepRecord.capabilityCeiling, `async status '${source}' steps[${index}].capabilityCeiling`);
273
260
  });
274
261
  }
275
262
  }
276
263
 
264
+ function normalizeRecoveryAcceptance(value: unknown, descriptorPath: string): AcceptanceInput | undefined {
265
+ if (value && typeof value === "object" && !Array.isArray(value) && ("explicit" in value || "inferredReason" in value)) {
266
+ const { explicit, inferredReason: _inferredReason, ...publicAcceptance } = value as Record<string, unknown>;
267
+ if (explicit === false) return undefined;
268
+ value = publicAcceptance;
269
+ }
270
+ const errors = validateAcceptanceInput(value, "recoveryDescriptor.acceptance");
271
+ if (errors.length) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${errors.join(" ")}`);
272
+ return value as AcceptanceInput;
273
+ }
274
+
275
+ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): SteeringRecoveryDescriptor | undefined {
276
+ if (!asyncDir) return undefined;
277
+ const descriptorPath = path.join(asyncDir, "recovery-descriptor.json");
278
+ if (!fs.existsSync(descriptorPath)) return undefined;
279
+ let value: unknown;
280
+ try {
281
+ value = JSON.parse(fs.readFileSync(descriptorPath, "utf-8"));
282
+ } catch (error) {
283
+ throw new Error(`Failed to parse async recovery descriptor '${descriptorPath}': ${getErrorMessage(error)}`, { cause: error instanceof Error ? error : undefined });
284
+ }
285
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
286
+ const parsed = value as Record<string, unknown>;
287
+ const allowedFields = new Set([
288
+ "version", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "fallbackModels", "thinking", "tools", "extensions",
289
+ "subagentOnlyExtensions", "mcpDirectTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritSkills", "skills",
290
+ "skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
291
+ "artifactsDir", "maxOutput", "controlConfig", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
292
+ ]);
293
+ for (const field of Object.keys(parsed)) {
294
+ if (!allowedFields.has(field)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': unknown field '${field}'.`);
295
+ }
296
+ const requiredStrings = ["sourceRunId", "agent", "cwd", "systemPromptMode", "outputMode"] as const;
297
+ for (const field of requiredStrings) {
298
+ if (typeof parsed[field] !== "string" || !(parsed[field] as string).trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
299
+ }
300
+ if (parsed.version !== 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': version must be 1.`);
301
+ if (parsed.capabilityCeiling !== undefined) parsed.capabilityCeiling = parseSubagentCapabilityCeiling(parsed.capabilityCeiling, `async recovery descriptor '${descriptorPath}' capabilityCeiling`);
302
+ if (parsed.agentContract !== undefined) {
303
+ if (!parsed.agentContract || typeof parsed.agentContract !== "object" || Array.isArray(parsed.agentContract)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be an object.`);
304
+ const contract = parsed.agentContract as Record<string, unknown>;
305
+ if (contract.version !== 1 || Object.keys(contract).some((key) => key !== "version")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': agentContract must be { version: 1 }.`);
306
+ }
307
+ if (parsed.systemPromptMode !== "append" && parsed.systemPromptMode !== "replace") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPromptMode is invalid.`);
308
+ if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
309
+ for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
310
+ if (typeof parsed[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a boolean.`);
311
+ }
312
+ if (!Number.isInteger(parsed.maxSubagentDepth) || (parsed.maxSubagentDepth as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxSubagentDepth must be a non-negative integer.`);
313
+ for (const field of ["fallbackModels", "tools", "extensions", "subagentOnlyExtensions", "mcpDirectTools", "skills", "skillPath"] as const) {
314
+ const item = parsed[field];
315
+ if (item !== undefined && (!Array.isArray(item) || item.some((entry) => typeof entry !== "string" || !entry.trim()))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must contain non-empty strings.`);
316
+ }
317
+ if (parsed.systemPrompt !== undefined && typeof parsed.systemPrompt !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': systemPrompt must be a string.`);
318
+ for (const field of ["sessionFile", "model", "thinking", "agentFilePath", "outputPath", "sessionDir", "artifactsDir"] as const) {
319
+ if (parsed[field] !== undefined && (typeof parsed[field] !== "string" || !(parsed[field] as string).trim())) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${field} must be a non-empty string.`);
320
+ }
321
+ if (parsed.completionGuard !== undefined && typeof parsed.completionGuard !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': completionGuard must be a boolean.`);
322
+ if (parsed.structuredOutputSchema !== undefined && (!parsed.structuredOutputSchema || typeof parsed.structuredOutputSchema !== "object" || Array.isArray(parsed.structuredOutputSchema))) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': structuredOutputSchema must be an object.`);
323
+ if (parsed.memory !== undefined) {
324
+ if (!parsed.memory || typeof parsed.memory !== "object" || Array.isArray(parsed.memory)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory must be an object.`);
325
+ const memory = parsed.memory as Record<string, unknown>;
326
+ if ((memory.scope !== "project" && memory.scope !== "user") || typeof memory.path !== "string" || !memory.path.trim()) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': memory is invalid.`);
327
+ }
328
+ if (parsed.absoluteDeadlineAt !== undefined && (!Number.isFinite(parsed.absoluteDeadlineAt) || (parsed.absoluteDeadlineAt as number) <= 0)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': absoluteDeadlineAt must be a positive timestamp.`);
329
+ if (parsed.initialTurnBudget !== undefined) {
330
+ const result = resolveTurnBudgetConfig(parsed.initialTurnBudget, "recoveryDescriptor.initialTurnBudget");
331
+ if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
332
+ }
333
+ if (parsed.initialToolBudget !== undefined) {
334
+ const result = validateToolBudgetConfig(parsed.initialToolBudget, "recoveryDescriptor.initialToolBudget");
335
+ if (result.error) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${result.error}`);
336
+ }
337
+ if (parsed.maxOutput !== undefined) {
338
+ if (!parsed.maxOutput || typeof parsed.maxOutput !== "object" || Array.isArray(parsed.maxOutput)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput must be an object.`);
339
+ for (const field of ["bytes", "lines"] as const) {
340
+ const item = (parsed.maxOutput as Record<string, unknown>)[field];
341
+ if (item !== undefined && (!Number.isInteger(item) || (item as number) < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': maxOutput.${field} must be a positive integer.`);
342
+ }
343
+ }
344
+ if (parsed.artifactConfig !== undefined) {
345
+ if (!parsed.artifactConfig || typeof parsed.artifactConfig !== "object" || Array.isArray(parsed.artifactConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig must be an object.`);
346
+ const artifact = parsed.artifactConfig as Record<string, unknown>;
347
+ for (const field of ["enabled", "includeInput", "includeOutput", "includeJsonl", "includeMetadata"] as const) {
348
+ if (typeof artifact[field] !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.${field} must be a boolean.`);
349
+ }
350
+ if (artifact.includeTranscript !== undefined && typeof artifact.includeTranscript !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.includeTranscript must be a boolean.`);
351
+ if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
352
+ }
353
+ if (parsed.controlConfig !== undefined) {
354
+ if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
355
+ const control = parsed.controlConfig as Record<string, unknown>;
356
+ if (typeof control.enabled !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.enabled must be a boolean.`);
357
+ for (const field of ["needsAttentionAfterMs", "activeNoticeAfterMs", "failedToolAttemptsBeforeAttention"] as const) {
358
+ if (!Number.isInteger(control[field]) || (control[field] as number) < 1) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
359
+ }
360
+ for (const field of ["activeNoticeAfterTurns", "activeNoticeAfterTokens"] as const) {
361
+ if (control[field] !== undefined && (!Number.isInteger(control[field]) || (control[field] as number) < 1)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.${field} must be a positive integer.`);
362
+ }
363
+ if (!Array.isArray(control.notifyOn) || control.notifyOn.some((item) => item !== "active_long_running" && item !== "needs_attention")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyOn is invalid.`);
364
+ if (!Array.isArray(control.notifyChannels) || control.notifyChannels.some((item) => item !== "event" && item !== "async" && item !== "intercom")) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig.notifyChannels is invalid.`);
365
+ }
366
+ if (parsed.acceptance !== undefined) {
367
+ const acceptance = normalizeRecoveryAcceptance(parsed.acceptance, descriptorPath);
368
+ if (acceptance === undefined) delete parsed.acceptance;
369
+ else parsed.acceptance = acceptance;
370
+ }
371
+ return parsed as unknown as SteeringRecoveryDescriptor;
372
+ }
373
+
277
374
  function validateResumeSessionFile(runId: string, sessionFile: string): string {
278
375
  if (path.extname(sessionFile) !== ".jsonl") throw new Error(`Async run '${runId}' session file must be a .jsonl file: ${sessionFile}`);
279
376
  const resolved = path.resolve(sessionFile);
@@ -295,10 +392,16 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
295
392
  : undefined;
296
393
  const status = reconciliation?.status ?? null;
297
394
  validateStatusForResume(status, location.asyncDir ? path.join(location.asyncDir, "status.json") : "status.json");
395
+ const recoveryDescriptor = readAsyncRecoveryDescriptor(location.asyncDir);
298
396
  const result = location.resultPath ? readResultFile(location.resultPath) : undefined;
299
397
  const runId = status?.runId ?? result?.runId ?? result?.id ?? location.resolvedId ?? (location.asyncDir ? path.basename(location.asyncDir) : "unknown");
398
+ if (options.sessionId && ((status && status.sessionId !== options.sessionId) || (result && result.sessionId !== options.sessionId))) {
399
+ throw new Error(`Async run '${runId}' was not found in the active session.`);
400
+ }
401
+ if (recoveryDescriptor && recoveryDescriptor.sourceRunId !== runId) throw new Error(`Async run '${runId}' has a recovery descriptor for a different source run.`);
300
402
  const state = status?.state ?? (result ? resultState(result) : undefined);
301
403
  if (!state) throw new Error(`Status file not found for async run '${runId}'.`);
404
+ if (state === "stopped") throw new Error(`Async run '${runId}' was stopped and cannot be resumed. Start a new run instead.`);
302
405
 
303
406
  const statusSteps = status?.steps ?? [];
304
407
  const resultSteps = result?.results ?? [];
@@ -312,6 +415,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
312
415
  if (requestedIndex < 0 || requestedIndex >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${requestedIndex} is out of range.`);
313
416
  const selectedStep = statusSteps[requestedIndex];
314
417
  if (selectedStep?.status === "running") {
418
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selectedStep.capabilityCeiling);
315
419
  return {
316
420
  kind: "live",
317
421
  runId,
@@ -319,9 +423,13 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
319
423
  state,
320
424
  agent: selectedStep.agent,
321
425
  index: requestedIndex,
322
- intercomTarget: resolveSubagentIntercomTarget(runId, selectedStep.agent, requestedIndex),
323
426
  cwd: status?.cwd ?? result?.cwd,
324
427
  sessionFile: selectedStep.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
428
+ model: selectedStep.model,
429
+ thinking: selectedStep.thinking,
430
+ launchContractDigest: selectedStep.launchContractDigest ?? result?.results?.[requestedIndex]?.launchContractDigest ?? result?.launchContractDigest,
431
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
432
+ ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
325
433
  };
326
434
  }
327
435
  if (selectedStep?.status === "pending") throw new Error(`Async run '${runId}' child ${requestedIndex} is pending and has not started yet. Wait for it to run or complete before resuming.`);
@@ -334,6 +442,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
334
442
  if (!selected) {
335
443
  throw new Error(`Async run '${runId}' has ${running.length} running children. Provide index to choose one.`);
336
444
  }
445
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, selected.step.capabilityCeiling);
337
446
  return {
338
447
  kind: "live",
339
448
  runId,
@@ -341,9 +450,13 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
341
450
  state,
342
451
  agent: selected.step.agent,
343
452
  index: selected.index,
344
- intercomTarget: resolveSubagentIntercomTarget(runId, selected.step.agent, selected.index),
345
453
  cwd: status?.cwd ?? result?.cwd,
346
454
  sessionFile: selected.step.sessionFile ?? status?.sessionFile ?? result?.sessionFile,
455
+ model: selected.step.model,
456
+ thinking: selected.step.thinking,
457
+ launchContractDigest: selected.step.launchContractDigest ?? result?.results?.[selected.index]?.launchContractDigest ?? result?.launchContractDigest,
458
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
459
+ ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
347
460
  };
348
461
  }
349
462
  }
@@ -356,11 +469,15 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
356
469
  if (index < 0 || index >= stepCount) throw new Error(`Async run '${runId}' has ${stepCount} children. Index ${index} is out of range.`);
357
470
  const agent = statusSteps[index]?.agent ?? resultSteps[index]?.agent ?? result?.agent;
358
471
  if (!agent) throw new Error(`Could not determine child agent for async run '${runId}'.`);
472
+ if (recoveryDescriptor && recoveryDescriptor.agent !== agent) throw new Error(`Async run '${runId}' has a recovery descriptor for '${recoveryDescriptor.agent}', not '${agent}'.`);
359
473
  const sessionFile = statusSteps[index]?.sessionFile
360
474
  ?? resultSteps[index]?.sessionFile
361
475
  ?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : undefined);
362
476
  if (!sessionFile && requireSessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
363
477
  const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
478
+ const stepModel = statusSteps[index]?.model ?? resultSteps[index]?.model ?? (stepCount === 1 ? result?.model : undefined);
479
+ const stepThinking = statusSteps[index]?.thinking ?? resultSteps[index]?.thinking ?? (stepCount === 1 ? result?.thinking : undefined);
480
+ const capabilityCeiling = intersectSubagentCapabilityCeilings(status?.capabilityCeiling, statusSteps[index]?.capabilityCeiling, result?.capabilityCeiling, resultSteps[index]?.capabilityCeiling);
364
481
 
365
482
  return {
366
483
  kind: "revive",
@@ -369,9 +486,38 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
369
486
  state,
370
487
  agent,
371
488
  index,
372
- intercomTarget: resolveSubagentIntercomTarget(runId, agent, index),
373
489
  cwd: status?.cwd ?? result?.cwd,
374
490
  ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
491
+ ...(stepModel ? { model: stepModel } : {}),
492
+ ...(stepThinking ? { thinking: stepThinking } : {}),
493
+ launchContractDigest: statusSteps[index]?.launchContractDigest ?? resultSteps[index]?.launchContractDigest ?? result?.launchContractDigest,
494
+ ...(capabilityCeiling ? { capabilityCeiling } : {}),
495
+ ...(recoveryDescriptor ? { recoveryDescriptor } : {}),
496
+ };
497
+ }
498
+
499
+ export function applySteeringRecoveryAgentConfig(agentConfig: AgentConfig, descriptor: SteeringRecoveryDescriptor): AgentConfig {
500
+ return {
501
+ ...agentConfig,
502
+ model: descriptor.model,
503
+ fallbackModels: descriptor.fallbackModels ? [...descriptor.fallbackModels] : undefined,
504
+ thinking: descriptor.thinking,
505
+ tools: descriptor.tools ? [...descriptor.tools] : undefined,
506
+ extensions: descriptor.extensions ? [...descriptor.extensions] : undefined,
507
+ subagentOnlyExtensions: descriptor.subagentOnlyExtensions ? [...descriptor.subagentOnlyExtensions] : undefined,
508
+ mcpDirectTools: descriptor.mcpDirectTools ? [...descriptor.mcpDirectTools] : undefined,
509
+ systemPrompt: descriptor.systemPrompt,
510
+ systemPromptMode: descriptor.systemPromptMode,
511
+ inheritProjectContext: descriptor.inheritProjectContext,
512
+ inheritSkills: descriptor.inheritSkills,
513
+ skills: descriptor.skills ? [...descriptor.skills] : undefined,
514
+ skillPath: descriptor.skillPath ? [...descriptor.skillPath] : undefined,
515
+ filePath: descriptor.agentFilePath,
516
+ completionGuard: descriptor.completionGuard,
517
+ memory: descriptor.memory ? { ...descriptor.memory } : undefined,
518
+ output: descriptor.outputPath,
519
+ toolBudget: descriptor.initialToolBudget,
520
+ maxSubagentDepth: descriptor.maxSubagentDepth,
375
521
  };
376
522
  }
377
523