@selesai/code 0.5.28 → 0.6.1

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 (341) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +9 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/cli/config-selector.d.ts.map +1 -1
  7. package/dist/cli/config-selector.js +1 -1
  8. package/dist/cli/config-selector.js.map +1 -1
  9. package/dist/cli/credential-print.d.ts +23 -0
  10. package/dist/cli/credential-print.d.ts.map +1 -0
  11. package/dist/cli/credential-print.js +117 -0
  12. package/dist/cli/credential-print.js.map +1 -0
  13. package/dist/cli/startup-ui.d.ts.map +1 -1
  14. package/dist/cli/startup-ui.js +1 -1
  15. package/dist/cli/startup-ui.js.map +1 -1
  16. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  17. package/dist/core/agent-session-runtime.js +3 -0
  18. package/dist/core/agent-session-runtime.js.map +1 -1
  19. package/dist/core/agent-session.d.ts +12 -1
  20. package/dist/core/agent-session.d.ts.map +1 -1
  21. package/dist/core/agent-session.js +20 -14
  22. package/dist/core/agent-session.js.map +1 -1
  23. package/dist/core/compaction/compaction.d.ts.map +1 -1
  24. package/dist/core/compaction/compaction.js +11 -3
  25. package/dist/core/compaction/compaction.js.map +1 -1
  26. package/dist/core/extensions/runner.d.ts +1 -0
  27. package/dist/core/extensions/runner.d.ts.map +1 -1
  28. package/dist/core/extensions/runner.js +11 -0
  29. package/dist/core/extensions/runner.js.map +1 -1
  30. package/dist/core/extensions/types.d.ts +14 -1
  31. package/dist/core/extensions/types.d.ts.map +1 -1
  32. package/dist/core/extensions/types.js.map +1 -1
  33. package/dist/core/footer-data-provider.d.ts +10 -0
  34. package/dist/core/footer-data-provider.d.ts.map +1 -1
  35. package/dist/core/footer-data-provider.js +1 -1
  36. package/dist/core/footer-data-provider.js.map +1 -1
  37. package/dist/core/llama/provider.d.ts.map +1 -1
  38. package/dist/core/llama/provider.js +8 -3
  39. package/dist/core/llama/provider.js.map +1 -1
  40. package/dist/core/model-config.d.ts +30 -0
  41. package/dist/core/model-config.d.ts.map +1 -1
  42. package/dist/core/model-config.js +6 -0
  43. package/dist/core/model-config.js.map +1 -1
  44. package/dist/core/model-registry.d.ts.map +1 -1
  45. package/dist/core/model-registry.js +2 -2
  46. package/dist/core/model-registry.js.map +1 -1
  47. package/dist/core/model-resolver.d.ts +1 -0
  48. package/dist/core/model-resolver.d.ts.map +1 -1
  49. package/dist/core/model-resolver.js +20 -3
  50. package/dist/core/model-resolver.js.map +1 -1
  51. package/dist/core/model-runtime.d.ts +2 -0
  52. package/dist/core/model-runtime.d.ts.map +1 -1
  53. package/dist/core/model-runtime.js +5 -4
  54. package/dist/core/model-runtime.js.map +1 -1
  55. package/dist/core/package-manager.d.ts.map +1 -1
  56. package/dist/core/package-manager.js +13 -6
  57. package/dist/core/package-manager.js.map +1 -1
  58. package/dist/core/remote-catalog-provider.d.ts +1 -1
  59. package/dist/core/remote-catalog-provider.d.ts.map +1 -1
  60. package/dist/core/remote-catalog-provider.js +24 -11
  61. package/dist/core/remote-catalog-provider.js.map +1 -1
  62. package/dist/core/resource-loader.d.ts +15 -0
  63. package/dist/core/resource-loader.d.ts.map +1 -1
  64. package/dist/core/resource-loader.js +66 -9
  65. package/dist/core/resource-loader.js.map +1 -1
  66. package/dist/core/settings-manager.d.ts +1 -1
  67. package/dist/core/settings-manager.d.ts.map +1 -1
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/system-prompt.d.ts.map +1 -1
  70. package/dist/core/system-prompt.js +19 -1
  71. package/dist/core/system-prompt.js.map +1 -1
  72. package/dist/core/system-prompt.test.d.ts +2 -0
  73. package/dist/core/system-prompt.test.d.ts.map +1 -0
  74. package/dist/core/system-prompt.test.js +89 -0
  75. package/dist/core/system-prompt.test.js.map +1 -0
  76. package/dist/core/tools/bash.d.ts +2 -0
  77. package/dist/core/tools/bash.d.ts.map +1 -1
  78. package/dist/core/tools/bash.js +34 -5
  79. package/dist/core/tools/bash.js.map +1 -1
  80. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  81. package/dist/core/tools/tool-definition-wrapper.js +3 -1
  82. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  83. package/dist/defaults/models.json +13 -45
  84. package/dist/defaults/settings.json +1 -2
  85. package/dist/extensions/copy-turn.test.ts +131 -0
  86. package/dist/extensions/copy-turn.ts +6 -1
  87. package/dist/extensions/package.json +0 -1
  88. package/dist/extensions/pi-intercom/CHANGELOG.md +249 -0
  89. package/dist/extensions/pi-intercom/README.md +102 -39
  90. package/dist/extensions/pi-intercom/broker/broker.ts +1233 -36
  91. package/dist/extensions/pi-intercom/broker/client.test.ts +83 -0
  92. package/dist/extensions/pi-intercom/broker/client.ts +315 -12
  93. package/dist/extensions/pi-intercom/broker/extension-state.ts +186 -0
  94. package/dist/extensions/pi-intercom/broker/extension.test.ts +387 -0
  95. package/dist/extensions/pi-intercom/broker/framing.test.ts +114 -0
  96. package/dist/extensions/pi-intercom/broker/framing.ts +82 -24
  97. package/dist/extensions/pi-intercom/broker/paths.test.ts +153 -0
  98. package/dist/extensions/pi-intercom/broker/paths.ts +117 -8
  99. package/dist/extensions/pi-intercom/broker/runtime-claim.test.ts +34 -0
  100. package/dist/extensions/pi-intercom/broker/runtime-claim.ts +21 -0
  101. package/dist/extensions/pi-intercom/broker/spawn.test.ts +160 -23
  102. package/dist/extensions/pi-intercom/broker/spawn.ts +113 -27
  103. package/dist/extensions/pi-intercom/config.test.ts +93 -0
  104. package/dist/extensions/pi-intercom/config.ts +55 -6
  105. package/dist/extensions/pi-intercom/cwd.test.ts +40 -0
  106. package/dist/extensions/pi-intercom/cwd.ts +31 -0
  107. package/dist/extensions/pi-intercom/extension-api.ts +44 -0
  108. package/dist/extensions/pi-intercom/format-context.test.ts +31 -0
  109. package/dist/extensions/pi-intercom/format-context.ts +32 -0
  110. package/dist/extensions/pi-intercom/index.ts +742 -145
  111. package/dist/extensions/pi-intercom/intercom.integration.test.ts +2646 -0
  112. package/dist/extensions/pi-intercom/package.json +15 -5
  113. package/dist/extensions/pi-intercom/reply-tracker.test.ts +134 -0
  114. package/dist/extensions/pi-intercom/reply-tracker.ts +31 -13
  115. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +13 -11
  116. package/dist/extensions/pi-intercom/test/inline-message.test.ts +184 -0
  117. package/dist/extensions/pi-intercom/test/overlay-width.test.ts +66 -0
  118. package/dist/extensions/pi-intercom/types.ts +94 -4
  119. package/dist/extensions/pi-intercom/ui/compose.ts +8 -4
  120. package/dist/extensions/pi-intercom/ui/inline-message.ts +61 -25
  121. package/dist/extensions/pi-intercom/ui/session-list.ts +7 -3
  122. package/dist/extensions/pi-subagents/CHANGELOG.md +88 -0
  123. package/dist/extensions/pi-subagents/LICENSE +21 -0
  124. package/dist/extensions/pi-subagents/README.md +158 -69
  125. package/dist/extensions/pi-subagents/agents/architect.md +5 -4
  126. package/dist/extensions/pi-subagents/agents/builder.md +6 -4
  127. package/dist/extensions/pi-subagents/agents/commentator.md +3 -2
  128. package/dist/extensions/pi-subagents/agents/explorer.md +3 -2
  129. package/dist/extensions/pi-subagents/agents/recapper.md +3 -2
  130. package/dist/extensions/pi-subagents/agents/researcher.md +4 -3
  131. package/dist/extensions/pi-subagents/package-lock.json +2 -2
  132. package/dist/extensions/pi-subagents/package.json +3 -3
  133. package/dist/extensions/pi-subagents/prompts/review-loop.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +22 -988
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +257 -0
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +431 -0
  137. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +144 -0
  138. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +281 -0
  139. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +125 -37
  140. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +4 -0
  141. package/dist/extensions/pi-subagents/src/agents/agents.ts +118 -9
  142. package/dist/extensions/pi-subagents/src/agents/skills.ts +14 -12
  143. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +125 -0
  144. package/dist/extensions/pi-subagents/src/api/delegation.ts +3 -0
  145. package/dist/extensions/pi-subagents/src/api/preflight.ts +17 -13
  146. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +17 -1
  147. package/dist/extensions/pi-subagents/src/extension/index.ts +22 -8
  148. package/dist/extensions/pi-subagents/src/extension/rpc.ts +248 -6
  149. package/dist/extensions/pi-subagents/src/extension/schemas.ts +33 -8
  150. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +32 -14
  151. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +9 -4
  152. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +33 -4
  153. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +86 -21
  154. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +24 -13
  155. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +7 -5
  156. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  157. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +48 -5
  158. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +68 -1
  159. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +56 -5
  160. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +92 -11
  161. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +29 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +388 -29
  163. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +65 -0
  164. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +3 -3
  165. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +214 -60
  166. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +546 -252
  167. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +42 -0
  168. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +553 -154
  169. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +19 -13
  170. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +51 -19
  171. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +3 -1
  172. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  173. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +44 -11
  174. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +8 -0
  175. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +97 -20
  176. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +43 -12
  177. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +52 -4
  178. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +19 -0
  179. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +45 -9
  180. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +71 -0
  181. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +63 -9
  182. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +31 -1
  183. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +101 -0
  184. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +22 -1
  185. package/dist/extensions/pi-subagents/src/runs/shared/usage-budget.ts +65 -0
  186. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +26 -1
  187. package/dist/extensions/pi-subagents/src/shared/settings.ts +17 -1
  188. package/dist/extensions/pi-subagents/src/shared/types.ts +196 -12
  189. package/dist/extensions/pi-subagents/src/shared/utils.ts +102 -5
  190. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +10 -1
  191. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +46 -4
  192. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +5 -3
  193. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +87 -24
  194. package/dist/extensions/pi-subagents/src/tui/fleet.ts +182 -9
  195. package/dist/extensions/pi-subagents/src/tui/render.ts +55 -34
  196. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +14 -7
  197. package/dist/extensions/pi-subagents/src/watchdog/review.ts +5 -4
  198. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +170 -17
  199. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +62 -0
  200. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +41 -1
  201. package/dist/extensions/pi-subagents/src/watchdog/types.ts +10 -0
  202. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +113 -8
  203. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +419 -47
  204. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +84 -0
  205. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +4 -2
  206. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +51 -2
  207. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +62 -22
  208. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +48 -0
  209. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +8 -6
  210. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +231 -19
  211. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +48 -7
  212. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +44 -0
  213. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +199 -17
  214. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +898 -16
  215. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +112 -23
  216. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -5
  217. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  218. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +18 -2
  219. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +3 -3
  220. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +3 -1
  221. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  222. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +198 -6
  223. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +161 -1
  224. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +47 -2
  225. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +60 -0
  226. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +8 -0
  227. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +63 -0
  228. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +136 -0
  229. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +1 -1
  230. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +6 -0
  231. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +1 -1
  232. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +48 -48
  233. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +23 -0
  234. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +27 -0
  235. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +28 -2
  236. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +1 -0
  237. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +176 -8
  238. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +164 -5
  239. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +41 -1
  240. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +17 -12
  241. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +18 -2
  242. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +20 -1
  243. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +30 -2
  244. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +39 -2
  245. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +22 -0
  246. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +103 -0
  247. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +18 -0
  248. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +48 -0
  249. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +13 -1
  250. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +29 -4
  251. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +223 -4
  252. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +30 -0
  253. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +52 -0
  254. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +46 -2
  255. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +91 -1
  256. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  257. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +1 -1
  258. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +78 -0
  259. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +41 -0
  260. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +77 -0
  261. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +213 -0
  262. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +26 -1
  263. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +61 -10
  264. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +1 -0
  265. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +210 -1
  266. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +33 -0
  267. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +32 -0
  268. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +1 -1
  269. package/dist/main.d.ts.map +1 -1
  270. package/dist/main.js +43 -0
  271. package/dist/main.js.map +1 -1
  272. package/dist/modes/interactive/components/custom-message.d.ts +3 -1
  273. package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
  274. package/dist/modes/interactive/components/custom-message.js +10 -2
  275. package/dist/modes/interactive/components/custom-message.js.map +1 -1
  276. package/dist/modes/interactive/components/extension-editor.d.ts +1 -2
  277. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  278. package/dist/modes/interactive/components/extension-editor.js +16 -46
  279. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  280. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  281. package/dist/modes/interactive/components/model-selector.js +4 -1
  282. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  283. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  284. package/dist/modes/interactive/components/scoped-models-selector.js +22 -13
  285. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  286. package/dist/modes/interactive/external-editor.d.ts +12 -0
  287. package/dist/modes/interactive/external-editor.d.ts.map +1 -0
  288. package/dist/modes/interactive/external-editor.js +37 -0
  289. package/dist/modes/interactive/external-editor.js.map +1 -0
  290. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  291. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  292. package/dist/modes/interactive/interactive-mode.js +72 -64
  293. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  294. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  295. package/dist/modes/rpc/rpc-mode.js +14 -0
  296. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  297. package/dist/skills/ponytail/SKILL.md +1 -3
  298. package/dist/utils/clipboard.d.ts.map +1 -1
  299. package/dist/utils/clipboard.js +19 -8
  300. package/dist/utils/clipboard.js.map +1 -1
  301. package/dist/utils/version-check.d.ts.map +1 -1
  302. package/dist/utils/version-check.js +1 -1
  303. package/dist/utils/version-check.js.map +1 -1
  304. package/docs/compaction.md +1 -1
  305. package/docs/custom-provider.md +14 -5
  306. package/docs/environment-variables.md +86 -0
  307. package/docs/extensions.md +15 -5
  308. package/docs/index.md +1 -0
  309. package/docs/models.md +9 -2
  310. package/docs/plans/subagent-delegation/phase-0-correctness.md +265 -0
  311. package/docs/plans/subagent-delegation/phase-1-behavioral-contract.md +486 -0
  312. package/docs/plans/subagent-delegation/phase-2-context-controls.md +282 -0
  313. package/docs/plans/subagent-delegation/phase-3-advisory-routing.md +362 -0
  314. package/docs/plans/subagent-delegation/phase-4-optional-enforcement.md +381 -0
  315. package/docs/providers.md +21 -2
  316. package/docs/rpc.md +23 -6
  317. package/docs/session-format.md +2 -0
  318. package/docs/settings.md +1 -1
  319. package/docs/usage.md +0 -13
  320. package/examples/extensions/custom-compaction.ts +5 -2
  321. package/examples/extensions/custom-provider-anthropic/index.ts +9 -3
  322. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  323. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  324. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  325. package/examples/extensions/gondolin/package-lock.json +2 -2
  326. package/examples/extensions/gondolin/package.json +1 -1
  327. package/examples/extensions/handoff.ts +11 -3
  328. package/examples/extensions/message-renderer.ts +3 -3
  329. package/examples/extensions/sandbox/package-lock.json +2 -2
  330. package/examples/extensions/sandbox/package.json +1 -1
  331. package/examples/extensions/summarize.ts +5 -2
  332. package/examples/extensions/with-deps/package-lock.json +2 -2
  333. package/examples/extensions/with-deps/package.json +1 -1
  334. package/examples/sdk/12-full-control.ts +3 -1
  335. package/package.json +16 -7
  336. package/dist/extensions/caveman/caveman-instructions.cjs +0 -11
  337. package/dist/extensions/caveman/index.js +0 -118
  338. package/dist/extensions/caveman/package.json +0 -8
  339. package/dist/extensions/caveman/test/extension.test.js +0 -203
  340. package/dist/extensions/caveman/test/helpers.test.js +0 -58
  341. package/dist/skills/caveman/SKILL.md +0 -50
