@selesai/code 0.5.27 → 0.5.29

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 (310) hide show
  1. package/dist/cli/args.d.ts.map +1 -1
  2. package/dist/cli/args.js +9 -1
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/config-selector.d.ts.map +1 -1
  5. package/dist/cli/config-selector.js +1 -1
  6. package/dist/cli/config-selector.js.map +1 -1
  7. package/dist/cli/credential-print.d.ts +23 -0
  8. package/dist/cli/credential-print.d.ts.map +1 -0
  9. package/dist/cli/credential-print.js +117 -0
  10. package/dist/cli/credential-print.js.map +1 -0
  11. package/dist/cli/startup-ui.d.ts.map +1 -1
  12. package/dist/cli/startup-ui.js +1 -1
  13. package/dist/cli/startup-ui.js.map +1 -1
  14. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  15. package/dist/core/agent-session-runtime.js +3 -0
  16. package/dist/core/agent-session-runtime.js.map +1 -1
  17. package/dist/core/agent-session.d.ts +12 -1
  18. package/dist/core/agent-session.d.ts.map +1 -1
  19. package/dist/core/agent-session.js +20 -14
  20. package/dist/core/agent-session.js.map +1 -1
  21. package/dist/core/compaction/compaction.d.ts.map +1 -1
  22. package/dist/core/compaction/compaction.js +11 -3
  23. package/dist/core/compaction/compaction.js.map +1 -1
  24. package/dist/core/extensions/runner.d.ts +1 -0
  25. package/dist/core/extensions/runner.d.ts.map +1 -1
  26. package/dist/core/extensions/runner.js +11 -0
  27. package/dist/core/extensions/runner.js.map +1 -1
  28. package/dist/core/extensions/types.d.ts +14 -1
  29. package/dist/core/extensions/types.d.ts.map +1 -1
  30. package/dist/core/extensions/types.js.map +1 -1
  31. package/dist/core/footer-data-provider.d.ts +10 -0
  32. package/dist/core/footer-data-provider.d.ts.map +1 -1
  33. package/dist/core/footer-data-provider.js +1 -1
  34. package/dist/core/footer-data-provider.js.map +1 -1
  35. package/dist/core/llama/provider.d.ts.map +1 -1
  36. package/dist/core/llama/provider.js +8 -3
  37. package/dist/core/llama/provider.js.map +1 -1
  38. package/dist/core/model-config.d.ts +30 -0
  39. package/dist/core/model-config.d.ts.map +1 -1
  40. package/dist/core/model-config.js +6 -0
  41. package/dist/core/model-config.js.map +1 -1
  42. package/dist/core/model-registry.d.ts.map +1 -1
  43. package/dist/core/model-registry.js +2 -2
  44. package/dist/core/model-registry.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +1 -0
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +20 -3
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/model-runtime.d.ts +2 -0
  50. package/dist/core/model-runtime.d.ts.map +1 -1
  51. package/dist/core/model-runtime.js +5 -4
  52. package/dist/core/model-runtime.js.map +1 -1
  53. package/dist/core/package-manager.d.ts.map +1 -1
  54. package/dist/core/package-manager.js +13 -6
  55. package/dist/core/package-manager.js.map +1 -1
  56. package/dist/core/remote-catalog-provider.d.ts +1 -1
  57. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  58. package/dist/core/remote-catalog-provider.js +24 -11
  59. package/dist/core/remote-catalog-provider.js.map +1 -1
  60. package/dist/core/resource-loader.d.ts +15 -0
  61. package/dist/core/resource-loader.d.ts.map +1 -1
  62. package/dist/core/resource-loader.js +66 -9
  63. package/dist/core/resource-loader.js.map +1 -1
  64. package/dist/core/settings-manager.d.ts +1 -1
  65. package/dist/core/settings-manager.d.ts.map +1 -1
  66. package/dist/core/settings-manager.js.map +1 -1
  67. package/dist/core/system-prompt.d.ts.map +1 -1
  68. package/dist/core/system-prompt.js +1 -1
  69. package/dist/core/system-prompt.js.map +1 -1
  70. package/dist/core/tools/bash.d.ts +2 -0
  71. package/dist/core/tools/bash.d.ts.map +1 -1
  72. package/dist/core/tools/bash.js +34 -5
  73. package/dist/core/tools/bash.js.map +1 -1
  74. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  75. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  76. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  77. package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
  78. package/dist/extensions/pi-intercom/README.md +102 -39
  79. package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
  80. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
  81. package/dist/extensions/pi-intercom/broker/client.ts +315 -12
  82. package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
  83. package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
  84. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
  85. package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
  86. package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
  87. package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
  88. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
  89. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
  90. package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
  91. package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
  92. package/dist/extensions/pi-intercom/config.test.ts +93 -0
  93. package/dist/extensions/pi-intercom/config.ts +55 -6
  94. package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
  95. package/dist/extensions/pi-intercom/cwd.ts +31 -0
  96. package/dist/extensions/pi-intercom/extension-api.ts +44 -0
  97. package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
  98. package/dist/extensions/pi-intercom/format-context.ts +32 -0
  99. package/dist/extensions/pi-intercom/index.ts +742 -145
  100. package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
  101. package/dist/extensions/pi-intercom/package.json +15 -5
  102. package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
  103. package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
  104. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
  105. package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
  106. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
  107. package/dist/extensions/pi-intercom/types.ts +94 -4
  108. package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
  109. package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
  110. package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
  111. package/dist/extensions/pi-subagents/CHANGELOG.md +85 -0
  112. package/dist/extensions/pi-subagents/LICENSE +21 -0
  113. package/dist/extensions/pi-subagents/README.md +146 -52
  114. package/dist/extensions/pi-subagents/agents/architect.md +1 -0
  115. package/dist/extensions/pi-subagents/agents/builder.md +1 -0
  116. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  117. package/dist/extensions/pi-subagents/package.json +3 -3
  118. package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
  119. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +21 -989
  120. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +256 -0
  121. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +430 -0
  122. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  123. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +282 -0
  124. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +71 -30
  125. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
  126. package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
  127. package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
  128. package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
  129. package/dist/extensions/pi-subagents/src/api/preflight.ts +16 -12
  130. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
  131. package/dist/extensions/pi-subagents/src/extension/index.ts +17 -7
  132. package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
  133. package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -6
  134. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +8 -7
  135. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
  136. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
  137. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +63 -16
  138. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
  139. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
  140. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  141. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
  142. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
  143. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +29 -4
  144. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +29 -6
  145. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
  146. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +372 -28
  147. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
  148. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
  149. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +143 -43
  150. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +527 -247
  151. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
  152. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +426 -123
  153. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +15 -7
  154. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
  155. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
  156. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  157. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
  158. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
  159. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
  160. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
  161. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
  162. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
  163. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
  164. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  165. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
  166. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
  167. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +1 -1
  168. package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
  169. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
  170. package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
  171. package/dist/extensions/pi-subagents/src/shared/types.ts +155 -10
  172. package/dist/extensions/pi-subagents/src/shared/utils.ts +73 -4
  173. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +5 -0
  174. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
  175. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
  176. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
  177. package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
  178. package/dist/extensions/pi-subagents/src/tui/render.ts +27 -28
  179. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
  180. package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
  181. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
  182. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
  183. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
  184. package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
  185. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  186. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +348 -7
  187. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
  188. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
  189. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
  190. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +26 -1
  191. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
  192. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +213 -13
  194. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +34 -0
  195. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
  196. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +118 -12
  197. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +850 -7
  198. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
  199. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
  200. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  201. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
  202. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
  203. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +128 -0
  204. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
  205. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
  206. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
  207. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +102 -0
  208. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
  209. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
  210. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
  211. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
  212. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
  213. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
  214. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +4 -2
  215. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
  216. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
  217. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
  218. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
  219. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +11 -11
  220. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
  221. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
  222. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
  223. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +10 -2
  224. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
  225. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
  226. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
  227. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +46 -0
  228. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
  229. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
  230. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
  231. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
  232. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
  233. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +34 -2
  234. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  235. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
  236. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
  237. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
  238. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
  239. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +3 -0
  240. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +1 -1
  241. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
  245. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
  246. package/dist/extensions/question/index.ts +14 -1
  247. package/dist/extensions/question/tests/wizard.test.ts +11 -8
  248. package/dist/main.d.ts.map +1 -1
  249. package/dist/main.js +43 -0
  250. package/dist/main.js.map +1 -1
  251. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  252. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  253. package/dist/modes/interactive/components/custom-message.js +10 -2
  254. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  255. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  256. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  257. package/dist/modes/interactive/components/extension-editor.js +16 -46
  258. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  259. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  260. package/dist/modes/interactive/components/model-selector.js +4 -1
  261. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  262. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  263. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  264. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  265. package/dist/modes/interactive/external-editor.d.ts +12 -0
  266. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  267. package/dist/modes/interactive/external-editor.js +37 -0
  268. package/dist/modes/interactive/external-editor.js.map +1 -0
  269. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  270. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  271. package/dist/modes/interactive/interactive-mode.js +72 -64
  272. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  273. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  274. package/dist/modes/rpc/rpc-mode.js +14 -0
  275. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  276. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  277. package/dist/utils/clipboard.d.ts.map +1 -1
  278. package/dist/utils/clipboard.js +19 -8
  279. package/dist/utils/clipboard.js.map +1 -1
  280. package/dist/utils/version-check.d.ts.map +1 -1
  281. package/dist/utils/version-check.js +1 -1
  282. package/dist/utils/version-check.js.map +1 -1
  283. package/docs/compaction.md +1 -1
  284. package/docs/custom-provider.md +14 -5
  285. package/docs/environment-variables.md +86 -0
  286. package/docs/extensions.md +15 -5
  287. package/docs/index.md +1 -0
  288. package/docs/models.md +9 -2
  289. package/docs/providers.md +21 -2
  290. package/docs/rpc.md +23 -6
  291. package/docs/session-format.md +2 -0
  292. package/docs/settings.md +1 -1
  293. package/docs/usage.md +0 -13
  294. package/examples/extensions/custom-compaction.ts +5 -2
  295. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  296. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  297. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  298. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  299. package/examples/extensions/gondolin/package-lock.json +2 -2
  300. package/examples/extensions/gondolin/package.json +1 -1
  301. package/examples/extensions/handoff.ts +11 -3
  302. package/examples/extensions/message-renderer.ts +3 -3
  303. package/examples/extensions/sandbox/package-lock.json +2 -2
  304. package/examples/extensions/sandbox/package.json +1 -1
  305. package/examples/extensions/summarize.ts +5 -2
  306. package/examples/extensions/with-deps/package-lock.json +2 -2
  307. package/examples/extensions/with-deps/package.json +1 -1
  308. package/examples/sdk/12-full-control.ts +3 -1
  309. package/package.json +15 -6
  310. package/dist/extensions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
