@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
@@ -3,7 +3,9 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { after, afterEach, before, beforeEach, describe, it } from "node:test";
6
- import { ASYNC_DIR, INTERCOM_DETACH_REQUEST_EVENT, RESULTS_DIR, SUBAGENT_ASYNC_STARTED_EVENT } from "../../src/shared/types.ts";
6
+ import { ASYNC_DIR, INTERCOM_DETACH_REQUEST_EVENT, RESULTS_DIR, SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_FOREGROUND_COMPLETE_EVENT } from "../../src/shared/types.ts";
7
+ import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
8
+ import { sessionLeaseDir } from "../../src/runs/shared/session-lease.ts";
7
9
  import type { MockPi } from "../support/helpers.ts";
8
10
  import {
9
11
  createMockPi,
@@ -21,7 +23,7 @@ interface ExecutorResult {
21
23
  details?: {
22
24
  mode?: string;
23
25
  runId?: string;
24
- results?: Array<{ agent?: string; finalOutput?: string }>;
26
+ results?: Array<{ agent?: string; finalOutput?: string; sessionFile?: string; acceptance?: { status?: string }; artifactPaths?: { metadataPath?: string } }>;
25
27
  asyncId?: string;
26
28
  };
27
29
  }
@@ -113,15 +115,25 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
113
115
  async function readMockCallArgs(index: number): Promise<string[]> {
114
116
  const deadline = Date.now() + 10_000;
115
117
  let callFile: string | undefined;
116
- while (!callFile) {
118
+ let parseError: SyntaxError | undefined;
119
+ while (Date.now() <= deadline) {
117
120
  callFile = fs.readdirSync(mockPi.dir)
118
121
  .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
119
122
  .sort()[index];
120
- if (callFile || Date.now() > deadline) break;
123
+ if (callFile) {
124
+ try {
125
+ return JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[];
126
+ } catch (error) {
127
+ if (!(error instanceof SyntaxError)) throw error;
128
+ parseError = error;
129
+ }
130
+ }
121
131
  await new Promise((resolve) => setTimeout(resolve, 50));
122
132
  }
123
- assert.ok(callFile, `expected mock pi call at index ${index}`);
124
- return JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[];
133
+ if (callFile && parseError) {
134
+ throw new Error(`Mock Pi call record '${callFile}' remained incomplete after 10s.`, { cause: parseError });
135
+ }
136
+ assert.fail(`expected mock pi call at index ${index}`);
125
137
  }
126
138
 
127
139
  async function waitForFile(filePath: string, timeoutMs = 10_000): Promise<void> {
@@ -132,6 +144,14 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
132
144
  }
133
145
  }
134
146
 
147
+ async function waitForMissingPath(filePath: string, timeoutMs = 10_000): Promise<void> {
148
+ const deadline = Date.now() + timeoutMs;
149
+ while (fs.existsSync(filePath)) {
150
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for path cleanup: ${filePath}`);
151
+ await new Promise((resolve) => setTimeout(resolve, 50));
152
+ }
153
+ }
154
+
135
155
  function makeExecutor(options: { bridgeMode?: "always" | "off"; agents?: ReturnType<typeof makeAgent>[]; acknowledgeResults?: boolean; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean } = {}) {
136
156
  const events = createRecordingEventBus({ acknowledgeResults: options.acknowledgeResults ?? true });
137
157
  const state = {
@@ -317,12 +337,12 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
317
337
 
318
338
  assert.equal(detachEmitted, true);
319
339
  assert.match(result.content[0]?.text ?? "", /Chain detached for intercom coordination/);
320
- assert.doesNotMatch(result.content[0]?.text ?? "", /resume/);
340
+ assert.match(result.content[0]?.text ?? "", /do not resume or launch a replacement/);
321
341
  assert.equal(bus.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
322
342
  assert.equal(mockPi.callCount(), 1);
323
343
  });
324
344
 
325
- it("resume action sends a follow-up to a live async child when the target is registered", async () => {
345
+ it("resume action rejects a live async child without side effects", async () => {
326
346
  const runId = `resume-live-${Date.now()}`;
327
347
  const asyncDir = path.join(ASYNC_DIR, runId);
328
348
  const kills: Array<{ pid: number; signal?: NodeJS.Signals | 0 }> = [];
@@ -330,6 +350,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
330
350
  fs.mkdirSync(asyncDir, { recursive: true });
331
351
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
332
352
  runId,
353
+ sessionId: "session-123",
333
354
  mode: "single",
334
355
  state: "running",
335
356
  pid: process.pid,
@@ -352,20 +373,72 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
352
373
  makeMinimalCtx(tempDir),
353
374
  );
354
375
 
355
- assert.equal(result.isError, undefined);
356
- assert.match(result.content[0]?.text ?? "", /Interrupted live async child, then delivered follow-up/);
357
- assert.deepEqual(kills, [
358
- { pid: process.pid, signal: 0 },
359
- { pid: process.pid, signal: process.platform === "win32" ? "SIGBREAK" : "SIGUSR2" },
360
- ]);
361
- const payload = events.emitted.find((entry) => entry.channel === "subagent:result-intercom")?.payload as { to?: string; message?: string } | undefined;
362
- assert.equal(payload?.to, `subagent-worker-${runId}-1`);
363
- assert.match(payload?.message ?? "", /Can you clarify the last change\?/);
376
+ assert.equal(result.isError, true);
377
+ assert.match(result.content[0]?.text ?? "", new RegExp(`Async child '${runId}' index 0 is still running`));
378
+ assert.match(result.content[0]?.text ?? "", new RegExp(`subagent\\(\\{ action: "steer", id: "${runId}", index: 0, message: "\\.\\.\\." \\}\\)`));
379
+ assert.deepEqual(kills, []);
380
+ assert.equal(fs.existsSync(path.join(asyncDir, "control", "interrupt.json")), false);
381
+ assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
364
382
  } finally {
365
383
  fs.rmSync(asyncDir, { recursive: true, force: true });
366
384
  }
367
385
  });
368
386
 
387
+ it("resume action rejects async runs from another or missing parent session", async () => {
388
+ for (const [suffix, sessionId] of [["other", "another-session"], ["missing", undefined]] as const) {
389
+ const runId = `resume-${suffix}-session-${Date.now()}`;
390
+ const asyncDir = path.join(ASYNC_DIR, runId);
391
+ try {
392
+ fs.mkdirSync(asyncDir, { recursive: true });
393
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
394
+ runId,
395
+ ...(sessionId ? { sessionId } : {}),
396
+ mode: "single",
397
+ state: "running",
398
+ pid: process.pid,
399
+ startedAt: 100,
400
+ lastUpdate: Date.now(),
401
+ steps: [{ agent: "worker", status: "running" }],
402
+ }, null, 2), "utf-8");
403
+ const { executor } = makeExecutor();
404
+
405
+ const result = await executor.execute(
406
+ `resume-${suffix}-session`,
407
+ { action: "resume", id: runId, message: "Continue" },
408
+ new AbortController().signal,
409
+ undefined,
410
+ makeMinimalCtx(tempDir),
411
+ );
412
+
413
+ assert.equal(result.isError, true);
414
+ assert.match(result.content[0]?.text ?? "", /not found in the active session/);
415
+ assert.equal(mockPi.callCount(), 0);
416
+ } finally {
417
+ fs.rmSync(asyncDir, { recursive: true, force: true });
418
+ }
419
+ }
420
+ });
421
+
422
+ it("resume action rejects explicit reviewed acceptance before launching", async () => {
423
+ for (const [index, params] of [
424
+ { message: "Continue", acceptance: "reviewed" },
425
+ { chain: [{ agent: "reviewer", task: "Review {previous}", acceptance: { level: "reviewed" } }] },
426
+ ].entries()) {
427
+ const { executor, events } = makeExecutor({ agents: [makeAgent("reviewer")] });
428
+ const result = await executor.execute(
429
+ `resume-invalid-acceptance-${index}`,
430
+ { action: "resume", id: "missing-source-run", ...params },
431
+ new AbortController().signal,
432
+ undefined,
433
+ makeMinimalCtx(tempDir),
434
+ );
435
+
436
+ assert.equal(result.isError, true);
437
+ assert.match(result.content[0]?.text ?? "", /Cannot resume:.*achieved status.*acceptance\.review\.required/i);
438
+ assert.equal(events.emitted.some((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT), false);
439
+ }
440
+ });
441
+
369
442
  it("resume action can attach a live async child as the first step of a new chain", async () => {
370
443
  const sourceRunId = `resume-chain-root-${Date.now()}`;
371
444
  const sourceAsyncDir = path.join(ASYNC_DIR, sourceRunId);
@@ -377,6 +450,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
377
450
  fs.writeFileSync(sourceSession, "", "utf-8");
378
451
  fs.writeFileSync(path.join(sourceAsyncDir, "status.json"), JSON.stringify({
379
452
  runId: sourceRunId,
453
+ sessionId: "session-123",
380
454
  mode: "single",
381
455
  state: "running",
382
456
  pid: process.pid,
@@ -387,6 +461,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
387
461
  }, null, 2), "utf-8");
388
462
  fs.writeFileSync(sourceResultPath, JSON.stringify({
389
463
  id: sourceRunId,
464
+ sessionId: "session-123",
390
465
  agent: "worker",
391
466
  mode: "single",
392
467
  success: true,
@@ -410,11 +485,12 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
410
485
 
411
486
  assert.equal(result.isError, undefined);
412
487
  assert.match(result.content[0]?.text ?? "", /Attached async subagent/);
413
- const startedEvent = events.emitted.find((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT)?.payload as { agent?: string; agents?: string[]; chain?: string[]; chainStepCount?: number } | undefined;
488
+ const startedEvent = events.emitted.find((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT)?.payload as { agent?: string; agents?: string[]; chain?: string[]; chainStepCount?: number; goal?: string } | undefined;
414
489
  assert.equal(startedEvent?.agent, "worker");
415
490
  assert.deepEqual(startedEvent?.agents, ["worker", "reviewer"]);
416
491
  assert.deepEqual(startedEvent?.chain, ["worker", "reviewer"]);
417
492
  assert.equal(startedEvent?.chainStepCount, 2);
493
+ assert.equal(startedEvent?.goal, "Review this root result: {previous}");
418
494
  const attachedId = result.details?.asyncId;
419
495
  assert.ok(attachedId, "expected attached chain async id");
420
496
  assert.match(result.details?.asyncDir ?? "", new RegExp(`${attachedId}$`));
@@ -441,6 +517,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
441
517
  fs.mkdirSync(RESULTS_DIR, { recursive: true });
442
518
  fs.writeFileSync(path.join(sourceAsyncDir, "status.json"), JSON.stringify({
443
519
  runId: sourceRunId,
520
+ sessionId: "session-123",
444
521
  mode: "single",
445
522
  state: "complete",
446
523
  startedAt: 100,
@@ -450,6 +527,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
450
527
  }, null, 2), "utf-8");
451
528
  fs.writeFileSync(sourceResultPath, JSON.stringify({
452
529
  id: sourceRunId,
530
+ sessionId: "session-123",
453
531
  agent: "worker",
454
532
  mode: "single",
455
533
  success: true,
@@ -503,6 +581,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
503
581
  fs.writeFileSync(secondSession, "", "utf-8");
504
582
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
505
583
  runId,
584
+ sessionId: "session-123",
506
585
  mode: "parallel",
507
586
  state: "complete",
508
587
  startedAt: 100,
@@ -510,7 +589,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
510
589
  cwd: tempDir,
511
590
  steps: [
512
591
  { agent: "a", status: "complete", sessionFile: firstSession },
513
- { agent: "b", status: "complete", sessionFile: secondSession },
592
+ { agent: "b", status: "complete", sessionFile: secondSession, model: "anthropic/claude-sonnet-4", thinking: "high" },
514
593
  ],
515
594
  }, null, 2), "utf-8");
516
595
  const { executor } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b")] });
@@ -529,6 +608,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
529
608
  assert.match(result.content[0]?.text ?? "", new RegExp(secondSession.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
530
609
  const args = await readMockCallArgs(0);
531
610
  assert.equal(args[args.indexOf("--session") + 1], secondSession);
611
+ assert.equal(args[args.indexOf("--model") + 1], "anthropic/claude-sonnet-4:high");
532
612
  } finally {
533
613
  fs.rmSync(asyncDir, { recursive: true, force: true });
534
614
  }
@@ -544,6 +624,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
544
624
  fs.writeFileSync(sessionFile, "", "utf-8");
545
625
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
546
626
  runId,
627
+ sessionId: "session-123",
547
628
  mode: "single",
548
629
  state: "complete",
549
630
  startedAt: 100,
@@ -552,7 +633,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
552
633
  sessionFile,
553
634
  steps: [{ agent: "worker", status: "complete" }],
554
635
  }, null, 2), "utf-8");
555
- const { executor } = makeExecutor();
636
+ const { executor, events } = makeExecutor();
556
637
 
557
638
  const result = await executor.execute(
558
639
  "resume-revive",
@@ -565,11 +646,13 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
565
646
  assert.equal(result.isError, undefined);
566
647
  assert.match(result.content[0]?.text ?? "", /Revived async subagent from/);
567
648
  assert.match(result.content[0]?.text ?? "", /Do not run sleep timers or polling loops/);
568
- assert.match(result.content[0]?.text ?? "", /call wait\(\)/);
649
+ assert.match(result.content[0]?.text ?? "", /call subagent_wait\(\)/);
569
650
  assert.match(result.content[0]?.text ?? "", /Status if needed: subagent\(\{ action: "status"/);
570
651
  assert.doesNotMatch(result.content[0]?.text ?? "", /Follow:/);
571
652
  const revivedId = result.details?.asyncId;
572
653
  assert.ok(revivedId, "expected revived async id");
654
+ const startedEvent = events.emitted.find((entry) => entry.channel === SUBAGENT_ASYNC_STARTED_EVENT && (entry.payload as { id?: string }).id === revivedId)?.payload as { goal?: string } | undefined;
655
+ assert.equal(startedEvent?.goal, "What changed?");
573
656
  const resultPath = path.join(RESULTS_DIR, `${revivedId}.json`);
574
657
  const deadline = Date.now() + 10_000;
575
658
  while (!fs.existsSync(resultPath)) {
@@ -581,6 +664,123 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
581
664
  }
582
665
  });
583
666
 
667
+ it("revives from the persisted contract when the agent definition was removed", async () => {
668
+ mockPi.onCall({ output: "descriptor-backed answer" });
669
+ const runId = `resume-descriptor-${Date.now()}`;
670
+ const asyncDir = path.join(ASYNC_DIR, runId);
671
+ const sessionFile = path.join(tempDir, "descriptor-child.jsonl");
672
+ let revivedId: string | undefined;
673
+ try {
674
+ fs.mkdirSync(asyncDir, { recursive: true });
675
+ fs.writeFileSync(sessionFile, "", "utf-8");
676
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
677
+ runId, sessionId: "session-123", mode: "single", state: "paused", startedAt: 100, lastUpdate: 200, cwd: tempDir,
678
+ steps: [{ agent: "removed-worker", status: "paused", sessionFile }],
679
+ }, null, 2), "utf-8");
680
+ fs.writeFileSync(path.join(asyncDir, "recovery-descriptor.json"), JSON.stringify({
681
+ version: 1,
682
+ sourceRunId: runId,
683
+ agent: "removed-worker",
684
+ cwd: tempDir,
685
+ model: "anthropic/claude-sonnet-4:high",
686
+ tools: ["read"],
687
+ systemPrompt: "Original persisted prompt",
688
+ systemPromptMode: "replace",
689
+ inheritProjectContext: false,
690
+ inheritSkills: false,
691
+ outputMode: "inline",
692
+ maxSubagentDepth: 1,
693
+ share: false,
694
+ }, null, 2), "utf-8");
695
+ const { executor } = makeExecutor({ agents: [] });
696
+
697
+ const result = await executor.execute(
698
+ "resume-descriptor",
699
+ { action: "resume", id: runId, message: "Continue safely." },
700
+ new AbortController().signal,
701
+ undefined,
702
+ makeMinimalCtx(tempDir),
703
+ );
704
+
705
+ assert.equal(result.isError, undefined);
706
+ revivedId = result.details.asyncId;
707
+ assert.ok(revivedId);
708
+ const args = await readMockCallArgs(0);
709
+ assert.equal(args[args.indexOf("--session") + 1], sessionFile);
710
+ assert.equal(args[args.indexOf("--model") + 1], "anthropic/claude-sonnet-4:high");
711
+ assert.equal(args[args.indexOf("--tools") + 1], "read");
712
+ assert.equal(args.includes("--system-prompt"), true);
713
+ assert.equal(args.includes("--append-system-prompt"), false);
714
+ await waitForFile(path.join(RESULTS_DIR, `${revivedId}.json`));
715
+ } finally {
716
+ fs.rmSync(asyncDir, { recursive: true, force: true });
717
+ }
718
+ });
719
+
720
+ it("rejects concurrent direct revival of the same completed async session and releases ownership", async () => {
721
+ const releasePath = path.join(tempDir, "release-async-revival");
722
+ mockPi.onCall({ waitForPath: releasePath, output: "first revived answer" });
723
+ mockPi.onCall({ output: "answer after release" });
724
+ const sourceRunId = `resume-lease-async-${Date.now()}`;
725
+ const sourceAsyncDir = path.join(ASYNC_DIR, sourceRunId);
726
+ const sessionFile = path.join(tempDir, "leased-async-child.jsonl");
727
+ fs.mkdirSync(sourceAsyncDir, { recursive: true });
728
+ fs.writeFileSync(sessionFile, "", "utf-8");
729
+ fs.writeFileSync(path.join(sourceAsyncDir, "status.json"), JSON.stringify({
730
+ runId: sourceRunId,
731
+ sessionId: "session-123",
732
+ mode: "single",
733
+ state: "complete",
734
+ startedAt: 100,
735
+ lastUpdate: 200,
736
+ cwd: tempDir,
737
+ sessionFile,
738
+ steps: [{ agent: "worker", status: "complete" }],
739
+ }, null, 2), "utf-8");
740
+ try {
741
+ const { executor } = makeExecutor();
742
+ const first = await executor.execute(
743
+ "resume-lease-async-first",
744
+ { action: "resume", id: sourceRunId, message: "First follow-up" },
745
+ new AbortController().signal,
746
+ undefined,
747
+ makeMinimalCtx(tempDir),
748
+ );
749
+ assert.equal(first.isError, undefined);
750
+ const firstRevivedId = first.details?.asyncId;
751
+ assert.ok(firstRevivedId);
752
+ await readMockCallArgs(0);
753
+
754
+ const blocked = await executor.execute(
755
+ "resume-lease-async-blocked",
756
+ { action: "resume", id: sourceRunId, message: "Conflicting follow-up" },
757
+ new AbortController().signal,
758
+ undefined,
759
+ makeMinimalCtx(tempDir),
760
+ );
761
+ assert.equal(blocked.isError, true);
762
+ assert.match(blocked.content[0]?.text ?? "", new RegExp(`already owned by run '${firstRevivedId}'`));
763
+ assert.equal(fs.readdirSync(mockPi.dir).filter((name) => name.startsWith("call-")).length, 1, "contending revival must not spawn Pi");
764
+
765
+ fs.writeFileSync(releasePath, "release", "utf-8");
766
+ await waitForFile(path.join(RESULTS_DIR, `${firstRevivedId}.json`));
767
+ await waitForMissingPath(sessionLeaseDir(sessionFile));
768
+ const afterRelease = await executor.execute(
769
+ "resume-lease-async-after-release",
770
+ { action: "resume", id: sourceRunId, message: "Follow-up after release" },
771
+ new AbortController().signal,
772
+ undefined,
773
+ makeMinimalCtx(tempDir),
774
+ );
775
+ assert.equal(afterRelease.isError, undefined);
776
+ assert.ok(afterRelease.details?.asyncId);
777
+ await waitForFile(path.join(RESULTS_DIR, `${afterRelease.details.asyncId}.json`));
778
+ } finally {
779
+ fs.writeFileSync(releasePath, "release", "utf-8");
780
+ fs.rmSync(sourceAsyncDir, { recursive: true, force: true });
781
+ }
782
+ });
783
+
584
784
  it("resume action revives a completed foreground child by index", async () => {
585
785
  mockPi.onCall({ output: "first child done" });
586
786
  mockPi.onCall({ output: "second child done" });
@@ -622,6 +822,65 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
622
822
  }
623
823
  });
624
824
 
825
+ it("applies the same exclusive lease to remembered foreground revival", async () => {
826
+ const releasePath = path.join(tempDir, "release-foreground-revival");
827
+ mockPi.onCall({ output: "original foreground answer" });
828
+ mockPi.onCall({ waitForPath: releasePath, output: "first foreground revival" });
829
+ mockPi.onCall({ output: "foreground revival after release" });
830
+ const { executor } = makeExecutor({ bridgeMode: "off" });
831
+ try {
832
+ const original = await executor.execute(
833
+ "foreground-lease-original",
834
+ { agent: "worker", task: "Original task" },
835
+ new AbortController().signal,
836
+ undefined,
837
+ makeMinimalCtx(tempDir),
838
+ );
839
+ const sourceRunId = original.details?.runId;
840
+ const sessionFile = original.details?.results?.[0]?.sessionFile;
841
+ assert.ok(sourceRunId);
842
+ assert.ok(sessionFile);
843
+
844
+ const first = await executor.execute(
845
+ "foreground-lease-first",
846
+ { action: "resume", id: sourceRunId, message: "First follow-up" },
847
+ new AbortController().signal,
848
+ undefined,
849
+ makeMinimalCtx(tempDir),
850
+ );
851
+ assert.equal(first.isError, undefined);
852
+ const firstRevivedId = first.details?.asyncId;
853
+ assert.ok(firstRevivedId);
854
+
855
+ const blocked = await executor.execute(
856
+ "foreground-lease-blocked",
857
+ { action: "resume", id: sourceRunId, message: "Conflicting follow-up" },
858
+ new AbortController().signal,
859
+ undefined,
860
+ makeMinimalCtx(tempDir),
861
+ );
862
+ assert.equal(blocked.isError, true);
863
+ assert.match(blocked.content[0]?.text ?? "", new RegExp(`already owned by run '${firstRevivedId}'`));
864
+ assert.equal(fs.readdirSync(mockPi.dir).filter((name) => name.startsWith("call-")).length, 2, "contending foreground revival must not spawn Pi");
865
+
866
+ fs.writeFileSync(releasePath, "release", "utf-8");
867
+ await waitForFile(path.join(RESULTS_DIR, `${firstRevivedId}.json`));
868
+ await waitForMissingPath(sessionLeaseDir(sessionFile));
869
+ const afterRelease = await executor.execute(
870
+ "foreground-lease-after-release",
871
+ { action: "resume", id: sourceRunId, message: "Follow-up after release" },
872
+ new AbortController().signal,
873
+ undefined,
874
+ makeMinimalCtx(tempDir),
875
+ );
876
+ assert.equal(afterRelease.isError, undefined);
877
+ assert.ok(afterRelease.details?.asyncId);
878
+ await waitForFile(path.join(RESULTS_DIR, `${afterRelease.details.asyncId}.json`));
879
+ } finally {
880
+ fs.writeFileSync(releasePath, "release", "utf-8");
881
+ }
882
+ });
883
+
625
884
  it("status recovers remembered detached foreground output after child exit", async () => {
626
885
  mockPi.onCall({
627
886
  steps: [
@@ -680,6 +939,152 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
680
939
  assert.match(transcriptText, /final recovered answer/);
681
940
  });
682
941
 
942
+ it("keeps detached acceptance pending and emits recovered foreground completion to the originating session", async () => {
943
+ const acceptanceReport = [
944
+ "final recovered answer",
945
+ "```acceptance-report",
946
+ JSON.stringify({
947
+ criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "review completed" }],
948
+ changedFiles: ["src/review.ts"],
949
+ testsAddedOrUpdated: ["test/review.test.ts"],
950
+ commandsRun: [{ command: "npm test", result: "passed", summary: "passed" }],
951
+ validationOutput: ["npm test passed"],
952
+ residualRisks: [],
953
+ noStagedFiles: true,
954
+ }),
955
+ "```",
956
+ ].join("\n");
957
+ mockPi.onCall({
958
+ steps: [
959
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
960
+ { delay: 75, jsonl: [events.assistantMessage(acceptanceReport)] },
961
+ ],
962
+ });
963
+ const { executor, events: bus, state } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" })] });
964
+ let detachEmitted = false;
965
+ const original = await executor.execute(
966
+ "foreground-detached-completion-original",
967
+ { agent: "a", task: "ask supervisor", acceptance: { level: "checked", criteria: ["Review completed"] } },
968
+ new AbortController().signal,
969
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
970
+ if (detachEmitted || !update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
971
+ detachEmitted = true;
972
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "single-detached-completion" });
973
+ },
974
+ makeMinimalCtx(tempDir),
975
+ );
976
+ const runId = original.details?.runId;
977
+ assert.ok(runId, "expected foreground run id");
978
+ assert.equal(original.details?.results?.[0]?.acceptance?.status, "pending");
979
+ assert.match(original.content[0]?.text ?? "", /subagent_wait\(\{ id:/);
980
+ const metadataPath = original.details?.results?.[0]?.artifactPaths?.metadataPath;
981
+ assert.ok(metadataPath);
982
+ const pendingMetadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { acceptance?: { status?: string } };
983
+ assert.equal(pendingMetadata.acceptance?.status, "pending");
984
+
985
+ const waited = await waitForSubagents({ id: runId, timeoutMs: 5000 }, undefined, {
986
+ state: state as never,
987
+ events: bus,
988
+ asyncDirRoot: path.join(tempDir, "async-runs"),
989
+ resultsDir: path.join(tempDir, "results"),
990
+ });
991
+ assert.equal(waited.isError, undefined);
992
+ assert.match(waited.content[0]?.text ?? "", /remembered detached foreground run/);
993
+
994
+ const completion = bus.emitted.find((event) => event.channel === SUBAGENT_FOREGROUND_COMPLETE_EVENT);
995
+ assert.ok(completion, "expected a foreground completion event");
996
+ const payload = completion.payload as {
997
+ runId?: string;
998
+ sessionId?: string;
999
+ source?: string;
1000
+ summary?: string;
1001
+ success?: boolean;
1002
+ };
1003
+ assert.equal(payload.runId, runId);
1004
+ assert.equal(payload.sessionId, "session-123");
1005
+ assert.equal(payload.source, "foreground");
1006
+ assert.equal(payload.success, true);
1007
+ assert.match(payload.summary ?? "", /final recovered answer/);
1008
+ const recoveredMetadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { exitCode?: number; acceptance?: { status?: string; childReport?: unknown } };
1009
+ assert.equal(recoveredMetadata.exitCode, 0);
1010
+ assert.equal(recoveredMetadata.acceptance?.status, "checked");
1011
+ assert.ok(recoveredMetadata.acceptance?.childReport);
1012
+
1013
+ const status = await executor.execute(
1014
+ "foreground-detached-completion-status",
1015
+ { action: "status", id: runId },
1016
+ new AbortController().signal,
1017
+ undefined,
1018
+ makeMinimalCtx(tempDir),
1019
+ );
1020
+ assert.match(status.content[0]?.text ?? "", /acceptance: checked/);
1021
+ });
1022
+
1023
+ it("preserves deferred acceptance failure details in foreground completion and status", async () => {
1024
+ const rejectedReport = [
1025
+ "final answer with rejected acceptance evidence",
1026
+ "```acceptance-report",
1027
+ JSON.stringify({
1028
+ criteriaSatisfied: [{ id: "criterion-1", status: "not-satisfied", evidence: "review found a blocker" }],
1029
+ changedFiles: ["src/review.ts"],
1030
+ testsAddedOrUpdated: ["test/review.test.ts"],
1031
+ commandsRun: [{ command: "npm test", result: "failed", summary: "blocker remains" }],
1032
+ validationOutput: ["blocker remains"],
1033
+ residualRisks: ["review blocker"],
1034
+ noStagedFiles: true,
1035
+ }),
1036
+ "```",
1037
+ ].join("\n");
1038
+ mockPi.onCall({
1039
+ steps: [
1040
+ { jsonl: [events.toolStart("contact_supervisor", { reason: "need_decision", message: "Need a decision" })] },
1041
+ { delay: 75, jsonl: [events.assistantMessage(rejectedReport)] },
1042
+ ],
1043
+ });
1044
+ const { executor, events: bus, state } = makeExecutor({ agents: [makeAgent("a", { systemPrompt: "Intercom orchestration channel:" })] });
1045
+ let detachEmitted = false;
1046
+ const original = await executor.execute(
1047
+ "foreground-detached-failed-acceptance",
1048
+ { agent: "a", task: "ask supervisor", acceptance: { level: "checked", criteria: ["Review completed"] } },
1049
+ new AbortController().signal,
1050
+ (update: { details?: { progress?: Array<{ currentTool?: string }> } }) => {
1051
+ if (detachEmitted || !update.details?.progress?.some((entry) => entry.currentTool === "contact_supervisor")) return;
1052
+ detachEmitted = true;
1053
+ bus.emit(INTERCOM_DETACH_REQUEST_EVENT, { requestId: "single-detached-failed-acceptance" });
1054
+ },
1055
+ makeMinimalCtx(tempDir),
1056
+ );
1057
+ const runId = original.details?.runId;
1058
+ assert.ok(runId);
1059
+ assert.equal(original.details?.results?.[0]?.acceptance?.status, "pending");
1060
+
1061
+ const waited = await waitForSubagents({ id: runId, timeoutMs: 5000 }, undefined, {
1062
+ state: state as never,
1063
+ events: bus,
1064
+ asyncDirRoot: path.join(tempDir, "async-runs"),
1065
+ resultsDir: path.join(tempDir, "results"),
1066
+ });
1067
+ assert.equal(waited.isError, undefined);
1068
+ assert.match(waited.content[0]?.text ?? "", /1 failed/);
1069
+
1070
+ const completion = bus.emitted.find((event) => event.channel === SUBAGENT_FOREGROUND_COMPLETE_EVENT);
1071
+ assert.ok(completion);
1072
+ const payload = completion.payload as { success?: boolean; summary?: string };
1073
+ assert.equal(payload.success, false);
1074
+ assert.match(payload.summary ?? "", /Acceptance rejected/);
1075
+ assert.match(payload.summary ?? "", /final answer with rejected acceptance evidence/);
1076
+
1077
+ const status = await executor.execute(
1078
+ "foreground-detached-failed-status",
1079
+ { action: "status", id: runId },
1080
+ new AbortController().signal,
1081
+ undefined,
1082
+ makeMinimalCtx(tempDir),
1083
+ );
1084
+ assert.match(status.content[0]?.text ?? "", /acceptance: rejected/);
1085
+ assert.match(status.content[0]?.text ?? "", /error: Acceptance rejected/);
1086
+ });
1087
+
683
1088
  it("status recovers remembered detached chain output after child exit", async () => {
684
1089
  mockPi.onCall({
685
1090
  steps: [
@@ -796,6 +1201,45 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
796
1201
  assert.match(transcript.content[0]?.text ?? "", /second recovered answer/);
797
1202
  });
798
1203
 
1204
+ it("blocks sibling resume while any remembered foreground child remains detached", async () => {
1205
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b")] });
1206
+ const siblingSession = path.join(tempDir, "completed-sibling.jsonl");
1207
+ fs.writeFileSync(siblingSession, "", "utf-8");
1208
+ state.foregroundRuns.set("mixed-detached-run", {
1209
+ runId: "mixed-detached-run",
1210
+ mode: "parallel",
1211
+ cwd: tempDir,
1212
+ sessionId: "session-123",
1213
+ updatedAt: Date.now(),
1214
+ children: [
1215
+ { agent: "a", index: 0, status: "detached", updatedAt: Date.now() },
1216
+ { agent: "b", index: 1, status: "completed", sessionFile: siblingSession, updatedAt: Date.now() },
1217
+ ],
1218
+ });
1219
+
1220
+ const status = await executor.execute(
1221
+ "mixed-detached-status",
1222
+ { action: "status", id: "mixed-detached-run" },
1223
+ new AbortController().signal,
1224
+ undefined,
1225
+ makeMinimalCtx(tempDir),
1226
+ );
1227
+ const statusText = status.content[0]?.text ?? "";
1228
+ assert.match(statusText, /do not resume or launch a replacement while any child remains detached/);
1229
+ assert.doesNotMatch(statusText, /Revive child:/);
1230
+
1231
+ const resumed = await executor.execute(
1232
+ "mixed-detached-resume",
1233
+ { action: "resume", id: "mixed-detached-run", index: 1, message: "replace sibling" },
1234
+ new AbortController().signal,
1235
+ undefined,
1236
+ makeMinimalCtx(tempDir),
1237
+ );
1238
+ assert.equal(resumed.isError, true);
1239
+ assert.match(resumed.content[0]?.text ?? "", /cannot be revived safely while any child may still be live/);
1240
+ assert.match(resumed.content[0]?.text ?? "", /do not launch a replacement/);
1241
+ });
1242
+
799
1243
  it("resume action rejects detached foreground children that may still be live", async () => {
800
1244
  mockPi.onCall({
801
1245
  steps: [
@@ -821,6 +1265,18 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
821
1265
  const runId = original.details?.runId;
822
1266
  assert.ok(runId, "expected foreground run id");
823
1267
 
1268
+ const fleet = await executor.execute(
1269
+ "foreground-detached-fleet",
1270
+ { action: "status", view: "fleet" },
1271
+ new AbortController().signal,
1272
+ undefined,
1273
+ makeMinimalCtx(tempDir),
1274
+ );
1275
+ const fleetText = fleet.content[0]?.text ?? "";
1276
+ assert.match(fleetText, /Detached foreground runs:/);
1277
+ assert.ok(fleetText.includes(runId));
1278
+ assert.match(fleetText, /recovery: reply to the supervisor request first/);
1279
+
824
1280
  const resumed = await executor.execute(
825
1281
  "foreground-detached-resume",
826
1282
  { action: "resume", id: runId, message: "Follow up" },
@@ -835,6 +1291,50 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
835
1291
  assert.doesNotMatch(resumed.content[0]?.text ?? "", /revive only/);
836
1292
  });
837
1293
 
1294
+ it("does not inspect or resume remembered foreground runs from another parent session", async () => {
1295
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a")] });
1296
+ const sessionFile = path.join(tempDir, "other-session-child.jsonl");
1297
+ fs.writeFileSync(sessionFile, "", "utf-8");
1298
+ state.foregroundRuns.set("other-session-run", {
1299
+ runId: "other-session-run",
1300
+ mode: "single",
1301
+ cwd: tempDir,
1302
+ sessionId: "session-other",
1303
+ updatedAt: Date.now(),
1304
+ children: [{ agent: "a", index: 0, status: "completed", sessionFile }],
1305
+ });
1306
+
1307
+ const status = await executor.execute(
1308
+ "other-session-status",
1309
+ { action: "status", id: "other-session-run" },
1310
+ new AbortController().signal,
1311
+ undefined,
1312
+ makeMinimalCtx(tempDir),
1313
+ );
1314
+ assert.equal(status.isError, true);
1315
+ assert.doesNotMatch(status.content[0]?.text ?? "", /State: remembered foreground/);
1316
+
1317
+ const fleet = await executor.execute(
1318
+ "other-session-fleet",
1319
+ { action: "status", view: "fleet" },
1320
+ new AbortController().signal,
1321
+ undefined,
1322
+ makeMinimalCtx(tempDir),
1323
+ );
1324
+ assert.doesNotMatch(fleet.content[0]?.text ?? "", /other-session-run/);
1325
+
1326
+ const resumed = await executor.execute(
1327
+ "other-session-resume",
1328
+ { action: "resume", id: "other-session-run", message: "Follow up" },
1329
+ new AbortController().signal,
1330
+ undefined,
1331
+ makeMinimalCtx(tempDir),
1332
+ );
1333
+ assert.equal(resumed.isError, true);
1334
+ assert.doesNotMatch(resumed.content[0]?.text ?? "", /Revived foreground subagent/);
1335
+ assert.equal(mockPi.callCount(), 0);
1336
+ });
1337
+
838
1338
  it("resume action keeps exact foreground validation errors over async prefix matches", async () => {
839
1339
  const base = `exact-invalid-${Date.now()}`;
840
1340
  const asyncSession = path.join(tempDir, "async-exact-prefix.jsonl");
@@ -856,6 +1356,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
856
1356
  runId: base,
857
1357
  mode: "single",
858
1358
  cwd: tempDir,
1359
+ sessionId: "session-123",
859
1360
  updatedAt: Date.now(),
860
1361
  children: [{ agent: "a", index: 0, status: "completed" }],
861
1362
  });
@@ -885,6 +1386,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
885
1386
  fs.mkdirSync(asyncDir, { recursive: true });
886
1387
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
887
1388
  runId: base,
1389
+ sessionId: "session-123",
888
1390
  mode: "single",
889
1391
  state: "complete",
890
1392
  startedAt: 100,
@@ -897,6 +1399,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
897
1399
  runId: `${base}-foreground`,
898
1400
  mode: "single",
899
1401
  cwd: tempDir,
1402
+ sessionId: "session-123",
900
1403
  updatedAt: Date.now(),
901
1404
  children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
902
1405
  });
@@ -932,6 +1435,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
932
1435
  fs.mkdirSync(asyncDir, { recursive: true });
933
1436
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
934
1437
  runId,
1438
+ sessionId: "session-123",
935
1439
  mode: "single",
936
1440
  state: "complete",
937
1441
  startedAt: 100,
@@ -945,6 +1449,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
945
1449
  runId: `${base}-foreground`,
946
1450
  mode: "single",
947
1451
  cwd: tempDir,
1452
+ sessionId: "session-123",
948
1453
  updatedAt: Date.now(),
949
1454
  children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
950
1455
  });
@@ -978,6 +1483,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
978
1483
  fs.mkdirSync(asyncDir, { recursive: true });
979
1484
  fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
980
1485
  runId: asyncId,
1486
+ sessionId: "session-123",
981
1487
  mode: "single",
982
1488
  state: "complete",
983
1489
  startedAt: 100,
@@ -990,6 +1496,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
990
1496
  runId: foregroundId,
991
1497
  mode: "single",
992
1498
  cwd: tempDir,
1499
+ sessionId: "session-123",
993
1500
  updatedAt: Date.now(),
994
1501
  children: [{ agent: "a", index: 0, status: "completed", sessionFile: foregroundSession }],
995
1502
  });