@@ -45,6 +45,9 @@ import {
45
45
  hasEmptyTerminalAssistantResponse,
46
46
  extractToolArgsPreview,
47
47
  extractTextFromContent,
48
+ boundStreamedRecentTools,
49
+ boundStreamedRecentOutput,
50
+ boundStreamedToolCalls,
48
51
  } from "../../shared/utils.ts";
49
52
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
50
53
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
@@ -52,17 +55,26 @@ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
52
55
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
53
56
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
54
57
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
55
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
56
- import { decodeSubagentCapabilityCeiling, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
58
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
59
+ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
60
+ import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
57
61
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
58
62
  import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
63
+ import { formatProcessSignalError, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
59
64
  import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
60
- import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
65
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatBoundedPersistenceFallback, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
61
66
  import {
62
67
  buildModelCandidates,
63
68
  formatModelAttemptNote,
64
69
  isRetryableModelFailure,
65
70
  } from "../shared/model-fallback.ts";
71
+ import {
72
+ SUBAGENT_STARTUP_RETRY_DELAYS_MS,
73
+ formatSubagentStartupRetryExhaustedError,
74
+ formatSubagentStartupRetryNote,
75
+ isRetryableSubagentStartupFailure,
76
+ waitForSubagentStartupRetry,
77
+ } from "../shared/subagent-startup-retry.ts";
66
78
  import {
67
79
  createMutatingFailureState,
68
80
  didMutatingToolFail,
@@ -111,6 +123,47 @@ function sumUsage(target: Usage, source: Usage): void {
111
123
  target.turns += source.turns;
112
124
  }
113
125
 
126
+ function persistSingleResultMetadata(input: {
127
+ metadataPath?: string;
128
+ enabled: boolean;
129
+ runId?: string;
130
+ agent: string;
131
+ task: string;
132
+ result: SingleResult;
133
+ }): void {
134
+ if (!input.enabled || !input.metadataPath) return;
135
+ const target = input.result;
136
+ writeMetadata(input.metadataPath, {
137
+ runId: input.runId,
138
+ agent: input.agent,
139
+ task: input.task,
140
+ exitCode: target.exitCode,
141
+ processSignal: target.processSignal,
142
+ usage: target.usage,
143
+ model: target.model,
144
+ attemptedModels: target.attemptedModels,
145
+ modelAttempts: target.modelAttempts,
146
+ durationMs: target.progressSummary?.durationMs,
147
+ toolCount: target.progressSummary?.toolCount,
148
+ error: target.error,
149
+ agentContract: target.agentContract,
150
+ launchContractDigest: target.launchContractDigest,
151
+ launchResolvedExtensions: target.launchResolvedExtensions,
152
+ runtimeAcknowledgedExtensions: target.runtimeAcknowledgedExtensions,
153
+ execution: target.execution,
154
+ acceptance: target.acceptance,
155
+ capabilityCeiling: target.capabilityCeiling,
156
+ capabilityAudit: target.capabilityAudit,
157
+ review: target.review,
158
+ effects: target.effects,
159
+ transcriptPath: target.transcriptPath,
160
+ transcriptError: target.transcriptError,
161
+ skills: target.skills,
162
+ skillsWarning: target.skillsWarning,
163
+ timestamp: Date.now(),
164
+ });
165
+ }
166
+
114
167
  function formatTimeoutMessage(timeoutMs: number): string {
115
168
  return `Subagent timed out after ${timeoutMs}ms.`;
116
169
  }
@@ -138,6 +191,21 @@ function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): Acc
138
191
  };
139
192
  }