@@ -64,7 +64,7 @@ interface TestParallelTask {
64
64
  gateOn?: "execution" | "acceptance";
65
65
  }
66
66
 
67
- type TestChainStep = TestSequentialStep | {
67
+ type TestChainStep = { checkpoint: string; message?: string; label?: string; phase?: string } | TestSequentialStep | {
68
68
  parallel: TestParallelTask[];
69
69
  concurrency?: number;
70
70
  failFast?: boolean;
@@ -91,6 +91,7 @@ type TestChainStep = TestSequentialStep | {
91
91
  };
92
92
 
93
93
  interface ChainResultItem {
94
+ index: number;
94
95
  agent: string;
95
96
  exitCode: number;
96
97
  finalOutput?: string;
@@ -118,6 +119,7 @@ interface ChainExecutionResult {
118
119
  currentStepIndex?: number;
119
120
  outputs?: Record<string, { text: string; structured?: unknown }>;
120
121
  parallelHandoff?: { version: number; path: string; groupCount: number; childCount: number; cleanupState: string };
122
+ checkpoint?: { name?: string; status?: string; message?: string };
121
123
  };
122
124
  }
123
125
 
@@ -215,6 +217,27 @@ describe("chain execution — sequential", { skip: !available ? "pi packages not
215
217
  );
216
218
  }
217
219
 
220
+ it("pauses foreground chains at approval checkpoints without launching the next child", async () => {
221
+ mockPi.onCall({ output: "analysis done" });
222
+ mockPi.onCall({ output: "should not run" });
223
+ const agents = [makeAgent("analyst"), makeAgent("worker")];
224
+
225
+ const result = await executeChain!(makeChainParams([
226
+ { agent: "analyst", task: "Analyze" },
227
+ { checkpoint: "review", message: "Approve implementation?" },
228
+ { agent: "worker", task: "Implement" },
229
+ ], agents));
230
+
231
+ assert.equal(result.isError, undefined);
232
+ assert.match(result.content[0]!.text, /Chain paused at checkpoint 'review'/);
233
+ assert.equal(result.details.results.length, 1);
234
+ assert.equal(result.details.checkpoint?.name, "review");
235
+ assert.equal(result.details.checkpoint?.status, "pending");
236
+ assert.equal(result.details.workflowGraph?.nodes[1]?.kind, "checkpoint");
237
+ assert.equal(result.details.workflowGraph?.nodes[1]?.status, "paused");
238
+ assert.equal(mockPi.callCount(), 1);
239
+ });
240
+
218
241
  it("runs a 2-step chain", async () => {
219
242
  mockPi.onCall({ output: "Analysis complete: found 3 issues" });
220
243
  const agents = [makeAgent("analyst"), makeAgent("reporter")];
@@ -1361,6 +1384,7 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
1361
1384
 
1362
1385
  assert.ok(!result.isError, `should succeed: ${JSON.stringify(result.content)}`);
1363
1386
  assert.equal(result.details.results.length, 2);
1387
+ assert.deepEqual(result.details.results.map((row) => row.index), [0, 1]);
1364
1388
  });
1365
1389
 
1366
1390
  it("aggregates worktree handoffs across foreground chain groups", { skip: process.platform === "win32" ? "worktree paths differ on Windows" : undefined }, async () => {
@@ -1700,6 +1724,7 @@ describe("chain execution — parallel steps", { skip: !available ? "pi packages
1700
1724
 
1701
1725
  assert.ok(!result.isError);
1702
1726
  assert.equal(result.details.results.length, 4);
1727
+ assert.deepEqual(result.details.results.map((row) => row.index), [0, 1, 2, 3]);
1703
1728
  assert.equal(result.details.totalSteps, 3);
1704
1729
  });
1705
1730
  });
@@ -53,6 +53,18 @@ function assistantToolCall(toolName: string): Record<string, unknown> {
53
53
  };
54
54
  }
55
55
 
56
+ function emptyTerminalAssistant(): Record<string, unknown> {
57
+ return {
58
+ role: "assistant",
59
+ content: [],
60
+ api: "test",
61
+ provider: "test",
62
+ model: "test",
63
+ stopReason: "stop",
64
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { total: 0 } },
65
+ };
66
+ }
67
+
56
68
  describe("detectSubagentError", { skip: !available ? "utils not importable" : undefined }, () => {
57
69
  // ---- Basic detection (must still work) ----
58
70
 
@@ -197,6 +209,42 @@ describe("detectSubagentError", { skip: !available ? "utils not importable" : un
197
209
  "tool-call assistant message without text is not a recovery");
198
210
  });
199
211
 
212
+ it("accepts a successful tool recovery followed by a clean empty terminal stop", () => {
213
+ const messages = [
214
+ toolResult("bash", "error: process exited with code 1", true),
215
+ assistantToolCall("edit"),
216
+ toolResult("edit", "Updated src/file.ts"),
217
+ emptyTerminalAssistant(),
218
+ ];
219
+ const result = detectSubagentError(messages);
220
+ assert.equal(result.hasError, false,
221
+ "later successful work plus a clean terminal stop recovers the earlier tool error");
222
+ });
223
+
224
+ it("keeps the final failed tool result despite a clean empty terminal stop", () => {
225
+ const messages = [
226
+ toolResult("edit", "Updated src/file.ts"),
227
+ assistantToolCall("bash"),
228
+ toolResult("bash", "error: process exited with code 2", true),
229
+ emptyTerminalAssistant(),
230
+ ];
231
+ const result = detectSubagentError(messages);
232
+ assert.equal(result.hasError, true,
233
+ "a clean terminal stop alone cannot recover the final failed tool result");
234
+ assert.equal(result.exitCode, 2);
235
+ });
236
+
237
+ it("does not recover an earlier tool error after an unsuccessful terminal stop", () => {
238
+ const messages = [
239
+ toolResult("bash", "error: process exited with code 1", true),
240
+ toolResult("edit", "Updated src/file.ts"),
241
+ { ...emptyTerminalAssistant(), errorMessage: "provider transport failed" },
242
+ ];
243
+ const result = detectSubagentError(messages);
244
+ assert.equal(result.hasError, true,
245
+ "the terminal assistant stop must itself be clean");
246
+ });
247
+
200
248
  it("does not treat empty/whitespace assistant message as recovery", () => {
201
249
  const messages = [
202
250
  toolResult("read", "EISDIR: illegal operation on a directory", true),
@@ -1863,14 +1863,14 @@ describe("fork context execution wiring", { skip: !available ? "subagent executo
1863
1863
  process.env.USERPROFILE = tempHome;
1864
1864
  const worktreeDir = path.join(tempDir, "worktree");
1865
1865
  fs.mkdirSync(worktreeDir, { recursive: true });
1866
- writeProjectOverride(tempDir, "reviewer", "openai/gpt-5-main");
1867
- writeProjectOverride(worktreeDir, "reviewer", "openai/gpt-5-worktree");
1866
+ writeProjectOverride(tempDir, "commentator", "openai/gpt-5-main");
1867
+ writeProjectOverride(worktreeDir, "commentator", "openai/gpt-5-worktree");
1868
1868
  const executor = makeExecutor();
1869
1869
 
1870
1870
  try {
1871
1871
  const result = await executor.execute(
1872
1872
  "id",
1873
- { action: "get", agent: "reviewer", cwd: "worktree" },
1873
+ { action: "get", agent: "commentator", cwd: "worktree" },
1874
1874
  new AbortController().signal,
1875
1875
  undefined,
1876
1876
  makeCtx(makeSessionManagerRecorder().manager),
@@ -144,6 +144,14 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
144
144
  }
145
145
  }
146
146
 
147
+ async function waitForCallCount(minimum: number, timeoutMs = 10_000): Promise<void> {
148
+ const deadline = Date.now() + timeoutMs;
149
+ while (mockPi.callCount() < minimum) {
150
+ if (Date.now() > deadline) assert.fail(`Timed out waiting for ${minimum} mock pi calls; saw ${mockPi.callCount()}`);
151
+ await new Promise((resolve) => setTimeout(resolve, 50));
152
+ }
153
+ }
154
+
147
155
  async function waitForMissingPath(filePath: string, timeoutMs = 10_000): Promise<void> {
148
156
  const deadline = Date.now() + timeoutMs;
149
157
  while (fs.existsSync(filePath)) {
@@ -152,7 +160,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
152
160
  }
153
161
  }
154
162
 
155
- function makeExecutor(options: { bridgeMode?: "always" | "off"; agents?: ReturnType<typeof makeAgent>[]; acknowledgeResults?: boolean; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean } = {}) {
163
+ function makeExecutor(options: { bridgeMode?: "always" | "off"; resultDelivery?: boolean; agents?: ReturnType<typeof makeAgent>[]; acknowledgeResults?: boolean; kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean } = {}) {
156
164
  const events = createRecordingEventBus({ acknowledgeResults: options.acknowledgeResults ?? true });
157
165
  const state = {
158
166
  baseCwd: tempDir,
@@ -180,7 +188,10 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
180
188
  },
181
189
  state,
182
190
  config: {
183
- intercomBridge: { mode: options.bridgeMode ?? "always" },
191
+ intercomBridge: {
192
+ mode: options.bridgeMode ?? "always",
193
+ ...(options.resultDelivery === undefined ? {} : { resultDelivery: options.resultDelivery }),
194
+ },
184
195
  },
185
196
  asyncByDefault: false,
186
197
  tempArtifactsDir: tempDir,
@@ -235,6 +246,22 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
235
246
  assert.match(result.content[0]?.text ?? "", /Legacy foreground output/);
236
247
  });
237
248
 
249
+ it("keeps native foreground output without attempting external grouped delivery when disabled", async () => {
250
+ mockPi.onCall({ output: "Native foreground output" });
251
+ const { executor, events } = makeExecutor({ resultDelivery: false });
252
+
253
+ const result = await executor.execute(
254
+ "single-native-delivery",
255
+ { agent: "worker", task: "Summarize feature" },
256
+ new AbortController().signal,
257
+ undefined,
258
+ makeMinimalCtx(tempDir),
259
+ );
260
+
261
+ assert.equal(events.emitted.some((entry) => entry.channel === "subagent:result-intercom"), false);
262
+ assert.match(result.content[0]?.text ?? "", /Native foreground output/);
263
+ });
264
+
238
265
  it("falls back to legacy foreground output when grouped delivery is not acknowledged", async () => {
239
266
  mockPi.onCall({ output: "Unacknowledged foreground output" });
240
267
  const { executor, events } = makeExecutor({ acknowledgeResults: false });
@@ -271,12 +298,145 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
271
298
  assert.equal((payload.children ?? []).every((child) => /^subagent-[ab]-[a-f0-9]+-[12]$/.test(child.intercomTarget ?? "")), true);
272
299
  assert.match(String(payload.message ?? ""), /Intercom targets below identify child sessions used while they were running/);
273
300
  assert.match(String(payload.message ?? ""), /Run intercom target: subagent-a-[a-f0-9]+-1/);
274
- assert.match(String(payload.message ?? ""), /1\. a — completed/);
275
- assert.match(String(payload.message ?? ""), /2\. b — completed/);
301
+ assert.match(String(payload.message ?? ""), /1\. a — process completed · output present/);
302
+ assert.match(String(payload.message ?? ""), /2\. b — process completed · output present/);
276
303
  assert.match(result.content[0]?.text ?? "", /Delivered parallel subagent results via intercom\./);
277
304
  assert.equal(result.details?.results?.every((entry) => entry.finalOutput === undefined), true);
278
305
  });
279
306
 
307
+ it("keeps a queued top-level parallel child controlled after an early outer rejection", async () => {
308
+ mockPi.onCall({ matchArgIncludes: "task-a", output: "early child", delay: 100 });
309
+ mockPi.onCall({ matchArgIncludes: "task-b", output: "middle child", delay: 400 });
310
+ mockPi.onCall({ matchArgIncludes: "task-c", output: "too late", delay: 10_000 });
311
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b"), makeAgent("c")] });
312
+ let rejected = false;
313
+ const result = await executor.execute(
314
+ "parallel-rejection-cleanup",
315
+ { concurrency: 2, tasks: [{ agent: "a", task: "task-a" }, { agent: "b", task: "task-b" }, { agent: "c", task: "task-c" }] },
316
+ new AbortController().signal,
317
+ (update: { details?: { progress?: Array<{ agent?: string; status?: string }> } }) => {
318
+ if (!rejected && update.details?.progress?.some((entry) => entry.agent === "a" && entry.status === "completed")) {
319
+ rejected = true;
320
+ throw new Error("reject early parallel completion");
321
+ }
322
+ },
323
+ makeMinimalCtx(tempDir),
324
+ );
325
+ assert.equal(rejected, true);
326
+ assert.equal(result.isError, true);
327
+ assert.equal(state.foregroundControls.size, 1, "remaining worker still owns queued scheduling");
328
+ await waitForCallCount(3);
329
+ const liveControl = [...state.foregroundControls.values()][0];
330
+ assert.equal(liveControl?.currentAgent, "c", "later queued child must remain discoverable after launch");
331
+ assert.ok(liveControl?.interrupt, "later queued child must remain interruptible");
332
+ assert.equal(liveControl.interrupt(), true);
333
+ for (let attempt = 0; attempt < 100 && state.foregroundControls.size > 0; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
334
+ assert.equal(state.foregroundControls.size, 0, "control must disappear after workers and children settle");
335
+ });
336
+
337
+ it("keeps a queued chain-parallel child controlled after an early outer rejection", async () => {
338
+ mockPi.onCall({ matchArgIncludes: "chain-task-a", output: "early chain child", delay: 100 });
339
+ mockPi.onCall({ matchArgIncludes: "chain-task-b", output: "middle chain child", delay: 400 });
340
+ mockPi.onCall({ matchArgIncludes: "chain-task-c", output: "too late", delay: 10_000 });
341
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b"), makeAgent("c")] });
342
+ let rejected = false;
343
+ const result = await executor.execute(
344
+ "chain-parallel-rejection-cleanup",
345
+ { chain: [{ concurrency: 2, parallel: [{ agent: "a", task: "chain-task-a" }, { agent: "b", task: "chain-task-b" }, { agent: "c", task: "chain-task-c" }] }] },
346
+ new AbortController().signal,
347
+ (update: { details?: { progress?: Array<{ agent?: string; status?: string }> } }) => {
348
+ if (!rejected && update.details?.progress?.some((entry) => entry.agent === "a" && entry.status === "completed")) {
349
+ rejected = true;
350
+ throw new Error("reject early chain completion");
351
+ }
352
+ },
353
+ makeMinimalCtx(tempDir),
354
+ );
355
+ assert.equal(rejected, true);
356
+ assert.equal(result.isError, true);
357
+ assert.equal(state.foregroundControls.size, 1, "remaining chain worker still owns queued scheduling");
358
+ await waitForCallCount(3);
359
+ const liveControl = [...state.foregroundControls.values()][0];
360
+ assert.equal(liveControl?.currentAgent, "c", "later queued chain child must remain discoverable after launch");
361
+ assert.ok(liveControl?.interrupt, "later queued chain child must remain interruptible");
362
+ assert.equal(liveControl.interrupt(), true);
363
+ for (let attempt = 0; attempt < 100 && state.foregroundControls.size > 0; attempt++) await new Promise((resolve) => setTimeout(resolve, 20));
364
+ assert.equal(state.foregroundControls.size, 0, "chain control must disappear after workers and children settle");
365
+ });
366
+
367
+ it("cleans a rejected sequential chain child and releases foreground ownership", async () => {
368
+ mockPi.onCall({ output: "sequential child output" });
369
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("worker")] });
370
+ let rejected = false;
371
+ let ownedControl: { schedulingOwners?: number; activeChildren?: Map<number, unknown> } | undefined;
372
+ const result = await executor.execute(
373
+ "chain-sequential-rejection-cleanup",
374
+ { chain: [{ agent: "worker", task: "sequential task" }] },
375
+ new AbortController().signal,
376
+ (update: { details?: { progress?: Array<{ status?: string }> } }) => {
377
+ if (rejected || !update.details?.progress?.some((entry) => entry.status === "completed")) return;
378
+ rejected = true;
379
+ ownedControl = [...state.foregroundControls.values()][0];
380
+ throw new Error("reject sequential chain completion");
381
+ },
382
+ makeMinimalCtx(tempDir),
383
+ );
384
+ assert.equal(rejected, true);
385
+ assert.equal(result.isError, true);
386
+ assert.ok(ownedControl);
387
+ assert.equal(ownedControl.schedulingOwners, 0);
388
+ assert.equal(ownedControl.activeChildren?.size, 0);
389
+ assert.equal(state.foregroundControls.size, 0);
390
+ });
391
+
392
+ it("cleans a sequential chain child after an early tool-budget validation return", async () => {
393
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("worker")] });
394
+ const execution = executor.execute(
395
+ "chain-sequential-budget-cleanup",
396
+ { chain: [{ agent: "worker", task: "invalid budget task", toolBudget: { hard: 0 } }] },
397
+ new AbortController().signal,
398
+ undefined,
399
+ makeMinimalCtx(tempDir),
400
+ );
401
+ const ownedControl = [...state.foregroundControls.values()][0];
402
+ assert.ok(ownedControl, "outer scheduling owner should remain visible until execution settles");
403
+ const result = await execution;
404
+ assert.equal(result.isError, true);
405
+ assert.match(result.content[0]?.text ?? "", /toolBudget\.hard must be an integer >= 1/);
406
+ assert.equal(mockPi.callCount(), 0);
407
+ assert.equal(ownedControl.schedulingOwners, 0);
408
+ assert.equal(ownedControl.activeChildren?.size, 0);
409
+ assert.equal(state.foregroundControls.size, 0);
410
+ });
411
+
412
+ it("cleans an attached single rejection and its temporary structured runtime", async () => {
413
+ mockPi.onCall({
414
+ stdoutRaw: [
415
+ { type: "tool_execution_start", toolName: "structured_output", args: { value: { ok: true } } },
416
+ { type: "tool_result_end", message: { role: "toolResult", toolName: "structured_output", content: [{ type: "text", text: "Structured output captured." }] } },
417
+ { type: "tool_execution_end", toolName: "structured_output" },
418
+ ].map((entry) => JSON.stringify(entry)).join("\n") + "\n",
419
+ structuredOutputCapture: { ok: true },
420
+ });
421
+ const { executor, state } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("worker")] });
422
+ let structuredDir: string | undefined;
423
+ const result = await executor.execute(
424
+ "single-rejection-cleanup",
425
+ { agent: "worker", task: "structured task", artifacts: false, outputSchema: { type: "object", properties: { ok: { type: "boolean" } }, required: ["ok"] } },
426
+ new AbortController().signal,
427
+ (update: { details?: { results?: Array<{ structuredOutputPath?: string }>; progress?: Array<{ status?: string }> } }) => {
428
+ const outputPath = update.details?.results?.[0]?.structuredOutputPath;
429
+ if (outputPath) structuredDir = path.dirname(outputPath);
430
+ if (update.details?.progress?.[0]?.status === "completed") throw new Error("reject attached single completion");
431
+ },
432
+ makeMinimalCtx(tempDir),
433
+ );
434
+ assert.equal(result.isError, true);
435
+ assert.equal(state.foregroundControls.size, 0);
436
+ assert.ok(structuredDir);
437
+ assert.equal(fs.existsSync(structuredDir), false, "temporary structured runtime must be removed on rejection");
438
+ });
439
+
280
440
  it("chain runs emit one grouped event containing all executed children", async () => {
281
441
  mockPi.onCall({ output: "Chain child output" });
282
442
  const { executor, events } = makeExecutor({ agents: [makeAgent("a"), makeAgent("b"), makeAgent("c")] });
@@ -300,9 +460,9 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
300
460
  assert.equal(payload.mode, "chain");
301
461
  assert.deepEqual((payload.children ?? []).map((child) => child.agent).sort(), ["a", "b", "c"]);
302
462
  assert.equal((payload.children ?? []).every((child) => /^subagent-[abc]-[a-f0-9]+-[123]$/.test(child.intercomTarget ?? "")), true);
303
- assert.match(String(payload.message ?? ""), /1\. a — completed/);
304
- assert.match(String(payload.message ?? ""), /2\. b — completed/);
305
- assert.match(String(payload.message ?? ""), /3\. c — completed/);
463
+ assert.match(String(payload.message ?? ""), /1\. a — process completed · output present/);
464
+ assert.match(String(payload.message ?? ""), /2\. b — process completed · output present/);
465
+ assert.match(String(payload.message ?? ""), /3\. c — process completed · output present/);
306
466
  assert.match(result.content[0]?.text ?? "", /Delivered chain subagent results via intercom\./);
307
467
  assert.equal(result.details?.results?.every((entry) => entry.finalOutput === undefined), true);
308
468
  });
