@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
@@ -31,13 +31,15 @@ function writeJson(filePath: string, value: object): void {
31
31
  fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
32
32
  }
33
33
 
34
- function createRunningAsync(state: SubagentState, runId: string, options: { track?: boolean } = {}): string {
34
+ function createRunningAsync(state: SubagentState, runId: string, options: { track?: boolean; sessionId?: string; state?: "queued" | "running"; pid?: number } = {}): string {
35
35
  const asyncDir = path.join(ASYNC_DIR, runId);
36
+ const runState = options.state ?? "running";
36
37
  writeJson(path.join(asyncDir, "status.json"), {
37
38
  runId,
38
39
  mode: "single",
39
- state: "running",
40
- pid: 12345,
40
+ state: runState,
41
+ sessionId: options.sessionId ?? "session",
42
+ ...(options.pid !== undefined ? { pid: options.pid } : runState === "running" ? { pid: 12345 } : {}),
41
43
  cwd: os.tmpdir(),
42
44
  startedAt: 100,
43
45
  lastUpdate: Date.now(),
@@ -94,11 +96,13 @@ describe("async interrupt action", () => {
94
96
  const runId = `steer-disk-${Date.now().toString(36)}`;
95
97
  const asyncDir = createRunningAsync(state, runId, { track: false });
96
98
  try {
99
+ const controller = new AbortController();
100
+ setTimeout(() => controller.abort(), 10);
97
101
  const result = await executorWithKill(state, () => true)
98
- .execute("steer", { action: "steer", id: runId, message: "Focus on tests." }, new AbortController().signal, undefined, ctx());
102
+ .execute("steer", { action: "steer", id: runId, message: "Focus on tests." }, controller.signal, undefined, ctx());
99
103
 
100
104
  assert.equal(result.isError, undefined);
101
- assert.match(text(result), new RegExp(`Steering queued for async run ${runId}`));
105
+ assert.match(text(result), new RegExp(`Steering pending for async run ${runId}`));
102
106
  const requests = consumeSteerRequests(asyncDir);
103
107
  assert.equal(requests.length, 1);
104
108
  assert.equal(requests[0]?.message, "Focus on tests.");
@@ -114,11 +118,13 @@ describe("async interrupt action", () => {
114
118
  const runId = `steer-dir-${Date.now().toString(36)}`;
115
119
  const asyncDir = createRunningAsync(state, runId, { track: false });
116
120
  try {
121
+ const controller = new AbortController();
122
+ setTimeout(() => controller.abort(), 10);
117
123
  const result = await executorWithKill(state, () => true)
118
- .execute("steer", { action: "steer", dir: asyncDir, message: "Focus on validation." }, new AbortController().signal, undefined, ctx());
124
+ .execute("steer", { action: "steer", dir: asyncDir, message: "Focus on validation." }, controller.signal, undefined, ctx());
119
125
 
120
126
  assert.equal(result.isError, undefined);
121
- assert.match(text(result), new RegExp(`Steering queued for async run ${runId}`));
127
+ assert.match(text(result), new RegExp(`Steering pending for async run ${runId}`));
122
128
  const requests = consumeSteerRequests(asyncDir);
123
129
  assert.equal(requests.length, 1);
124
130
  assert.equal(requests[0]?.message, "Focus on validation.");
@@ -133,6 +139,7 @@ describe("async interrupt action", () => {
133
139
  const asyncDir = path.join(ASYNC_DIR, runId);
134
140
  writeJson(path.join(asyncDir, "status.json"), {
135
141
  runId,
142
+ sessionId: "session",
136
143
  mode: "chain",
137
144
  state: "running",
138
145
  pid: 12345,
@@ -158,6 +165,21 @@ describe("async interrupt action", () => {
158
165
  }
159
166
  });
160
167
 
168
+ it("rejects steering async runs outside the active session", async () => {
169
+ const state = createState();
170
+ const runId = `steer-other-session-${Date.now().toString(36)}`;
171
+ const asyncDir = createRunningAsync(state, runId, { track: false, sessionId: "other-session" });
172
+ try {
173
+ const result = await executorWithKill(state, () => true)
174
+ .execute("steer", { action: "steer", id: runId, message: "do not deliver" }, new AbortController().signal, undefined, ctx());
175
+ assert.equal(result.isError, true);
176
+ assert.match(text(result), /active session/);
177
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "steer-requests")), false);
178
+ } finally {
179
+ cleanup(runId, asyncDir);
180
+ }
181
+ });
182
+
161
183
  it("interrupts a running async run resolved from disk after in-memory tracking is gone", async () => {
162
184
  const state = createState();
163
185
  const runId = `interrupt-disk-${Date.now().toString(36)}`;
@@ -198,6 +220,79 @@ describe("async interrupt action", () => {
198
220
  }
199
221
  });
200
222
 
223
+ it("stops a running async run resolved from disk", async () => {
224
+ const state = createState();
225
+ state.currentSessionId = "session";
226
+ const runId = `stop-disk-${Date.now().toString(36)}`;
227
+ const asyncDir = createRunningAsync(state, runId, { track: false, sessionId: "session" });
228
+ try {
229
+ const kills: Array<{ pid: number; signal?: NodeJS.Signals | 0 }> = [];
230
+ const result = await executorWithKill(state, (pid, signal) => {
231
+ kills.push({ pid, signal });
232
+ return true;
233
+ }).execute("stop", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx());
234
+
235
+ assert.equal(result.isError, undefined);
236
+ assert.match(text(result), new RegExp(`Stop requested for async run ${runId}`));
237
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "stop.json")), true);
238
+ assert.deepEqual(kills, [{ pid: 12345, signal: 0 }]);
239
+ } finally {
240
+ cleanup(runId, asyncDir);
241
+ }
242
+ });
243
+
244
+ it("does not stop a different async run when the requested id is missing", async () => {
245
+ const state = createState();
246
+ state.currentSessionId = "session";
247
+ const runId = `stop-existing-${Date.now().toString(36)}`;
248
+ const asyncDir = createRunningAsync(state, runId, { sessionId: "session" });
249
+ try {
250
+ const result = await executorWithKill(state, () => true)
251
+ .execute("stop", { action: "stop", id: "missing-run" }, new AbortController().signal, undefined, ctx());
252
+
253
+ assert.equal(result.isError, true);
254
+ assert.match(text(result), /Run not found|No stoppable async run found in this session/);
255
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "stop.json")), false);
256
+ } finally {
257
+ cleanup(runId, asyncDir);
258
+ }
259
+ });
260
+
261
+ it("stops a queued async run by writing the portable request", async () => {
262
+ const state = createState();
263
+ state.currentSessionId = "session";
264
+ const runId = `stop-queued-${Date.now().toString(36)}`;
265
+ const asyncDir = createRunningAsync(state, runId, { track: false, sessionId: "session", state: "queued" });
266
+ try {
267
+ const result = await executorWithKill(state, () => {
268
+ throw new Error("queued stop should not signal a process");
269
+ }).execute("stop", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx());
270
+
271
+ assert.equal(result.isError, undefined);
272
+ assert.match(text(result), new RegExp(`Stop requested for async run ${runId}`));
273
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "stop.json")), true);
274
+ } finally {
275
+ cleanup(runId, asyncDir);
276
+ }
277
+ });
278
+
279
+ it("rejects stop for async runs outside the active session", async () => {
280
+ const state = createState();
281
+ state.currentSessionId = "session";
282
+ const runId = `stop-other-session-${Date.now().toString(36)}`;
283
+ const asyncDir = createRunningAsync(state, runId, { track: false, sessionId: "other-session" });
284
+ try {
285
+ const result = await executorWithKill(state, () => true)
286
+ .execute("stop", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx());
287
+
288
+ assert.equal(result.isError, true);
289
+ assert.match(text(result), /active session/);
290
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "stop.json")), false);
291
+ } finally {
292
+ cleanup(runId, asyncDir);
293
+ }
294
+ });
295
+
201
296
  it("does not report success for stale running status with a dead pid", async () => {
202
297
  const state = createState();
203
298
  const runId = `interrupt-esrch-${Date.now().toString(36)}`;
@@ -36,7 +36,249 @@ describe("async resume lookup", () => {
36
36
  assert.equal(target.agent, "worker");
37
37
  assert.equal(target.sessionFile, sessionFile);
38
38
  assert.equal(target.cwd, root);
39
- assert.equal(target.intercomTarget, "subagent-worker-run-abc-1");
39
+ } finally {
40
+ fs.rmSync(root, { recursive: true, force: true });
41
+ }
42
+ });
43
+
44
+ it("preserves and validates persisted capability ceilings for resume", () => {
45
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-ceiling-"));
46
+ try {
47
+ const asyncRoot = path.join(root, "runs");
48
+ const asyncDir = path.join(asyncRoot, "run-ceiling");
49
+ const resultsDir = path.join(root, "results");
50
+ const sessionFile = path.join(root, "session.jsonl");
51
+ fs.writeFileSync(sessionFile, "", "utf-8");
52
+ writeJson(path.join(asyncDir, "status.json"), {
53
+ runId: "run-ceiling", mode: "single", state: "complete", startedAt: 100, endedAt: 200, lastUpdate: 200, cwd: root,
54
+ capabilityCeiling: { version: 1, allowedTools: ["read", "write"], denyExtensions: true, sources: ["run"] },
55
+ steps: [{ agent: "worker", status: "complete", sessionFile, capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: false, sources: ["step"] } }],
56
+ });
57
+
58
+ const target = resolveAsyncResumeTarget({ id: "run-ceiling" }, { asyncDirRoot: asyncRoot, resultsDir });
59
+ assert.deepEqual(target.capabilityCeiling, { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["run", "step"] });
60
+
61
+ writeJson(path.join(asyncDir, "status.json"), {
62
+ runId: "run-ceiling", mode: "single", state: "complete", startedAt: 100, endedAt: 200, lastUpdate: 200, cwd: root,
63
+ capabilityCeiling: { version: 2, allowedTools: ["read"], denyExtensions: true, sources: ["run"] },
64
+ steps: [{ agent: "worker", status: "complete", sessionFile }],
65
+ });
66
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-ceiling" }, { asyncDirRoot: asyncRoot, resultsDir }), /capabilityCeiling version/);
67
+
68
+ fs.rmSync(asyncDir, { recursive: true, force: true });
69
+ writeJson(path.join(resultsDir, "run-ceiling.json"), {
70
+ runId: "run-ceiling", mode: "single", state: "complete", success: true, cwd: root,
71
+ capabilityCeiling: { version: 1, allowedTools: ["read", "grep"], denyExtensions: false, sources: ["result"] },
72
+ results: [{ agent: "worker", success: true, sessionFile, capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["result-step"] } }],
73
+ });
74
+ const resultOnly = resolveAsyncResumeTarget({ id: "run-ceiling" }, { asyncDirRoot: asyncRoot, resultsDir });
75
+ assert.deepEqual(resultOnly.capabilityCeiling, { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["result", "result-step"] });
76
+
77
+ writeJson(path.join(resultsDir, "run-ceiling.json"), {
78
+ runId: "run-ceiling", mode: "single", state: "complete", success: true, cwd: root,
79
+ results: [{ agent: "worker", success: true, sessionFile, capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: true } }],
80
+ });
81
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-ceiling" }, { asyncDirRoot: asyncRoot, resultsDir }), /capabilityCeiling sources/);
82
+ } finally {
83
+ fs.rmSync(root, { recursive: true, force: true });
84
+ }
85
+ });
86
+
87
+ it("rejects unknown, cross-run, and wrong-agent recovery descriptors", () => {
88
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-descriptor-"));
89
+ try {
90
+ const asyncRoot = path.join(root, "runs");
91
+ const asyncDir = path.join(asyncRoot, "run-descriptor");
92
+ const resultsDir = path.join(root, "results");
93
+ const sessionFile = path.join(root, "session.jsonl");
94
+ fs.writeFileSync(sessionFile, "", "utf-8");
95
+ writeJson(path.join(asyncDir, "status.json"), {
96
+ runId: "run-descriptor", mode: "single", state: "paused", startedAt: 100, lastUpdate: 200, cwd: root,
97
+ steps: [{ agent: "worker", status: "paused", sessionFile }],
98
+ });
99
+ const descriptor = {
100
+ version: 1, sourceRunId: "run-descriptor", agent: "worker", cwd: root, systemPromptMode: "replace",
101
+ inheritProjectContext: false, inheritSkills: false, outputMode: "inline", maxSubagentDepth: 2, share: false,
102
+ };
103
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), { ...descriptor, token: "must-not-be-accepted" });
104
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-descriptor" }, { asyncDirRoot: asyncRoot, resultsDir }), /unknown field 'token'/);
105
+
106
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
107
+ ...descriptor,
108
+ capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: true },
109
+ });
110
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-descriptor" }, { asyncDirRoot: asyncRoot, resultsDir }), /capabilityCeiling sources/);
111
+
112
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), { ...descriptor, sourceRunId: "another-run" });
113
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-descriptor" }, { asyncDirRoot: asyncRoot, resultsDir }), /different source run/);
114
+
115
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), { ...descriptor, agent: "another-agent" });
116
+ assert.throws(() => resolveAsyncResumeTarget({ id: "run-descriptor" }, { asyncDirRoot: asyncRoot, resultsDir }), /not 'worker'/);
117
+ } finally {
118
+ fs.rmSync(root, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ it("accepts legacy resolved acceptance metadata in recovery descriptors", () => {
123
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-acceptance-"));
124
+ try {
125
+ const asyncRoot = path.join(root, "runs");
126
+ const asyncDir = path.join(asyncRoot, "run-acceptance");
127
+ const resultsDir = path.join(root, "results");
128
+ const sessionFile = path.join(root, "session.jsonl");
129
+ fs.writeFileSync(sessionFile, "", "utf-8");
130
+ writeJson(path.join(asyncDir, "status.json"), {
131
+ runId: "run-acceptance", mode: "single", state: "paused", startedAt: 100, lastUpdate: 200, cwd: root,
132
+ steps: [{ agent: "worker", status: "paused", sessionFile }],
133
+ });
134
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
135
+ version: 1,
136
+ sourceRunId: "run-acceptance",
137
+ agent: "worker",
138
+ cwd: root,
139
+ systemPromptMode: "replace",
140
+ inheritProjectContext: false,
141
+ inheritSkills: false,
142
+ outputMode: "inline",
143
+ maxSubagentDepth: 2,
144
+ share: false,
145
+ acceptance: {
146
+ level: "attested",
147
+ explicit: true,
148
+ inferredReason: ["async write-capable or risky run"],
149
+ criteria: [{ id: "criterion-1", must: "Return evidence", evidence: ["manual-notes"], severity: "required" }],
150
+ evidence: ["manual-notes", "residual-risks"],
151
+ verify: [],
152
+ stopRules: [],
153
+ },
154
+ });
155
+
156
+ const target = resolveAsyncResumeTarget({ id: "run-acceptance" }, { asyncDirRoot: asyncRoot, resultsDir });
157
+
158
+ assert.equal(target.kind, "revive");
159
+ assert.deepEqual(target.recoveryDescriptor?.acceptance, {
160
+ level: "attested",
161
+ criteria: [{ id: "criterion-1", must: "Return evidence", evidence: ["manual-notes"], severity: "required" }],
162
+ evidence: ["manual-notes", "residual-risks"],
163
+ verify: [],
164
+ stopRules: [],
165
+ });
166
+ } finally {
167
+ fs.rmSync(root, { recursive: true, force: true });
168
+ }
169
+ });
170
+
171
+ it("rejects stale explicit reviewed acceptance metadata in recovery descriptors", () => {
172
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-reviewed-acceptance-"));
173
+ try {
174
+ const asyncRoot = path.join(root, "runs");
175
+ const asyncDir = path.join(asyncRoot, "run-reviewed-acceptance");
176
+ const resultsDir = path.join(root, "results");
177
+ const sessionFile = path.join(root, "session.jsonl");
178
+ fs.writeFileSync(sessionFile, "", "utf-8");
179
+ writeJson(path.join(asyncDir, "status.json"), {
180
+ runId: "run-reviewed-acceptance", mode: "single", state: "paused", startedAt: 100, lastUpdate: 200, cwd: root,
181
+ steps: [{ agent: "worker", status: "paused", sessionFile }],
182
+ });
183
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
184
+ version: 1,
185
+ sourceRunId: "run-reviewed-acceptance",
186
+ agent: "worker",
187
+ cwd: root,
188
+ systemPromptMode: "replace",
189
+ inheritProjectContext: false,
190
+ inheritSkills: false,
191
+ outputMode: "inline",
192
+ maxSubagentDepth: 2,
193
+ share: false,
194
+ acceptance: {
195
+ level: "reviewed",
196
+ explicit: true,
197
+ inferredReason: ["async write-capable or risky run"],
198
+ criteria: ["Return evidence"],
199
+ evidence: ["validation-output"],
200
+ verify: [],
201
+ review: { agent: "reviewer", required: true },
202
+ stopRules: [],
203
+ },
204
+ });
205
+
206
+ assert.throws(
207
+ () => resolveAsyncResumeTarget({ id: "run-reviewed-acceptance" }, { asyncDirRoot: asyncRoot, resultsDir }),
208
+ /achieved status.*acceptance\.review\.required/i,
209
+ );
210
+ } finally {
211
+ fs.rmSync(root, { recursive: true, force: true });
212
+ }
213
+ });
214
+
215
+ it("drops inferred legacy acceptance metadata from recovery descriptors", () => {
216
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-inferred-acceptance-"));
217
+ try {
218
+ const asyncRoot = path.join(root, "runs");
219
+ const asyncDir = path.join(asyncRoot, "run-inferred-acceptance");
220
+ const resultsDir = path.join(root, "results");
221
+ const sessionFile = path.join(root, "session.jsonl");
222
+ fs.writeFileSync(sessionFile, "", "utf-8");
223
+ writeJson(path.join(asyncDir, "status.json"), {
224
+ runId: "run-inferred-acceptance", mode: "single", state: "paused", startedAt: 100, lastUpdate: 200, cwd: root,
225
+ steps: [{ agent: "worker", status: "paused", sessionFile }],
226
+ });
227
+ writeJson(path.join(asyncDir, "recovery-descriptor.json"), {
228
+ version: 1,
229
+ sourceRunId: "run-inferred-acceptance",
230
+ agent: "worker",
231
+ cwd: root,
232
+ systemPromptMode: "replace",
233
+ inheritProjectContext: false,
234
+ inheritSkills: false,
235
+ outputMode: "inline",
236
+ maxSubagentDepth: 2,
237
+ share: false,
238
+ acceptance: {
239
+ level: "reviewed",
240
+ explicit: false,
241
+ inferredReason: ["async write-capable or risky run"],
242
+ criteria: [],
243
+ evidence: [],
244
+ verify: [],
245
+ review: { agent: "reviewer", required: true },
246
+ stopRules: [],
247
+ },
248
+ });
249
+
250
+ const target = resolveAsyncResumeTarget({ id: "run-inferred-acceptance" }, { asyncDirRoot: asyncRoot, resultsDir });
251
+
252
+ assert.equal(target.kind, "revive");
253
+ assert.equal(target.recoveryDescriptor?.acceptance, undefined);
254
+ } finally {
255
+ fs.rmSync(root, { recursive: true, force: true });
256
+ }
257
+ });
258
+
259
+ it("rejects stopped runs instead of reviving them", () => {
260
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-stopped-"));
261
+ try {
262
+ const asyncRoot = path.join(root, "runs");
263
+ const sessionFile = path.join(root, "session.jsonl");
264
+ fs.writeFileSync(sessionFile, "", "utf-8");
265
+ writeJson(path.join(asyncRoot, "run-stopped", "status.json"), {
266
+ runId: "run-stopped",
267
+ mode: "single",
268
+ state: "stopped",
269
+ stopped: true,
270
+ startedAt: 100,
271
+ endedAt: 200,
272
+ lastUpdate: 200,
273
+ cwd: root,
274
+ sessionFile,
275
+ steps: [{ agent: "worker", status: "stopped", stopped: true, sessionFile }],
276
+ });
277
+
278
+ assert.throws(
279
+ () => resolveAsyncResumeTarget({ id: "run-stopped" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") }),
280
+ /stopped and cannot be resumed/,
281
+ );
40
282
  } finally {
41
283
  fs.rmSync(root, { recursive: true, force: true });
42
284
  }
@@ -133,6 +375,40 @@ describe("async resume lookup", () => {
133
375
  }
134
376
  });
135
377
 
378
+ it("rejects malformed result model metadata", () => {
379
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-malformed-result-model-"));
380
+ try {
381
+ const resultsDir = path.join(root, "results");
382
+ writeJson(path.join(resultsDir, "run-result-model.json"), {
383
+ id: "run-result-model",
384
+ agent: "worker",
385
+ success: true,
386
+ state: "complete",
387
+ results: [{ agent: "worker", model: { id: "bad" } }],
388
+ });
389
+
390
+ assert.throws(
391
+ () => resolveAsyncResumeTarget({ id: "run-result-model" }, { asyncDirRoot: path.join(root, "runs"), resultsDir }, { requireSessionFile: false }),
392
+ /results\[0\].model must be a string/,
393
+ );
394
+
395
+ writeJson(path.join(resultsDir, "run-result-model.json"), {
396
+ id: "run-result-model",
397
+ agent: "worker",
398
+ success: true,
399
+ state: "complete",
400
+ results: [{ agent: "worker", thinking: { level: "high" } }],
401
+ });
402
+
403
+ assert.throws(
404
+ () => resolveAsyncResumeTarget({ id: "run-result-model" }, { asyncDirRoot: path.join(root, "runs"), resultsDir }, { requireSessionFile: false }),
405
+ /results\[0\].thinking must be a string/,
406
+ );
407
+ } finally {
408
+ fs.rmSync(root, { recursive: true, force: true });
409
+ }
410
+ });
411
+
136
412
  it("rejects malformed status session ids", () => {
137
413
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-malformed-session-id-"));
138
414
  try {
@@ -155,7 +431,41 @@ describe("async resume lookup", () => {
155
431
  }
156
432
  });
157
433
 
158
- it("returns a live intercom target for a running child", () => {
434
+ it("rejects malformed status model metadata", () => {
435
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-malformed-status-model-"));
436
+ try {
437
+ const asyncRoot = path.join(root, "runs");
438
+ writeJson(path.join(asyncRoot, "run-status-model", "status.json"), {
439
+ runId: "run-status-model",
440
+ mode: "single",
441
+ state: "running",
442
+ startedAt: 100,
443
+ steps: [{ agent: "worker", status: "running", model: { id: "bad" } }],
444
+ });
445
+
446
+ assert.throws(
447
+ () => resolveAsyncResumeTarget({ id: "run-status-model" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") }),
448
+ /steps\[0\].model must be a string/,
449
+ );
450
+
451
+ writeJson(path.join(asyncRoot, "run-status-model", "status.json"), {
452
+ runId: "run-status-model",
453
+ mode: "single",
454
+ state: "running",
455
+ startedAt: 100,
456
+ steps: [{ agent: "worker", status: "running", thinking: { level: "high" } }],
457
+ });
458
+
459
+ assert.throws(
460
+ () => resolveAsyncResumeTarget({ id: "run-status-model" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") }),
461
+ /steps\[0\].thinking must be a string/,
462
+ );
463
+ } finally {
464
+ fs.rmSync(root, { recursive: true, force: true });
465
+ }
466
+ });
467
+
468
+ it("returns live child metadata for a running child", () => {
159
469
  const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-live-"));
160
470
  try {
161
471
  const asyncRoot = path.join(root, "runs");
@@ -165,13 +475,97 @@ describe("async resume lookup", () => {
165
475
  state: "running",
166
476
  startedAt: 100,
167
477
  lastUpdate: 100,
168
- steps: [{ agent: "scout", status: "running" }],
478
+ steps: [{ agent: "scout", status: "running", model: "openai/gpt-4.1", thinking: "off" }],
169
479
  });
170
480
 
171
481
  const target = resolveAsyncResumeTarget({ id: "run-live" }, { asyncDirRoot: asyncRoot, resultsDir: path.join(root, "results") });
172
482
 
173
483
  assert.equal(target.kind, "live");
174
- assert.equal(target.intercomTarget, "subagent-scout-run-live-1");
484
+ assert.equal(target.model, "openai/gpt-4.1");
485
+ assert.equal(target.thinking, "off");
486
+ } finally {
487
+ fs.rmSync(root, { recursive: true, force: true });
488
+ }
489
+ });
490
+
491
+ it("enforces current-session ownership for status and result-only runs", () => {
492
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-ownership-"));
493
+ try {
494
+ const asyncRoot = path.join(root, "runs");
495
+ const resultsDir = path.join(root, "results");
496
+ writeJson(path.join(asyncRoot, "owned", "status.json"), {
497
+ runId: "owned",
498
+ sessionId: "session-a",
499
+ mode: "single",
500
+ state: "running",
501
+ startedAt: 100,
502
+ steps: [{ agent: "worker", status: "running" }],
503
+ });
504
+ writeJson(path.join(asyncRoot, "sessionless", "status.json"), {
505
+ runId: "sessionless",
506
+ mode: "single",
507
+ state: "running",
508
+ startedAt: 100,
509
+ steps: [{ agent: "worker", status: "running" }],
510
+ });
511
+ writeJson(path.join(resultsDir, "result-only.json"), {
512
+ id: "result-only",
513
+ sessionId: "session-a",
514
+ agent: "worker",
515
+ state: "complete",
516
+ success: true,
517
+ });
518
+ writeJson(path.join(asyncRoot, "mixed", "status.json"), {
519
+ runId: "mixed",
520
+ sessionId: "session-a",
521
+ mode: "single",
522
+ state: "complete",
523
+ startedAt: 100,
524
+ steps: [{ agent: "worker", status: "complete" }],
525
+ });
526
+ writeJson(path.join(resultsDir, "mixed.json"), {
527
+ id: "mixed",
528
+ agent: "worker",
529
+ state: "complete",
530
+ success: true,
531
+ });
532
+ writeJson(path.join(asyncRoot, "mixed-reverse", "status.json"), {
533
+ runId: "mixed-reverse",
534
+ mode: "single",
535
+ state: "complete",
536
+ startedAt: 100,
537
+ steps: [{ agent: "worker", status: "complete" }],
538
+ });
539
+ writeJson(path.join(resultsDir, "mixed-reverse.json"), {
540
+ id: "mixed-reverse",
541
+ sessionId: "session-a",
542
+ agent: "worker",
543
+ state: "complete",
544
+ success: true,
545
+ });
546
+
547
+ assert.equal(resolveAsyncResumeTarget({ id: "owned" }, { asyncDirRoot: asyncRoot, resultsDir }, { sessionId: "session-a" }).kind, "live");
548
+ assert.throws(
549
+ () => resolveAsyncResumeTarget({ id: "owned" }, { asyncDirRoot: asyncRoot, resultsDir }, { sessionId: "session-b" }),
550
+ /not found in the active session/,
551
+ );
552
+ assert.throws(
553
+ () => resolveAsyncResumeTarget({ id: "sessionless" }, { asyncDirRoot: asyncRoot, resultsDir }, { sessionId: "session-a" }),
554
+ /not found in the active session/,
555
+ );
556
+ assert.equal(resolveAsyncResumeTarget({ id: "result-only" }, { asyncDirRoot: asyncRoot, resultsDir }, { requireSessionFile: false, sessionId: "session-a" }).kind, "revive");
557
+ assert.throws(
558
+ () => resolveAsyncResumeTarget({ id: "result-only" }, { asyncDirRoot: asyncRoot, resultsDir }, { requireSessionFile: false, sessionId: "session-b" }),
559
+ /not found in the active session/,
560
+ );
561
+ assert.throws(
562
+ () => resolveAsyncResumeTarget({ id: "mixed" }, { asyncDirRoot: asyncRoot, resultsDir }, { requireSessionFile: false, sessionId: "session-a" }),
563
+ /not found in the active session/,
564
+ );
565
+ assert.throws(
566
+ () => resolveAsyncResumeTarget({ id: "mixed-reverse" }, { asyncDirRoot: asyncRoot, resultsDir }, { requireSessionFile: false, sessionId: "session-a" }),
567
+ /not found in the active session/,
568
+ );
175
569
  } finally {
176
570
  fs.rmSync(root, { recursive: true, force: true });
177
571
  }
@@ -247,7 +641,7 @@ describe("async resume lookup", () => {
247
641
  lastUpdate: 200,
248
642
  steps: [
249
643
  { agent: "a", status: "complete", sessionFile: firstSession },
250
- { agent: "b", status: "complete", sessionFile: secondSession },
644
+ { agent: "b", status: "complete", sessionFile: secondSession, model: "anthropic/claude-sonnet-4", thinking: "high" },
251
645
  ],
252
646
  });
253
647
 
@@ -260,6 +654,46 @@ describe("async resume lookup", () => {
260
654
  assert.equal(target.agent, "b");
261
655
  assert.equal(target.index, 1);
262
656
  assert.equal(target.sessionFile, secondSession);
657
+ assert.equal(target.model, "anthropic/claude-sonnet-4");
658
+ assert.equal(target.thinking, "high");
659
+ } finally {
660
+ fs.rmSync(root, { recursive: true, force: true });
661
+ }
662
+ });
663
+
664
+ it("resolves a completed result-only child with per-child model metadata", () => {
665
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-async-resume-result-only-"));
666
+ try {
667
+ const asyncRoot = path.join(root, "runs");
668
+ const resultsDir = path.join(root, "results");
669
+ const firstSession = path.join(root, "a.jsonl");
670
+ const secondSession = path.join(root, "b.jsonl");
671
+ fs.writeFileSync(firstSession, "", "utf-8");
672
+ fs.writeFileSync(secondSession, "", "utf-8");
673
+ writeJson(path.join(resultsDir, "run-result-only.json"), {
674
+ id: "run-result-only",
675
+ mode: "parallel",
676
+ success: true,
677
+ state: "complete",
678
+ cwd: root,
679
+ results: [
680
+ { agent: "a", success: true, sessionFile: firstSession, model: "openai/gpt-4.1" },
681
+ { agent: "b", success: true, sessionFile: secondSession, model: "anthropic/claude-sonnet-4", thinking: "high" },
682
+ ],
683
+ });
684
+
685
+ assert.throws(
686
+ () => resolveAsyncResumeTarget({ id: "run-result-only" }, { asyncDirRoot: asyncRoot, resultsDir }),
687
+ /Provide index to choose one/,
688
+ );
689
+ const target = resolveAsyncResumeTarget({ id: "run-result-only", index: 1 }, { asyncDirRoot: asyncRoot, resultsDir });
690
+ assert.equal(target.kind, "revive");
691
+ assert.equal(target.agent, "b");
692
+ assert.equal(target.index, 1);
693
+ assert.equal(target.cwd, root);
694
+ assert.equal(target.sessionFile, secondSession);
695
+ assert.equal(target.model, "anthropic/claude-sonnet-4");
696
+ assert.equal(target.thinking, "high");
263
697
  } finally {
264
698
  fs.rmSync(root, { recursive: true, force: true });
265
699
  }
@@ -272,7 +706,6 @@ describe("async resume lookup", () => {
272
706
  state: "complete",
273
707
  agent: "worker",
274
708
  index: 0,
275
- intercomTarget: "subagent-worker-run-old-1",
276
709
  sessionFile: "/tmp/session.jsonl",
277
710
  }, "What changed?");
278
711