140
193
 
194
+ function buildInterruptedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
195
+ const pending = buildPendingAcceptanceLedger(acceptance);
196
+ if (acceptance.level === "none") {
197
+ pending.status = "not-required";
198
+ pending.evidenceStatus = "not-required";
199
+ } else {
200
+ pending.runtimeChecks = [{
201
+ id: "interrupted",
202
+ status: "not-applicable",
203
+ message: "Acceptance was not evaluated because the subagent was interrupted.",
204
+ }];
205
+ }
206
+ return pending;
207
+ }
208
+
141
209
  function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
142
210
  if (lines.length === 0) return;
143
211
  progress.recentOutput.push(...lines.filter((line) => line.trim()));
@@ -161,8 +229,8 @@ function snapshotProgress(progress: AgentProgress): AgentProgress {
161
229
  return {
162
230
  ...progress,
163
231
  skills: progress.skills ? [...progress.skills] : undefined,
164
- recentTools: progress.recentTools.map((tool) => ({ ...tool })),
165
- recentOutput: [...progress.recentOutput],
232
+ recentTools: boundStreamedRecentTools(progress.recentTools),
233
+ recentOutput: boundStreamedRecentOutput(progress.recentOutput),
166
234
  };
167
235
  }
168
236
 
@@ -188,6 +256,20 @@ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleRe
188
256
  };
189
257
  }
190
258
 
259
+ /**
260
+ * Streaming variant of snapshotResult for `onUpdate` progress events: it drops the
261
+ * unbounded `messages` transcript in favour of compact tool-call summaries so a
262
+ * single `tool_execution_update` line stays well under the child-stdout protocol
263
+ * cap (`MAX_CHILD_PENDING_LINE_BYTES`). Non-streaming consumers such as
264
+ * foreground detach receipts and terminal consumers use snapshotResult directly.
265
+ */
266
+ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): SingleResult {
267
+ const snapshot = snapshotResult(result, progress);
268
+ snapshot.messages = undefined;
269
+ snapshot.toolCalls = boundStreamedToolCalls(result);
270
+ return snapshot;
271
+ }
272
+
191
273
  async function runSingleAttempt(
192
274
  runtimeCwd: string,
193
275
  agent: AgentConfig,
@@ -220,7 +302,7 @@ async function runSingleAttempt(
220
302
  childIndex: options.index ?? 0,
221
303
  })
222
304
  : undefined;
223
- const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
305
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
224
306
  baseArgs: ["--offline", "--mode", "json", "-p"],
225
307
  task,
226
308
  sessionEnabled: shared.sessionEnabled,
@@ -268,7 +350,9 @@ async function runSingleAttempt(
268
350
  structuredOutput: Boolean(options.structuredOutput),
269
351
  capabilityCeiling: options.capabilityCeiling,
270
352
  inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
353
+ agentName: agent.name,
271
354
  });