@@ -631,7 +791,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
631
791
  lastUpdate: 200,
632
792
  cwd: tempDir,
633
793
  sessionFile,
634
- steps: [{ agent: "worker", status: "complete" }],
794
+ steps: [{ agent: "worker", status: "complete", launchContractDigest: "source-launch-contract-digest" }],
635
795
  }, null, 2), "utf-8");
636
796
  const { executor, events } = makeExecutor();
637
797
 
@@ -651,6 +811,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
651
811
  assert.doesNotMatch(result.content[0]?.text ?? "", /Follow:/);
652
812
  const revivedId = result.details?.asyncId;
653
813
  assert.ok(revivedId, "expected revived async id");
814
+ assert.equal(result.details?.sourceLaunchContractDigest, "source-launch-contract-digest");
654
815
  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
816
  assert.equal(startedEvent?.goal, "What changed?");
656
817
  const resultPath = path.join(RESULTS_DIR, `${revivedId}.json`);
@@ -785,7 +946,7 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
785
946
  mockPi.onCall({ output: "first child done" });
786
947
  mockPi.onCall({ output: "second child done" });
787
948
  mockPi.onCall({ output: "revived foreground answer" });
788
- const { executor } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b")] });
949
+ const { executor } = makeExecutor({ bridgeMode: "off", agents: [makeAgent("a"), makeAgent("b", { model: "anthropic/claude-sonnet-4", thinking: "high" })] });
789
950
 
790
951
  const original = await executor.execute(
791
952
  "foreground-resume-original",
@@ -797,6 +958,17 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
797
958
  const runId = original.details?.runId;
798
959
  assert.ok(runId, "expected foreground run id");
799
960
 
961
+ const overridden = await executor.execute(
962
+ "foreground-resume-model-override",
963
+ { action: "resume", id: runId, index: 1, message: "Follow up with b", model: "openai/gpt-5" },
964
+ new AbortController().signal,
965
+ undefined,
966
+ makeMinimalCtx(tempDir),
967
+ );
968
+ assert.equal(overridden.isError, true);
969
+ assert.match(overridden.content[0]?.text ?? "", /reuses the persisted child model/);
970
+ assert.equal(mockPi.callCount(), 2, "rejected model override must not spawn a revival");
971
+
800
972
  const revived = await executor.execute(
801
973
  "foreground-resume",
802
974
  { action: "resume", id: runId, index: 1, message: "Follow up with b" },
@@ -808,10 +980,13 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
808
980
  assert.equal(revived.isError, undefined);
809
981
  assert.match(revived.content[0]?.text ?? "", /Revived foreground subagent from/);
810
982
  assert.match(revived.content[0]?.text ?? "", /Agent: b/);
983
+ assert.equal(revived.details?.sourceLaunchContractDigest, original.details?.results?.[1]?.launchContractDigest);
984
+ assert.ok(revived.details?.sourceLaunchContractDigest, "expected foreground source launch contract digest");
811
985
  const reviveArgs = await readMockCallArgs(2);
812
986
  const selectedSession = original.details?.results?.[1]?.sessionFile;
813
987
  assert.ok(selectedSession, "expected selected child session file");
814
988
  assert.equal(reviveArgs[reviveArgs.indexOf("--session") + 1], selectedSession);
989
+ assert.equal(reviveArgs[reviveArgs.indexOf("--model") + 1], original.details?.results?.[1]?.model);
815
990
  const revivedId = revived.details?.asyncId;
816
991
  assert.ok(revivedId, "expected revived async id");
817
992
  const resultPath = path.join(RESULTS_DIR, `${revivedId}.json`);
@@ -861,7 +1036,10 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
861
1036
  );
862
1037
  assert.equal(blocked.isError, true);
863
1038
  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");
1039
+ // The original and the first revival each spawn Pi asynchronously, so wait for both markers
1040
+ // before asserting the contending revival added no third spawn.
1041
+ await waitForCallCount(2);
1042
+ assert.equal(mockPi.callCount(), 2, "contending foreground revival must not spawn Pi");
865
1043
 
866
1044
  fs.writeFileSync(releasePath, "release", "utf-8");
867
1045
  await waitForFile(path.join(RESULTS_DIR, `${firstRevivedId}.json`));
@@ -1273,9 +1451,10 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
1273
1451
  makeMinimalCtx(tempDir),
1274
1452
  );