355
+ const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
272
356
  const launchContractDigest = launchBindingDigest({
273
357
  definitionDigest: agentDefinitionDigest(agent),
274
358
  task: shared.originalTask ?? task,
@@ -288,11 +372,14 @@ async function runSingleAttempt(
288
372
  ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
289
373
  });
290
374
  const result: SingleResult = withRunContext({
375
+ index: options.index ?? 0,
291
376
  agent: agent.name,
292
377
  task: shared.originalTask ?? task,
293
378
  ...(options.agentContract ? { agentContract: options.agentContract } : {}),
294
379
  launchContractDigest,
380
+ launchResolvedExtensions,
295
381
  exitCode: 0,
382
+ outputState: "absent",
296
383
  messages: [],
297
384
  usage: emptyUsage(),
298
385
  model: modelArg,
@@ -337,6 +424,10 @@ async function runSingleAttempt(
337
424
  recentOutput: [...shared.attemptNotes],
338
425
  toolCount: 0,
339
426
  tokens: 0,
427
+ ...(modelArg ? { model: modelArg } : {}),
428
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
429
+ inputTokens: 0,
430
+ outputTokens: 0,
340
431
  durationMs: 0,
341
432
  lastActivityAt: startTime,
342
433
  };
@@ -371,7 +462,7 @@ async function runSingleAttempt(
371
462
  });
372
463
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
373
464
  let processClosed = false;
374
- let settled = false;
465
+ let lifecycleFinished = false;
375
466
  let detached = false;
376
467
  let intercomStarted = false;
377
468
  let assistantError: string | undefined;
@@ -410,19 +501,43 @@ async function runSingleAttempt(
410
501
  }
411
502
  };
412
503
 
413
- const detachForIntercom = () => {
414
- detached = true;
415
- processClosed = true;
416
- result.detached = true;
417
- result.detachedReason = "intercom coordination";
418
- progress.status = "detached";
419
- progress.durationMs = Date.now() - startTime;
420
- result.progressSummary = {
421
- toolCount: progress.toolCount,
422
- tokens: progress.tokens,
423
- durationMs: progress.durationMs,
504
+ const detachForeground = (reason: string): boolean => {
505
+ if (detached || processClosed || lifecycleFinished || options.signal?.aborted) return false;
506
+ const receiptProgress = snapshotProgress(progress);
507
+ receiptProgress.status = "detached";
508
+ receiptProgress.durationMs = Date.now() - startTime;
509
+ const receipt = snapshotResult(result, receiptProgress);
510
+ receipt.exitCode = -2;
511
+ receipt.detached = true;
512
+ receipt.detachedReason = reason;
513
+ receipt.finalOutput = reason === "intercom coordination"
514
+ ? "Detached for intercom coordination before task completion."
515
+ : reason === "user request"
516
+ ? "Detached at user request before task completion."
517
+ : `Detached for ${reason} before task completion.`;
518
+ receipt.outputMode = options.outputMode ?? "inline";
519
+ if (options.outputPath) {
520
+ receipt.outputSaveError = reason === "user request"
521
+ ? "Output file was not finalized because the subagent detached at user request."
522
+ : `Output file was not finalized because the subagent detached for ${reason}.`;
523
+ }
524
+ receipt.progressSummary = {
525
+ toolCount: receiptProgress.toolCount,
526
+ tokens: receiptProgress.tokens,
527
+ durationMs: receiptProgress.durationMs,
424
528
  };
425
- finish(-2);
529
+ // The attempt is not detached until the outer coordinator has accepted and
530
+ // synchronously published the receipt. A persistence/callback failure must
531
+ // leave this attempt attached and abortable rather than orphaning it.
532
+ let accepted = false;
533
+ try {
534
+ accepted = options.onDetachReceipt?.(receipt) === true;
535
+ } catch {
536
+ return false;
537
+ }
538
+ if (!accepted) return false;
539
+ detached = true;
540
+ return true;
426
541
  };
427
542
 
428
543
  // If the child emits a terminal assistant stop but never exits,
@@ -463,9 +578,9 @@ async function runSingleAttempt(
463
578
  armWatchdogTail();
464
579
  return;
465
580
  }
466
- if (childExited || finalDrainTimer || settled || processClosed || detached) return;
581
+ if (childExited || finalDrainTimer || lifecycleFinished || processClosed) return;
467
582
  finalDrainTimer = setTimeout(() => {
468
- if (settled || processClosed || detached) return;
583
+ if (lifecycleFinished || processClosed) return;
469
584
  const termSent = trySignalChild(proc, "SIGTERM");
470
585
  if (!termSent) return;
471
586
  forcedTerminationSignal = true;
@@ -473,7 +588,7 @@ async function runSingleAttempt(
473
588
  result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
474
589
  }
475
590
  finalHardKillTimer = setTimeout(() => {
476
- if (settled || processClosed || detached) return;
591
+ if (lifecycleFinished || processClosed) return;
477
592
  forcedTerminationSignal = trySignalChild(proc, "SIGKILL") || forcedTerminationSignal;
478
593
  }, HARD_KILL_MS);
479
594
  finalHardKillTimer.unref?.();
@@ -481,7 +596,7 @@ async function runSingleAttempt(
481
596
  finalDrainTimer.unref?.();
482
597
  };
483
598
  function armWatchdogTail(): void {
484
- if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled || processClosed || detached) return;
599
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || lifecycleFinished || processClosed) return;
485
600
  watchdogTailTimer = setTimeout(() => {
486
601
  watchdogTailTimer = undefined;
487
602
  updateChildWatchdogState({
@@ -507,7 +622,7 @@ async function runSingleAttempt(
507
622
  };
508
623
 
509
624
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
510
- if (!options.allowIntercomDetach || detached || processClosed) return;
625
+ if (!options.allowIntercomDetach || processClosed) return;
511
626
  if (!payload || typeof payload !== "object") return;
512
627
  const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
513
628
  const requestId = event.requestId;
@@ -518,13 +633,13 @@ async function runSingleAttempt(
518
633
  if (typeof event.agent === "string" && event.agent !== agent.name) return;
519
634
  if (typeof event.childIndex === "number" && event.childIndex !== (options.index ?? 0)) return;
520
635
  } else if (!intercomStarted) return;
521
- options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true, runId: options.runId, agent: agent.name, childIndex: options.index ?? 0 });
522
- detachForIntercom();
636
+ const accepted = detachForeground("intercom coordination");
637
+ options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted, runId: options.runId, agent: agent.name, childIndex: options.index ?? 0 });
523
638
  });
524
639
 
525
640
  const finish = (code: number) => {
526
- if (settled) return;
527
- settled = true;
641
+ if (lifecycleFinished) return;
642
+ lifecycleFinished = true;
528
643
  clearFinalDrainTimers();
529
644
  clearWatchdogTailTimer();
530
645
  clearStdioGuard();
@@ -609,7 +724,7 @@ async function runSingleAttempt(
609
724
  };
610
725
  const requestTurnBudgetAbort = (turnCount: number) => {
611
726
  const budget = options.turnBudget;
612
- if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || settled || detached) return;
727
+ if (!budget || result.timedOut || result.turnBudgetExceeded || interruptedByControl || processClosed || lifecycleFinished) return;
613
728
  const message = turnBudgetExceededMessage(budget, turnCount);
614
729
  result.turnBudgetExceeded = true;
615
730
  result.wrapUpRequested = true;
@@ -622,12 +737,12 @@ async function runSingleAttempt(
622
737
  fireUpdate();
623
738
  trySignalChild(proc, "SIGINT");
624
739
  turnBudgetTerminationTimer = setTimeout(() => {
625
- if (processClosed || settled || detached || result.timedOut) return;
740
+ if (processClosed || lifecycleFinished || result.timedOut) return;
626
741
  trySignalChild(proc, "SIGTERM");
627
742
  }, 1000);
628
743
  turnBudgetTerminationTimer.unref?.();
629
744
  turnBudgetHardKillTimer = setTimeout(() => {
630
- if (processClosed || settled || detached || result.timedOut) return;
745
+ if (processClosed || lifecycleFinished || result.timedOut) return;
631
746
  trySignalChild(proc, "SIGKILL");
632
747
  }, 4000);
633
748
  turnBudgetHardKillTimer.unref?.();
@@ -683,7 +798,7 @@ async function runSingleAttempt(
683
798
  const emitUpdateSnapshot = (text: string) => {
684
799
  if (!options.onUpdate || processClosed) return;
685
800
  const progressSnapshot = snapshotProgress(progress);
686
- const resultSnapshot = snapshotResult(result, progressSnapshot);
801
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
687
802
  const controlEvents = drainPendingControlEvents();
688
803
  options.onUpdate({
689
804
  content: [{ type: "text", text }],
@@ -703,6 +818,7 @@ async function runSingleAttempt(
703
818
  emitUpdateSnapshot(output || "(running...)");
704
819
  };
705
820
 
821
+ const rawStdoutTail = createBoundedByteTail();
706
822
  const processLine = (line: string) => {
707
823
  if (!line.trim()) return;
708
824
  jsonlWriter.writeLine(line);
@@ -710,6 +826,7 @@ async function runSingleAttempt(
710
826
  try {
711
827
  evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
712
828
  } catch {
829
+ rawStdoutTail.push(`${line}\n`);
713
830
  shared.transcriptWriter?.writeStdoutLine(line);
714
831
  // Non-JSON stdout lines are expected; only structured events are parsed.
715
832
  return;
@@ -805,8 +922,13 @@ async function runSingleAttempt(
805
922
  result.usage.cacheWrite += u.cacheWrite || 0;
806
923
  result.usage.cost += u.cost?.total || 0;
807
924
  progress.tokens = result.usage.input + result.usage.output;
925
+ progress.inputTokens = result.usage.input;
926
+ progress.outputTokens = result.usage.output;
927
+ }
928
+ if (evt.message.model) {
929
+ progress.model = evt.message.model;
930
+ if (!result.model) result.model = evt.message.model;
808
931
  }
809
- if (!result.model && evt.message.model) result.model = evt.message.model;
810
932
  if (evt.message.errorMessage) assistantError = evt.message.errorMessage;
811
933
  const assistantText = extractTextFromContent(evt.message.content);
812
934
  appendRecentOutput(progress, assistantText.split("\n").slice(-10));
@@ -857,7 +979,7 @@ async function runSingleAttempt(
857
979
 
858
980
  if (controlConfig.enabled) {
859
981
  activityTimer = setInterval(() => {
860
- if (processClosed || settled || detached) return;
982
+ if (processClosed || lifecycleFinished) return;
861
983
  const now = Date.now();
862
984
  if (updateActivityState(now)) {
863
985
  progress.durationMs = now - startTime;
@@ -869,7 +991,7 @@ async function runSingleAttempt(
869
991
 
870
992
  if (attemptTimeout) {
871
993
  timeoutTimer = setTimeout(() => {
872
- if (processClosed || settled || detached || interruptedByControl) return;
994
+ if (processClosed || lifecycleFinished || interruptedByControl) return;
873
995
  result.timedOut = true;
874
996
  result.error = attemptTimeout.message;
875
997
  result.finalOutput = attemptTimeout.message;
@@ -879,12 +1001,12 @@ async function runSingleAttempt(
879
1001
  fireUpdate();
880
1002
  trySignalChild(proc, "SIGINT");
881
1003
  timeoutTerminationTimer = setTimeout(() => {
882
- if (processClosed || settled || detached) return;
1004
+ if (processClosed || lifecycleFinished) return;
883
1005
  trySignalChild(proc, "SIGTERM");
884
1006
  }, 1000);
885
1007
  timeoutTerminationTimer.unref?.();
886
1008
  timeoutHardKillTimer = setTimeout(() => {
887
- if (processClosed || settled || detached) return;
1009
+ if (processClosed || lifecycleFinished) return;
888
1010
  trySignalChild(proc, "SIGKILL");
889
1011
  }, 4000);
890
1012
  timeoutHardKillTimer.unref?.();
@@ -928,76 +1050,46 @@ async function runSingleAttempt(
928
1050
  clearFinalDrainTimers();
929
1051
  });
930
1052
  proc.on("close", (code, signal) => {
1053
+ if (lifecycleFinished) return;
1054
+ processClosed = true;
931
1055
  clearFinalDrainTimers();
932
1056
  clearStdioGuard();
933
1057
  void jsonlWriter.close().catch(() => {
934
1058
  // JSONL artifact flush is best effort.
935
1059
  });
936
1060
  const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
1061
+ result.runtimeAcknowledgedExtensions = readRuntimeAcknowledgedExtensions(runtimeAcknowledgedExtensionsPath);
937
1062
  cleanupTempDir(tempDir);
938
1063
  stdoutReader.end();
939
1064
  stderrReader.end();
940
1065
  const stderr = stderrTail.text();
1066
+ const rawStdout = rawStdoutTail.text();
941
1067
  let closeError = result.error ?? toolDiagnosticError ?? assistantError;
942
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1068
+ const forcedDrainAfterFinalSuccess = Boolean(forcedTerminationSignal || signal) && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
1069
+ if (signal) result.processSignal = signal;
1070
+ if (!closeError && isUnexplainedProcessSignal({
1071
+ processSignal: signal,
1072
+ interrupted: result.interrupted,
1073
+ timedOut: result.timedOut,
1074
+ stopped: result.stopped,
1075
+ turnBudgetExceeded: result.turnBudgetExceeded,
1076
+ forcedDrainAfterFinalSuccess,
1077
+ })) {
1078
+ closeError = formatProcessSignalError(signal!);
1079
+ }
1080
+ if (code !== 0 && rawStdout.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
1081
+ closeError = rawStdout.trim();
1082
+ }
943
1083
  if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
944
1084
  closeError = stderr.trim();
945
1085
  }
946
1086
  const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
947
- if (detached) {
948
- const recoveredProgress = snapshotProgress(progress);
949
- const recoveredResult = snapshotResult(result, recoveredProgress);
950
- if (!recoveredResult.error && closeError) recoveredResult.error = closeError;
951
- recoveredResult.exitCode = recoveredResult.error && finalCode === 0 ? 1 : finalCode;
952
- recoveredProgress.status = recoveredResult.exitCode === 0 ? "completed" : "failed";
953
- recoveredProgress.durationMs = Date.now() - startTime;
954
- if (recoveredResult.error) recoveredProgress.error = recoveredResult.error;
955
- recoveredResult.progressSummary = {
956
- toolCount: recoveredProgress.toolCount,
957
- tokens: recoveredProgress.tokens,
958
- durationMs: recoveredProgress.durationMs,
959
- };
960
- const acceptanceOutput = getFinalOutput(recoveredResult.messages ?? []).trim()
961
- || recoveredResult.error
962
- || recoveredResult.finalOutput
963
- || "Detached child exited without final output.";
964
- acceptanceOutputByResult.set(recoveredResult, acceptanceOutput);
965
- let fullOutput = stripAcceptanceReport(acceptanceOutput);
966
- fullOutput = fullOutput.trim() || recoveredResult.error || recoveredResult.finalOutput || "Detached child exited without final output.";
967
- recoveredResult.outputMode = options.outputMode ?? "inline";
968
- if (options.outputPath && recoveredResult.exitCode === 0) {
969
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
970
- fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
971
- recoveredResult.savedOutputPath = resolvedOutput.savedPath;
972
- recoveredResult.outputSaveError = resolvedOutput.saveError;
973
- if (resolvedOutput.savedPath) {
974
- recoveredResult.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
975
- } else {
976
- recoveredResult.exitCode = 1;
977
- recoveredResult.error = `Output file was not finalized after detached child exit: ${resolvedOutput.saveError ?? options.outputPath}`;
978
- recoveredProgress.status = "failed";
979
- recoveredProgress.error = recoveredResult.error;
980
- }
981
- }
982
- recoveredResult.finalOutput = options.outputMode === "file-only" && recoveredResult.savedOutputPath && recoveredResult.outputReference
983
- ? recoveredResult.outputReference.message
984
- : fullOutput;
985
- if (recoveredResult.artifactPaths && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {
986
- try {
987
- writeArtifact(recoveredResult.artifactPaths.outputPath, fullOutput);
988
- } catch {
989
- // Detached children may outlive test/temp cleanup; recovered status is best-effort.
990
- }
991
- }
992
- options.onDetachedExit?.(recoveredResult);
993
- finish(-2);
994
- return;
995
- }
996
1087
  if (!result.error && closeError) result.error = closeError;
997
- processClosed = true;
998
1088
  finish(finalCode);
999
1089
  });
1000
1090
  proc.on("error", (error) => {
1091
+ if (lifecycleFinished) return;
1092
+ processClosed = true;
1001
1093
  clearFinalDrainTimers();
1002
1094
  clearStdioGuard();
1003
1095
  void jsonlWriter.close().catch(() => {
@@ -1014,7 +1106,7 @@ async function runSingleAttempt(
1014
1106
 
1015
1107
  if (options.signal) {
1016
1108
  const kill = () => {
1017
- if (processClosed || detached) return;
1109
+ if (processClosed || lifecycleFinished) return;
1018
1110
  proc.kill("SIGTERM");
1019
1111
  setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
1020
1112
  };
@@ -1027,7 +1119,7 @@ async function runSingleAttempt(
1027
1119
 
1028
1120
  if (options.interruptSignal) {
1029
1121
  const interrupt = () => {
1030
- if (processClosed || detached || settled) return;
1122
+ if (processClosed || lifecycleFinished) return;
1031
1123
  if (result.timedOut) return;
1032
1124
  interruptedByControl = true;
1033
1125
  clearTimeoutTimers();
@@ -1039,7 +1131,7 @@ async function runSingleAttempt(
1039
1131
  fireUpdate();
1040
1132
  trySignalChild(proc, "SIGINT");
1041
1133
  setTimeout(() => {
1042
- if (settled || processClosed || detached) return;
1134
+ if (lifecycleFinished || processClosed) return;
1043
1135
  trySignalChild(proc, "SIGTERM");
1044
1136
  }, 1000).unref?.();
1045
1137
  };
@@ -1049,6 +1141,16 @@ async function runSingleAttempt(
1049
1141
  removeInterruptListener = () => options.interruptSignal?.removeEventListener("abort", interrupt);
1050
1142
  }
1051
1143
  }
1144
+
1145
+ // Publish only after every callback and cleanup guard captured by detach or
1146
+ // later lifecycle events has initialized. Consumers may invoke synchronously.
1147
+ try {
1148
+ options.onDetachReady?.((reason = "user request") => detachForeground(reason));
1149
+ } catch (error) {
1150
+ // A consumer callback is advisory. Keep the child attached and observable
1151
+ // rather than rejecting this attempt and orphaning its live process.
1152
+ appendRecentOutput(progress, [`Foreground detach callback failed: ${error instanceof Error ? error.message : String(error)}`]);
1153
+ }
1052
1154
  });
1053
1155
  result.exitCode = exitCode;
1054
1156
  if (interruptedByControl) {
@@ -1066,16 +1168,6 @@ async function runSingleAttempt(
1066
1168
  };
1067
1169
  return result;
1068
1170
  }
1069
- if (result.detached) {
1070
- result.exitCode = -2;
1071
- result.finalOutput = "Detached for intercom coordination before task completion.";
1072
- result.outputMode = options.outputMode ?? "inline";
1073
- if (options.outputPath) {
1074
- result.outputSaveError = "Output file was not finalized because the subagent detached for intercom coordination.";
1075
- }
1076
- return result;
1077
- }
1078
-
1079
1171
  if (result.error && result.exitCode === 0) {
1080
1172
  result.exitCode = 1;
1081
1173
  }
@@ -1137,6 +1229,7 @@ async function runSingleAttempt(
1137
1229
 
1138
1230
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1139
1231
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
1232
+ result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1140
1233
  if (result.timedOut) {
1141
1234
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1142
1235
  fullOutput = fullOutput.trim()
@@ -1189,26 +1282,41 @@ async function runSingleAttempt(
1189
1282
  reason: "completion_guard",
1190
1283
  }));
1191
1284
  }
1192
- if (options.outputPath && result.exitCode === 0) {
1285
+ // Persist whenever an output path is configured and meaningful child output
1286
+ // exists, not only for successful runs: failed children with output keep a
1287
+ // readable result file and a saved-output reference instead of raw inline
1288
+ // output. Synthetic failures without output (startup errors, empty responses)
1289
+ // stay outputState "absent" and are not promoted to a bogus saved reference.
1290
+ if (options.outputPath && (fullOutput.trim() || result.exitCode === 0)) {
1193
1291
  const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
1194
1292
  fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
1195
1293
  result.savedOutputPath = resolvedOutput.savedPath;
1196
1294
  result.outputSaveError = resolvedOutput.saveError;
1197
1295
  if (resolvedOutput.savedPath) {
1198
1296
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1297
+ if (result.outputState === "absent") result.outputState = "unknown";
1199
1298
  }
1200
1299
  }
1201
1300
  artifactOutputByResult.set(result, fullOutput);
1202
1301
  acceptanceOutputByResult.set(result, acceptanceOutput);
1203
1302
  result.outputMode = options.outputMode ?? "inline";
1204
- result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
1205
- ? result.outputReference.message
1206
- : fullOutput;
1303
+ const hasSavedReference = result.savedOutputPath && result.outputReference ? true : false;
1304
+ result.finalOutput = hasSavedReference && (options.outputMode === "file-only" || result.exitCode !== 0)
1305
+ ? result.outputReference!.message
1306
+ : result.outputSaveError && options.outputPath
1307
+ ? formatBoundedPersistenceFallback({
1308
+ error: result.outputSaveError,
1309
+ outputPath: options.outputPath,
1310
+ fullOutput,
1311
+ exitCode: result.exitCode,
1312
+ processError: result.error,
1313
+ })
1314
+ : fullOutput;
1207
1315
  result.controlEvents = allControlEvents.length ? allControlEvents : undefined;
1208
1316
  if (options.onUpdate) {
1209
1317
  const finalText = result.finalOutput || result.error || "(no output)";
1210
1318
  const progressSnapshot = snapshotProgress(progress);
1211
- const resultSnapshot = snapshotResult(result, progressSnapshot);
1319
+ const resultSnapshot = snapshotStreamResult(result, progressSnapshot);
1212
1320
  options.onUpdate({
1213
1321
  content: [{ type: "text", text: finalText }],
1214
1322
  details: {
@@ -1225,7 +1333,7 @@ async function runSingleAttempt(
1225
1333
  /**
1226
1334
  * Run a subagent synchronously (blocking until complete)
1227
1335
  */
1228
- export async function runSync(
1336
+ async function runSyncCompletion(
1229
1337
  runtimeCwd: string,
1230
1338
  agents: AgentConfig[],
1231
1339
  agentName: string,
@@ -1234,11 +1342,12 @@ export async function runSync(
1234
1342
  ): Promise<SingleResult> {
1235
1343
  options = {
1236
1344
  ...options,
1237
- capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
1345
+ capabilityCeiling: intersectSubagentCapabilityCeilings(options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId), decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV])),
1238
1346
  };
1239
1347
  const agent = agents.find((a) => a.name === agentName);
1240
1348
  if (!agent) {
1241
1349
  return withRunContext({
1350
+ index: options.index ?? 0,
1242
1351
  agent: agentName,
1243
1352
  task,
1244
1353
  exitCode: 1,
@@ -1247,9 +1356,23 @@ export async function runSync(
1247
1356
  error: `Unknown agent: ${agentName}`,
1248
1357
  }, options.context);
1249
1358
  }
1359
+ try {
1360
+ assertAgentAllowedByCapabilityCeiling(agent.name, options.capabilityCeiling);
1361
+ } catch (error) {
1362
+ return withRunContext({
1363
+ agent: agent.name,
1364
+ task,
1365
+ exitCode: 1,
1366
+ messages: [],
1367
+ usage: emptyUsage(),
1368
+ error: error instanceof Error ? error.message : String(error),
1369
+ ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
1370
+ }, options.context);
1371
+ }
1250
1372
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1251
1373
  if (outputModeValidationError) {
1252
1374
  return withRunContext({
1375
+ index: options.index ?? 0,
1253
1376
  agent: agentName,
1254
1377
  task,
1255
1378
  exitCode: 1,
@@ -1286,6 +1409,7 @@ export async function runSync(
1286
1409
  );
1287
1410
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1288
1411
  return withRunContext({
1412
+ index: options.index ?? 0,
1289
1413
  agent: agentName,
1290
1414
  task,
1291
1415
  exitCode: 1,
@@ -1345,121 +1469,149 @@ export async function runSync(
1345
1469
  }
1346
1470
 
1347
1471
  const persistResultMetadata = (target: SingleResult): void => {
1348
- if (!artifactPathsResult || options.artifactConfig?.enabled === false || options.artifactConfig?.includeMetadata === false) return;
1349
- writeMetadata(artifactPathsResult.metadataPath, {
1472
+ persistSingleResultMetadata({
1473
+ metadataPath: artifactPathsResult?.metadataPath,
1474
+ enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
1350
1475
  runId: options.runId,
1351
1476
  agent: agentName,
1352
1477
  task,
1353
- exitCode: target.exitCode,
1354
- usage: target.usage,
1355
- model: target.model,
1356
- attemptedModels: target.attemptedModels,
1357
- modelAttempts: target.modelAttempts,
1358
- durationMs: target.progressSummary?.durationMs,
1359
- toolCount: target.progressSummary?.toolCount,
1360
- error: target.error,
1361
- agentContract: target.agentContract,
1362
- launchContractDigest: target.launchContractDigest,
1363
- execution: target.execution,
1364
- acceptance: target.acceptance,
1365
- capabilityCeiling: target.capabilityCeiling,
1366
- capabilityAudit: target.capabilityAudit,
1367
- review: target.review,
1368
- effects: target.effects,
1369
- ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
1370
- transcriptError: target.transcriptError,
1371
- skills: target.skills,
1372
- skillsWarning: target.skillsWarning,
1373
- timestamp: Date.now(),
1478
+ result: target,
1374
1479
  });
1375
1480
  };
1376
1481
 
1377
- const detachedAwareOptions: RunSyncOptions = options.onDetachedExit
1378
- ? {
1379
- ...options,
1380
- onDetachedExit: (recoveredResult) => {
1381
- void (async () => {
1382
- const childWrittenOutput = options.outputPath
1383
- ? extractChildWrittenOutput(recoveredResult.messages, options.outputPath, options.cwd ?? runtimeCwd)
1384
- : undefined;
1385
- recoveredResult.acceptance = await evaluateAcceptance({
1386
- acceptance: effectiveAcceptance,
1387
- output: acceptanceOutputByResult.get(recoveredResult) ?? recoveredResult.finalOutput ?? "",
1388
- fileOutput: childWrittenOutput !== undefined && options.outputPath
1389
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1390
- : undefined,
1391
- cwd: options.cwd ?? runtimeCwd,
1392
- reportOptional: isAgentContractV1(options.agentContract),
1393
- });
1394
- const acceptanceFailure = acceptanceFailureMessage(recoveredResult.acceptance);
1395
- stripAcceptanceReportsFromMessages(recoveredResult.messages);
1396
- if (acceptanceFailure && recoveredResult.acceptance.explicit && recoveredResult.exitCode === 0 && !isAgentContractV1(options.agentContract)) {
1397
- recoveredResult.exitCode = 1;
1398
- recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\n${acceptanceFailure}` : acceptanceFailure;
1399
- if (recoveredResult.progress) {
1400
- recoveredResult.progress.status = "failed";
1401
- recoveredResult.progress.error = recoveredResult.error;
1482
+ let intercomDetached = false;
1483
+ let detachedReason: string | undefined;
1484
+ const attemptOptions: RunSyncOptions = {
1485
+ ...options,
1486
+ onDetachReceipt: (receipt) => {
1487
+ receipt.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
1488
+ try {
1489
+ persistResultMetadata(receipt);
1490
+ } catch (error) {
1491
+ receipt.metadataSaveError = error instanceof Error ? error.message : String(error);
1492
+ }
1493
+ const accepted = options.onDetachReceipt?.(receipt) === true;
1494
+ if (accepted) {
1495
+ detachedReason = receipt.detachedReason;
1496
+ if (receipt.detachedReason === "intercom coordination") intercomDetached = true;
1497
+ }
1498
+ return accepted;
1499
+ },
1500
+ };
1501
+ let lastResult: SingleResult | undefined;
1502
+ const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1503
+ modelAttemptsLoop: for (let modelIndex = 0; modelIndex < modelsToTry.length; modelIndex++) {
1504
+ const candidate = modelsToTry[modelIndex];
1505
+ for (let startupAttemptIndex = 0; ; startupAttemptIndex++) {
1506
+ const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1507
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, attemptOptions, {
1508
+ sessionEnabled,
1509
+ systemPrompt,
1510
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1511
+ skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1512
+ jsonlPath,
1513
+ artifactPaths: artifactPathsResult,
1514
+ transcriptWriter,
1515
+ attemptNotes,
1516
+ modelCandidates: candidates
1517
+ .map((modelCandidate) => applyThinkingSuffix(modelCandidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined))
1518
+ .filter((modelCandidate): modelCandidate is string => Boolean(modelCandidate)),
1519
+ outputSnapshot,
1520
+ originalTask: task,
1521
+ });
1522
+ lastResult = result;
1523
+ if (startupAttemptIndex === 0) {
1524
+ if (result.model) attemptedModels.push(result.model);
1525
+ else if (candidate) attemptedModels.push(candidate);
1526
+ }
1527
+ sumUsage(aggregateUsage, result.usage);
1528
+ totalToolCount += result.progressSummary?.toolCount ?? 0;
1529
+ totalDurationMs += result.progressSummary?.durationMs ?? 0;
1530
+ const attemptSucceeded = result.exitCode === 0 && !result.error;
1531
+ const attempt: ModelAttempt = {
1532
+ model: result.model ?? candidate ?? agent.model ?? "default",
1533
+ success: attemptSucceeded,
1534
+ exitCode: result.exitCode,
1535
+ error: result.error,
1536
+ usage: { ...result.usage },
1537
+ };
1538
+ modelAttempts.push(attempt);
1539
+ // Preserve the legacy intercom handoff contract: once this logical run has
1540
+ // been handed to a supervisor, terminating that attempt must not launch a
1541
+ // startup retry or model fallback. Explicit user detach retains fallback.
1542
+ if (intercomDetached || result.timedOut || result.turnBudgetExceeded) break modelAttemptsLoop;
1543
+ if (attemptSucceeded) break modelAttemptsLoop;
1544
+
1545
+ const startupFailure = isRetryableSubagentStartupFailure({
1546
+ exitCode: result.exitCode,
1547
+ error: result.error,
1548
+ finalOutput: result.finalOutput,
1549
+ messageCount: result.messages?.length ?? 0,
1550
+ toolCount: result.progressSummary?.toolCount ?? 0,
1551
+ usage: result.usage,
1552
+ durationMs: result.progressSummary?.durationMs ?? Number.POSITIVE_INFINITY,
1553
+ protocolError: result.protocolError,
1554
+ processSignal: result.processSignal,
1555
+ detached: result.detached,
1556
+ interrupted: result.interrupted,
1557
+ timedOut: result.timedOut,
1558
+ stopped: result.stopped,
1559
+ turnBudgetExceeded: result.turnBudgetExceeded,
1560
+ });
1561
+ const retryDelayMs = SUBAGENT_STARTUP_RETRY_DELAYS_MS[startupAttemptIndex];
1562
+ if (startupFailure && retryDelayMs !== undefined) {
1563
+ const retryNote = formatSubagentStartupRetryNote({
1564
+ model: attempt.model,
1565
+ attempt: startupAttemptIndex + 1,
1566
+ maxAttempts: SUBAGENT_STARTUP_RETRY_DELAYS_MS.length + 1,
1567
+ delayMs: retryDelayMs,
1568
+ });
1569
+ const shouldRetry = await waitForSubagentStartupRetry(retryDelayMs, [options.signal, options.interruptSignal]);
1570
+ if (!shouldRetry) {
1571
+ if (options.interruptSignal?.aborted) {
1572
+ result.exitCode = 0;
1573
+ result.interrupted = true;
1574
+ result.error = undefined;
1575
+ result.finalOutput = "Interrupted. Waiting for explicit next action.";
1576
+ if (result.progress) {
1577
+ result.progress.status = "running";
1578
+ result.progress.error = undefined;
1402
1579
  }
1580
+ } else {
1581
+ const cancellationError = "Subagent startup retry cancelled before relaunch.";
1582
+ result.error = cancellationError;
1583
+ result.finalOutput = cancellationError;
1584
+ attempt.error = cancellationError;
1585
+ if (result.progress) result.progress.error = cancellationError;
1403
1586
  }
1404
- if (isAgentContractV1(options.agentContract)) attachContractProjections(recoveredResult);
1405
- persistResultMetadata(recoveredResult);
1406
- options.onDetachedExit?.(recoveredResult);
1407
- })().catch((error) => {
1408
- const message = error instanceof Error ? error.message : String(error);
1409
- recoveredResult.exitCode = 1;
1410
- recoveredResult.error = recoveredResult.error ? `${recoveredResult.error}\nAcceptance evaluation failed: ${message}` : `Acceptance evaluation failed: ${message}`;
1411
- options.onDetachedExit?.(recoveredResult);
1587
+ break modelAttemptsLoop;
1588
+ }
1589
+ attempt.error = retryNote;
1590
+ attemptNotes.push(retryNote);
1591
+ continue;
1592
+ }
1593
+ if (startupFailure) {
1594
+ const startupError = formatSubagentStartupRetryExhaustedError({
1595
+ model: attempt.model,
1596
+ attempts: startupAttemptIndex + 1,
1412
1597
  });
1413
- },
1414
- }
1415
- : options;
1416
-
1417
- let lastResult: SingleResult | undefined;
1418
- const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1419
- for (let i = 0; i < modelsToTry.length; i++) {
1420
- const candidate = modelsToTry[i];
1421
- const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1422
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
1423
- sessionEnabled,
1424
- systemPrompt,
1425
- resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1426
- skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
1427
- jsonlPath,
1428
- artifactPaths: artifactPathsResult,
1429
- transcriptWriter,
1430
- attemptNotes,
1431
- modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1432
- outputSnapshot,
1433
- originalTask: task,
1434
- });
1435
- lastResult = result;
1436
- if (result.model) attemptedModels.push(result.model);
1437
- else if (candidate) attemptedModels.push(candidate);
1438
- sumUsage(aggregateUsage, result.usage);
1439
- totalToolCount += result.progressSummary?.toolCount ?? 0;
1440
- totalDurationMs += result.progressSummary?.durationMs ?? 0;
1441
- const attemptSucceeded = result.exitCode === 0 && !result.error;
1442
- const attempt: ModelAttempt = {
1443
- model: result.model ?? candidate ?? agent.model ?? "default",
1444
- success: attemptSucceeded,
1445
- exitCode: result.exitCode,
1446
- error: result.error,
1447
- usage: { ...result.usage },
1448
- };
1449
- modelAttempts.push(attempt);
1450
- if (result.detached || result.timedOut || result.turnBudgetExceeded) {
1451
- break;
1452
- }
1453
- if (attemptSucceeded) {
1454
- break;
1455
- }
1456
- if (!isRetryableModelFailure(result.error) || i === modelsToTry.length - 1) {
1598
+ result.error = startupError;
1599
+ result.finalOutput = startupError;
1600
+ if (result.progress) {
1601
+ result.progress.error = startupError;
1602
+ result.progress.status = "failed";
1603
+ }
1604
+ attempt.error = startupError;
1605
+ break modelAttemptsLoop;
1606
+ }
1607
+ if (!isRetryableModelFailure(result.error) || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
1608
+ attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
1457
1609
  break;
1458
1610
  }
1459
- attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1460
1611
  }
1461
1612
 
1462
1613
  const result = withRunContext(lastResult ?? {
1614
+ index: options.index ?? 0,
1463
1615
  agent: agentName,
1464
1616
  task,
1465
1617
  exitCode: 1,
@@ -1476,35 +1628,33 @@ export async function runSync(
1476
1628
  tokens: aggregateUsage.input + aggregateUsage.output,
1477
1629
  durationMs: totalDurationMs,
1478
1630
  };
1479
- if (attemptNotes.length > 0 && result.progress) {
1480
- result.progress.recentOutput = [...attemptNotes, ...result.progress.recentOutput];
1481
- if (result.progress.recentOutput.length > 50) {
1482
- result.progress.recentOutput.splice(50);
1483
- }
1484
- }
1485
-
1486
1631
  if (transcriptWriter) result.transcriptPath = artifactPathsResult?.transcriptPath;
1487
1632
  if (transcriptWriter?.getError()) result.transcriptError = transcriptWriter.getError();
1488
1633
 
1489
- if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1490
- result.artifactPaths = artifactPathsResult;
1491
- if (options.artifactConfig?.includeOutput !== false) {
1492
- writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1493
- output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1494
- error: result.error,
1495
- transcriptPath: result.transcriptPath,
1496
- metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1497
- }));
1498
- }
1499
- if (options.maxOutput) {
1634
+ try {
1635
+ if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1636
+ result.artifactPaths = artifactPathsResult;
1637
+ if (options.artifactConfig?.includeOutput !== false) {
1638
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1639
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1640
+ error: result.error,
1641
+ transcriptPath: result.transcriptPath,
1642
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1643
+ }));
1644
+ }
1645
+ if (options.maxOutput) {
1646
+ const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1647
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
1648
+ if (truncationResult.truncated) result.truncation = truncationResult;
1649
+ }
1650
+ } else if (options.maxOutput) {
1500
1651
  const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1501
- const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
1652
+ const truncationResult = truncateOutput(result.finalOutput ?? "", config);
1502
1653
  if (truncationResult.truncated) result.truncation = truncationResult;
1503
1654
  }
1504
- } else if (options.maxOutput) {
1505
- const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1506
- const truncationResult = truncateOutput(result.finalOutput ?? "", config);
1507
- if (truncationResult.truncated) result.truncation = truncationResult;
1655
+ } catch (error) {
1656
+ const message = `Artifact output post-processing failed: ${error instanceof Error ? error.message : String(error)}`;
1657
+ result.outputSaveError = result.outputSaveError ? `${result.outputSaveError}\n${message}` : message;
1508
1658
  }
1509
1659
 
1510
1660
  if (options.sessionFile && (existsSync(options.sessionFile) || result.messages?.length)) {
@@ -1517,28 +1667,36 @@ export async function runSync(
1517
1667
  const childWrittenOutput = options.outputPath
1518
1668
  ? extractChildWrittenOutput(result.messages, options.outputPath, options.cwd ?? runtimeCwd)
1519
1669
  : undefined;
1520
- if (result.detached) {
1521
- result.acceptance = buildPendingAcceptanceLedger(effectiveAcceptance);
1522
- } else if (result.stopped) {
1523
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
1524
- } else if (result.timedOut) {
1525
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
1526
- } else if (result.turnBudgetExceeded) {
1527
- result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
1528
- } else {
1529
- result.acceptance = await evaluateAcceptance({
1530
- acceptance: effectiveAcceptance,
1531
- output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
1532
- fileOutput: childWrittenOutput !== undefined && options.outputPath
1533
- ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1534
- : undefined,
1535
- cwd: options.cwd ?? runtimeCwd,
1536
- reportOptional: isAgentContractV1(options.agentContract),
1537
- });
1670
+ try {
1671
+ if (result.interrupted && detachedReason === "user request") {
1672
+ // Only an accepted user-detach receipt needs a non-rejecting terminal
1673
+ // ledger. Attached and background execution retain their baseline
1674
+ // acceptance behavior.
1675
+ result.acceptance = buildInterruptedAcceptanceLedger(effectiveAcceptance);
1676
+ } else if (result.stopped) {
1677
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." });
1678
+ } else if (result.timedOut) {
1679
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." });
1680
+ } else if (result.turnBudgetExceeded) {
1681
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." });
1682
+ } else {
1683
+ result.acceptance = await evaluateAcceptance({
1684
+ acceptance: effectiveAcceptance,
1685
+ output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
1686
+ fileOutput: childWrittenOutput !== undefined && options.outputPath
1687
+ ? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
1688
+ : undefined,
1689
+ cwd: options.cwd ?? runtimeCwd,
1690
+ reportOptional: isAgentContractV1(options.agentContract),
1691
+ });
1692
+ }
1693
+ } catch (error) {
1694
+ const message = `Acceptance evaluation failed: ${error instanceof Error ? error.message : String(error)}`;
1695
+ result.acceptance = buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "acceptance-evaluation", message });
1538
1696
  }
1539
1697
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1540
1698
  stripAcceptanceReportsFromMessages(result.messages);
1541
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1699
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1542
1700
  result.exitCode = 1;
1543
1701
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1544
1702
  if (result.progress) {
@@ -1547,7 +1705,143 @@ export async function runSync(
1547
1705
  }
1548
1706
  }
1549
1707
  if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1550
- persistResultMetadata(result);
1708
+ try {
1709
+ persistResultMetadata(result);
1710
+ } catch (error) {
1711
+ result.metadataSaveError = error instanceof Error ? error.message : String(error);
1712
+ }
1551
1713
 
1552
1714
  return result;
1553
1715
  }
1716
+
1717
+ /**
1718
+ * Runs the authoritative completion pipeline independently from the foreground
1719
+ * receipt. Detachment is same-runtime only: it does not adopt or daemonize work.
1720
+ */
1721
+ export async function runSync(
1722
+ runtimeCwd: string,
1723
+ agents: AgentConfig[],
1724
+ agentName: string,
1725
+ task: string,
1726
+ options: RunSyncOptions,
1727
+ ): Promise<SingleResult> {
1728
+ // Capture the strict contract before consumer-owned objects can be mutated
1729
+ // after a detached receipt is published.
1730
+ const strictContract = isAgentContractV1(options.agentContract);
1731
+ let detachedReason: string | undefined;
1732
+ let publishedReceipt: SingleResult | undefined;
1733
+ let activeDetachAttempt: ((reason?: string) => boolean) | undefined;
1734
+ let detachReadyPublished = false;
1735
+ let resolveReceipt!: (result: SingleResult) => void;
1736
+ const receipt = new Promise<SingleResult>((resolve) => { resolveReceipt = resolve; });
1737
+ const originSignal = options.signal;
1738
+ const originController = new AbortController();
1739
+ const forwardOriginAbort = () => {
1740
+ if (!detachedReason) originController.abort(originSignal?.reason);
1741
+ };
1742
+ if (originSignal?.aborted) forwardOriginAbort();
1743
+ else originSignal?.addEventListener("abort", forwardOriginAbort, { once: true });
1744
+
1745
+ const completion = runSyncCompletion(runtimeCwd, agents, agentName, task, {
1746
+ ...options,
1747
+ signal: originController.signal,
1748
+ onDetachedExit: undefined,
1749
+ onDetachReceipt: (detachedReceipt) => {
1750
+ if (detachedReason || publishedReceipt) return false;
1751
+ // Keep the authoritative result, fallback snapshot, and caller receipt
1752
+ // separately owned while the completion pipeline remains live.
1753
+ publishedReceipt = structuredClone(detachedReceipt);
1754
+ const callerReceipt = structuredClone(detachedReceipt);
1755
+ // A strict contract was already validated before detach; normalize private
1756
+ // and caller snapshots to the only safely known version.
1757
+ publishedReceipt.agentContract = strictContract ? { version: 1 } : undefined;
1758
+ callerReceipt.agentContract = strictContract ? { version: 1 } : undefined;
1759
+ detachedReason = detachedReceipt.detachedReason ?? "user request";
1760
+ resolveReceipt(callerReceipt);
1761
+ return true;
1762
+ },
1763
+ onDetachReady: (detachAttempt) => {
1764
+ activeDetachAttempt = detachAttempt;
1765
+ if (detachReadyPublished) return;
1766
+ detachReadyPublished = true;
1767
+ options.onDetachReady?.((reason = "user request") => {
1768
+ if (detachedReason || originController.signal.aborted) return false;
1769
+ return activeDetachAttempt?.(reason) ?? false;
1770
+ });
1771
+ },
1772
+ });
1773
+
1774
+ const authoritativeCompletion = completion.catch((error: unknown) => {
1775
+ if (!publishedReceipt || !detachedReason) throw error;
1776
+ const message = error instanceof Error ? error.message : String(error);
1777
+ const failureMessage = `Detached completion pipeline failed after receipt: ${message}`;
1778
+ const failedProgress: AgentProgress = publishedReceipt.progress
1779
+ ? { ...publishedReceipt.progress, status: "failed", error: failureMessage }
1780
+ : {
1781
+ index: options.index ?? 0,
1782
+ agent: publishedReceipt.agent,
1783
+ status: "failed",
1784
+ task: publishedReceipt.task,
1785
+ recentTools: [],
1786
+ recentOutput: [],
1787
+ toolCount: publishedReceipt.progressSummary?.toolCount ?? 0,
1788
+ tokens: publishedReceipt.progressSummary?.tokens ?? 0,
1789
+ durationMs: publishedReceipt.progressSummary?.durationMs ?? 0,
1790
+ error: failureMessage,
1791
+ };
1792
+ const failedResult: SingleResult = {
1793
+ ...publishedReceipt,
1794
+ detached: undefined,
1795
+ detachedReason,
1796
+ exitCode: 1,
1797
+ error: failureMessage,
1798
+ finalOutput: failureMessage,
1799
+ progress: failedProgress,
1800
+ progressSummary: {
1801
+ toolCount: failedProgress.toolCount,
1802
+ tokens: failedProgress.tokens,
1803
+ durationMs: failedProgress.durationMs,
1804
+ },
1805
+ acceptance: publishedReceipt.acceptance
1806
+ ? buildSkippedAcceptanceLedger(publishedReceipt.acceptance.effectiveAcceptance, { id: "completion-pipeline", message: failureMessage })
1807
+ : undefined,
1808
+ };
1809
+ if (strictContract) attachContractProjections(failedResult);
1810
+ try {
1811
+ // Replace the provisional detach receipt metadata with the authoritative
1812
+ // terminal failure. Persistence remains best-effort and cannot orphan work.
1813
+ persistSingleResultMetadata({
1814
+ metadataPath: failedResult.artifactPaths?.metadataPath,
1815
+ enabled: options.artifactConfig?.enabled !== false && options.artifactConfig?.includeMetadata !== false,
1816
+ runId: options.runId,
1817
+ agent: failedResult.agent,
1818
+ task: failedResult.task,
1819
+ result: failedResult,
1820
+ });
1821
+ } catch (metadataError) {
1822
+ failedResult.metadataSaveError = metadataError instanceof Error ? metadataError.message : String(metadataError);
1823
+ }
1824
+ return failedResult;
1825
+ });
1826
+
1827
+ let terminalCallbackInvoked = false;
1828
+ void authoritativeCompletion.then((terminalResult) => {
1829
+ if (!detachedReason || terminalCallbackInvoked) return;
1830
+ terminalCallbackInvoked = true;
1831
+ terminalResult.detached = undefined;
1832
+ terminalResult.detachedReason = detachedReason;
1833
+ try {
1834
+ options.onDetachedExit?.(terminalResult);
1835
+ } catch {
1836
+ // The authoritative result has settled. Consumer callback failures are
1837
+ // contained here; each consumer owns cleanup through its own finally block.
1838
+ }
1839
+ }).catch(() => {
1840
+ // Attached completion rejection remains observable through the returned
1841
+ // promise without becoming an unhandled side-channel rejection.
1842
+ }).finally(() => {
1843
+ originSignal?.removeEventListener("abort", forwardOriginAbort);
1844
+ });
1845
+
1846
+ return Promise.race([authoritativeCompletion, receipt]);
1847
+ }