1275
1453
  const fleetText = fleet.content[0]?.text ?? "";
1276
- assert.match(fleetText, /Detached foreground runs:/);
1454
+ assert.match(fleetText, /Foreground runs:/);
1277
1455
  assert.ok(fleetText.includes(runId));
1278
- assert.match(fleetText, /recovery: reply to the supervisor request first/);
1456
+ assert.match(fleetText, /running/);
1457
+ assert.match(fleetText, /contact_supervisor/);
1279
1458
 
1280
1459
  const resumed = await executor.execute(
1281
1460
  "foreground-detached-resume",
@@ -1534,7 +1713,28 @@ describe("intercom result delivery cutover", { skip: !available ? "executor not
1534
1713
  const payload = intercomEvents[0]!.payload as { status?: string; summary?: string; message?: string };
1535
1714
  assert.equal(payload.status, "failed");
1536
1715
  assert.match(String(payload.summary ?? ""), /1 completed, 1 failed/);
1537
- assert.match(String(payload.message ?? ""), /Status: failed/);
1716
+ assert.match(String(payload.message ?? ""), /Process status: failed/);
1717
+ assert.match(String(payload.message ?? ""), /Outputs: 2 present \(semantic adequacy unassessed\)/);
1718
+ assert.match(String(payload.message ?? ""), /Inspect that output before retrying/);
1538
1719
  assert.match(result.content[0]?.text ?? "", /Children: 1 completed, 1 failed/);
1539
1720
  });
1721
+
1722
+ it("does not treat a synthetic foreground startup error as child output", async () => {
1723
+ mockPi.onCall({ output: "", stderr: "mock startup failure", exitCode: 1 });
1724
+ const { executor, events } = makeExecutor({ agents: [makeAgent("worker")] });
1725
+
1726
+ await executor.execute(
1727
+ "foreground-synthetic-error",
1728
+ { agent: "worker", task: "synthetic-error-task" },
1729
+ new AbortController().signal,
1730
+ undefined,
1731
+ makeMinimalCtx(tempDir),
1732
+ );
1733
+
1734
+ const intercomEvents = events.emitted.filter((entry) => entry.channel === "subagent:result-intercom");
1735
+ assert.equal(intercomEvents.length, 1);
1736
+ const message = String((intercomEvents[0]!.payload as { message?: string }).message ?? "");
1737
+ assert.match(message, /process failed · output absent/);
1738
+ assert.doesNotMatch(message, /Inspect that output before retrying/);
1739
+ });
1540
1740
  });
@@ -227,6 +227,40 @@ describe("parallel agent execution", { skip: !piAvailable ? "pi packages not ava
227
227
  assert.equal(state.foregroundControls.size, 0);
228
228
  });
229
229
 
230
+ it("exposes stable child indexes for counted top-level parallel results", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, async () => {
231
+ mockPi.onCall({
232
+ steps: [
233
+ { jsonl: [{ type: "tool_execution_start", toolName: "read", args: { path: "a.md" } }], delay: 10 },
234
+ { jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Counted A done" }], model: "mock/test-model", stopReason: "stop", usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } } } }] },
235
+ ],
236
+ });
237
+ mockPi.onCall({
238
+ steps: [
239
+ { jsonl: [{ type: "tool_execution_start", toolName: "read", args: { path: "b.md" } }], delay: 10 },
240
+ { jsonl: [{ type: "message_end", message: { role: "assistant", content: [{ type: "text", text: "Counted B done" }], model: "mock/test-model", stopReason: "stop", usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: { total: 0.001 } } } }] },
241
+ ],
242
+ });
243
+ const executor = makeExecutor();
244
+ const updateIndexes: number[] = [];
245
+
246
+ const result = await executor.execute(
247
+ "counted-parallel-indexes",
248
+ { tasks: [{ agent: "echo", task: "Counted inspection", count: 2 }] },
249
+ new AbortController().signal,
250
+ (update) => {
251
+ for (const row of update.details?.results ?? []) updateIndexes.push(row.index);
252
+ },
253
+ makeMinimalCtx(tempDir),
254
+ );
255
+
256
+ assert.equal(result.isError, undefined);
257
+ assert.deepEqual(result.details?.results.map((row: { index: number }) => row.index), [0, 1]);
258
+ assert.deepEqual([...new Set(result.details?.results.map((row: { index: number }) => row.index))], [0, 1]);
259
+ assert.ok(updateIndexes.includes(0));
260
+ assert.ok(updateIndexes.includes(1));
261
+ assert.ok(updateIndexes.every((index) => Number.isInteger(index)));
262
+ });
263
+
230
264
  it("publishes a durable handoff before cleaning foreground parallel worktrees", { skip: !createSubagentExecutor || process.platform === "win32" ? "executor unavailable or worktree paths differ on Windows" : undefined }, async () => {
231
265
  git(["init"]);
232
266
  git(["config", "user.email", "test@example.com"]);
@@ -454,6 +454,50 @@ describe("renderSubagentResult fork indicator", () => {
454
454
  assert.match(text, /output: \/tmp\/reviewer_output\.md/);
455
455
  });
456
456
 
457
+ it("shows model and thinking on running compact single and multi rows", () => {
458
+ const single = renderSubagentResult!({
459
+ content: [{ type: "text", text: "(running...)" }],
460
+ details: {
461
+ mode: "single",
462
+ results: [{
463
+ agent: "reviewer",
464
+ task: "review",
465
+ exitCode: 0,
466
+ messages: [],
467
+ usage: emptyUsage,
468
+ progress: {
469
+ index: 0,
470
+ agent: "reviewer",
471
+ status: "running",
472
+ task: "review",
473
+ model: "openai-codex/gpt-5.5",
474
+ thinking: "high",
475
+ recentTools: [],
476
+ recentOutput: [],
477
+ toolCount: 1,
478
+ tokens: 42,
479
+ durationMs: 3_000,
480
+ },
481
+ }],
482
+ },
483
+ }, { expanded: false }, theme).render(160).join("\n");
484
+ assert.match(single, /reviewer \(gpt-5\.5 · thinking high\)/);
485
+
486
+ const multi = renderSubagentResult!({
487
+ content: [{ type: "text", text: "(running...)" }],
488
+ details: {
489
+ mode: "parallel",
490
+ totalSteps: 2,
491
+ results: [
492
+ { agent: "scout", task: "scan", exitCode: 0, messages: [], usage: emptyUsage, model: "anthropic/claude-haiku-4-5", thinking: "low", progress: { index: 0, agent: "scout", status: "running", task: "scan", recentTools: [], recentOutput: [], toolCount: 0, tokens: 0, durationMs: 0 } },
493
+ { agent: "worker", task: "fix", exitCode: 0, messages: [], usage: emptyUsage, progress: { index: 1, agent: "worker", status: "running", task: "fix", model: "openai/gpt-5-mini", recentTools: [], recentOutput: [], toolCount: 0, tokens: 0, durationMs: 0 } },
494
+ ],
495
+ },
496
+ }, { expanded: false }, theme).render(160).join("\n");
497
+ assert.match(multi, /Agent 1\/2: scout \(claude-haiku-4-5 · thinking low\)/);
498
+ assert.match(multi, /Agent 2\/2: worker \(gpt-5-mini\)/);
499
+ });
500
+
457
501
  it("keeps running compact result output stable when progress is unchanged", async () => {
458
502
  const result = {
459
503
  content: [{ type: "text" as const, text: "(running...)" }],