@selesai/code 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -4,10 +4,12 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync, unlinkSync } from "node:fs";
7
+ import * as path from "node:path";
7
8
  import type { Message } from "@earendil-works/pi-ai";
8
9
  import type { AgentConfig } from "../../agents/agents.ts";
9
10
  import {
10
11
  ensureArtifactsDir,
12
+ formatOutputArtifactContent,
11
13
  getArtifactPaths,
12
14
  writeArtifact,
13
15
  writeMetadata,
@@ -40,18 +42,22 @@ import {
40
42
  getFinalOutput,
41
43
  findLatestSessionFile,
42
44
  detectSubagentError,
45
+ hasEmptyTerminalAssistantResponse,
43
46
  extractToolArgsPreview,
44
47
  extractTextFromContent,
45
48
  } from "../../shared/utils.ts";
46
49
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
47
50
  import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
48
51
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
49
- import { getSelesaiSpawnCommand } from "../shared/pi-spawn.ts";
52
+ import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
50
53
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
51
54
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
52
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
53
- import { readStructuredOutput } from "../shared/structured-output.ts";
54
- import { captureSingleOutputSnapshot, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.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";
57
+ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
58
+ import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput } from "../shared/structured-output.ts";
59
+ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
60
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
55
61
  import {
56
62
  buildModelCandidates,
57
63
  formatModelAttemptNote,
@@ -68,9 +74,20 @@ import {
68
74
  shouldEscalateMutatingFailures,
69
75
  summarizeRecentMutatingFailures,
70
76
  } from "../shared/long-running-guard.ts";
71
- import { acceptanceFailureMessage, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
72
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
77
+ import { acceptanceFailureMessage, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
78
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
79
+ import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
73
80
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
81
+ import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
82
+ import { agentDefinitionDigest, launchBindingDigest } from "../../shared/launch-contract.ts";
83
+ import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
84
+ import {
85
+ acceptChildWatchdogEvent,
86
+ childWatchdogIsActive,
87
+ isChildWatchdogStatusEvent,
88
+ resolveChildWatchdogConfig,
89
+ type ChildWatchdogStateSnapshot,
90
+ } from "../../watchdog/child-status.ts";
74
91
 
75
92
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
76
93
  const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
@@ -79,6 +96,12 @@ function emptyUsage(): Usage {
79
96
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
80
97
  }
81
98
 
99
+ function withRunContext<T extends SingleResult>(result: T, context: RunSyncOptions["context"]): T {
100
+ if (!context) return result;
101
+ result.context = context;
102
+ return result;
103
+ }
104
+
82
105
  function sumUsage(target: Usage, source: Usage): void {
83
106
  target.input += source.input;
84
107
  target.output += source.output;
@@ -102,16 +125,15 @@ function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; re
102
125
  };
103
126
  }
104
127
 
105
- function buildSkippedAcceptanceLedger(acceptance: ResolvedAcceptanceConfig, input: { id: string; message: string }): AcceptanceLedger {
128
+ function buildPendingAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
106
129
  return {
107
- status: acceptance.level === "none" ? "not-required" : "rejected",
130
+ status: "pending",
131
+ evidenceStatus: "pending",
108
132
  explicit: acceptance.explicit,
109
133
  effectiveAcceptance: acceptance,
110
134
  inferredReason: acceptance.inferredReason,
111
135
  criteria: acceptance.criteria,
112
- runtimeChecks: acceptance.level === "none"
113
- ? []
114
- : [{ id: input.id, status: "failed", message: input.message }],
136
+ runtimeChecks: [],
115
137
  verifyRuns: [],
116
138
  };
117
139
  }
@@ -176,6 +198,7 @@ async function runSingleAttempt(
176
198
  sessionEnabled: boolean;
177
199
  systemPrompt: string;
178
200
  resolvedSkillNames?: string[];
201
+ modelCandidates?: string[];
179
202
  skillsWarning?: string;
180
203
  jsonlPath?: string;
181
204
  artifactPaths?: ArtifactPaths;
@@ -187,7 +210,17 @@ async function runSingleAttempt(
187
210
  ): Promise<SingleResult> {
188
211
  const effectiveThinking = options.thinkingOverride ?? agent.thinking;
189
212
  const modelArg = applyThinkingSuffix(model, effectiveThinking, options.thinkingOverride !== undefined);
190
- const { args, env: sharedEnv, tempDir } = buildPiArgs({
213
+ const resolvedThinking = resolveEffectiveThinking(modelArg, effectiveThinking);
214
+ const watchdogConfig = resolveWatchdogConfig(options.cwd ?? runtimeCwd);
215
+ const childWatchdog = watchdogConfig.ok
216
+ ? resolveChildWatchdogConfig({
217
+ config: watchdogConfig.config,
218
+ agent: agent.name,
219
+ runId: options.runId,
220
+ childIndex: options.index ?? 0,
221
+ })
222
+ : undefined;
223
+ const { args, env: sharedEnv, tempDir, toolDiagnosticPath, capabilityAudit } = buildPiArgs({
191
224
  baseArgs: ["--offline", "--mode", "json", "-p"],
192
225
  task,
193
226
  sessionEnabled: shared.sessionEnabled,
@@ -218,22 +251,61 @@ async function runSingleAttempt(
218
251
  parentSessionId: options.parentSessionId,
219
252
  structuredOutput: options.structuredOutput,
220
253
  toolBudget: options.toolBudget,
254
+ allowZeroToolBudget: options.allowZeroToolBudget,
255
+ childWatchdog,
256
+ waitToolEnabled: options.waitToolEnabled,
257
+ capabilityCeiling: options.capabilityCeiling,
221
258
  });
222
259
 
223
- const result: SingleResult = {
260
+ const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
261
+ const toolPlan = resolvePiLaunchToolPlan({
262
+ tools: agent.tools,
263
+ extensions: agent.extensions,
264
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
265
+ mcpDirectTools: agent.mcpDirectTools,
266
+ cwd: options.cwd ?? runtimeCwd,
267
+ requireReadTool: Boolean(shared.resolvedSkillNames?.length),
268
+ structuredOutput: Boolean(options.structuredOutput),
269
+ capabilityCeiling: options.capabilityCeiling,
270
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
271
+ });
272
+ const launchContractDigest = launchBindingDigest({
273
+ definitionDigest: agentDefinitionDigest(agent),
274
+ task: shared.originalTask ?? task,
275
+ ...(modelArg ? { model: modelArg } : {}),
276
+ modelCandidates: shared.modelCandidates,
277
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
278
+ systemPrompt: effectiveSystemPrompt,
279
+ systemPromptMode: agent.systemPromptMode,
280
+ inheritProjectContext: agent.inheritProjectContext,
281
+ inheritSkills: agent.inheritSkills,
282
+ skills: shared.resolvedSkillNames ?? [],
283
+ tools: toolPlan.effectiveToolAllowlist,
284
+ extensions: toolPlan.extensionArgs,
285
+ mcpDirectTools: toolPlan.effectiveMcpTools,
286
+ ...(options.outputPath ? { outputPath: options.outputPath } : {}),
287
+ outputMode: options.outputMode ?? "inline",
288
+ ...(options.structuredOutput ? { structuredOutputSchema: options.structuredOutput.schema } : {}),
289
+ });
290
+ const result: SingleResult = withRunContext({
224
291
  agent: agent.name,
225
292
  task: shared.originalTask ?? task,
293
+ ...(options.agentContract ? { agentContract: options.agentContract } : {}),
294
+ launchContractDigest,
226
295
  exitCode: 0,
227
296
  messages: [],
228
297
  usage: emptyUsage(),
229
298
  model: modelArg,
299
+ ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
230
300
  artifactPaths: shared.artifactPaths,
231
301
  transcriptPath: shared.transcriptWriter ? shared.artifactPaths?.transcriptPath : undefined,
232
302
  skills: shared.resolvedSkillNames,
233
303
  skillsWarning: shared.skillsWarning,
234
304
  ...(options.turnBudget ? { turnBudget: initialTurnBudgetState(options.turnBudget) } : {}),
235
305
  ...(options.toolBudget ? { toolBudget: initialToolBudgetState(options.toolBudget) } : {}),
236
- };
306
+ ...(options.capabilityCeiling ? { capabilityCeiling: options.capabilityCeiling } : {}),
307
+ ...(capabilityAudit ? { capabilityAudit } : {}),
308
+ }, options.context);
237
309
  const startTime = Date.now();
238
310
  if (options.structuredOutput) {
239
311
  try {
@@ -271,6 +343,7 @@ async function runSingleAttempt(
271
343
  result.progress = progress;
272
344
  const attemptTimeout = resolveAttemptTimeout(options);
273
345
  if (attemptTimeout?.remainingMs === 0) {
346
+ cleanupTempDir(tempDir);
274
347
  result.exitCode = 1;
275
348
  result.timedOut = true;
276
349
  result.error = attemptTimeout.message;
@@ -286,9 +359,10 @@ async function runSingleAttempt(
286
359
  }
287
360
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
288
361
  let observedMutationAttempt = false;
362
+ let structuredOutputToolInvoked = false;
289
363
 
290
364
  const exitCode = await new Promise<number>((resolve) => {
291
- const spawnSpec = getSelesaiSpawnCommand(args);
365
+ const spawnSpec = getPiSpawnCommand(args);
292
366
  const proc = spawn(spawnSpec.command, spawnSpec.args, {
293
367
  cwd: options.cwd ?? runtimeCwd,
294
368
  env: spawnEnv,
@@ -296,7 +370,6 @@ async function runSingleAttempt(
296
370
  windowsHide: true,
297
371
  });
298
372
  const jsonlWriter = createJsonlWriter(shared.jsonlPath, proc.stdout);
299
- let buf = "";
300
373
  let processClosed = false;
301
374
  let settled = false;
302
375
  let detached = false;
@@ -311,6 +384,7 @@ async function runSingleAttempt(
311
384
  let turnBudgetSoftReached = false;
312
385
  let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
313
386
  let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
387
+ let protocolHardKillTimer: NodeJS.Timeout | undefined;
314
388
  const clearTurnBudgetTimers = () => {
315
389
  if (turnBudgetTerminationTimer) {
316
390
  clearTimeout(turnBudgetTerminationTimer);
@@ -358,8 +432,22 @@ async function runSingleAttempt(
358
432
  let childExited = false;
359
433
  let forcedTerminationSignal = false;
360
434
  let cleanTerminalAssistantStopReceived = false;
435
+ let agentSettledReceived = false;
361
436
  let finalDrainTimer: NodeJS.Timeout | undefined;
362
437
  let finalHardKillTimer: NodeJS.Timeout | undefined;
438
+ let watchdogTailTimer: NodeJS.Timeout | undefined;
439
+ let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
440
+ const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
441
+ childWatchdogState = snapshot;
442
+ result.watchdog = snapshot;
443
+ progress.watchdog = snapshot;
444
+ };
445
+ const clearWatchdogTailTimer = () => {
446
+ if (watchdogTailTimer) {
447
+ clearTimeout(watchdogTailTimer);
448
+ watchdogTailTimer = undefined;
449
+ }
450
+ };
363
451
  const clearFinalDrainTimers = () => {
364
452
  if (finalDrainTimer) {
365
453
  clearTimeout(finalDrainTimer);
@@ -371,14 +459,18 @@ async function runSingleAttempt(
371
459
  }
372
460
  };
373
461
  const startFinalDrain = () => {
462
+ if (childWatchdogIsActive(childWatchdogState)) {
463
+ armWatchdogTail();
464
+ return;
465
+ }
374
466
  if (childExited || finalDrainTimer || settled || processClosed || detached) return;
375
467
  finalDrainTimer = setTimeout(() => {
376
468
  if (settled || processClosed || detached) return;
377
469
  const termSent = trySignalChild(proc, "SIGTERM");
378
470
  if (!termSent) return;
379
471
  forcedTerminationSignal = true;
380
- if (!cleanTerminalAssistantStopReceived && !assistantError) {
381
- result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
472
+ if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !assistantError) {
473
+ result.error = result.error ?? `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
382
474
  }
383
475
  finalHardKillTimer = setTimeout(() => {
384
476
  if (settled || processClosed || detached) return;
@@ -388,13 +480,45 @@ async function runSingleAttempt(
388
480
  }, FINAL_STOP_GRACE_MS);
389
481
  finalDrainTimer.unref?.();
390
482
  };
483
+ function armWatchdogTail(): void {
484
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled || processClosed || detached) return;
485
+ watchdogTailTimer = setTimeout(() => {
486
+ watchdogTailTimer = undefined;
487
+ updateChildWatchdogState({
488
+ phase: "stale",
489
+ seq: (childWatchdogState?.seq ?? 0) + 1,
490
+ lastUpdate: Date.now(),
491
+ followUpPending: false,
492
+ reason: "child watchdog tail timeout",
493
+ timedOut: true,
494
+ });
495
+ startFinalDrain();
496
+ fireUpdate();
497
+ }, childWatchdog?.watchdogTailTimeoutMs ?? 120_000);
498
+ watchdogTailTimer.unref?.();
499
+ }
500
+ const applyChildLifecycle = (action: ChildLifecycleAction): void => {
501
+ if (action === "cancel-drain") {
502
+ clearFinalDrainTimers();
503
+ clearWatchdogTailTimer();
504
+ return;
505
+ }
506
+ if (action === "start-drain") startFinalDrain();
507
+ };
391
508
 
392
509
  const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
393
- if (!options.allowIntercomDetach || detached || processClosed || !intercomStarted) return;
510
+ if (!options.allowIntercomDetach || detached || processClosed) return;
394
511
  if (!payload || typeof payload !== "object") return;
395
- const requestId = (payload as { requestId?: unknown }).requestId;
512
+ const event = payload as { requestId?: unknown; runId?: unknown; agent?: unknown; childIndex?: unknown };
513
+ const requestId = event.requestId;
396
514
  if (typeof requestId !== "string" || requestId.length === 0) return;
397
- options.intercomEvents?.emit(INTERCOM_DETACH_RESPONSE_EVENT, { requestId, accepted: true });
515
+ const hasRoute = event.runId !== undefined || event.agent !== undefined || event.childIndex !== undefined;
516
+ if (hasRoute) {
517
+ if (typeof event.runId === "string" && event.runId !== options.runId) return;
518
+ if (typeof event.agent === "string" && event.agent !== agent.name) return;
519
+ if (typeof event.childIndex === "number" && event.childIndex !== (options.index ?? 0)) return;
520
+ } 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 });
398
522
  detachForIntercom();
399
523
  });
400
524
 
@@ -402,9 +526,14 @@ async function runSingleAttempt(
402
526
  if (settled) return;
403
527
  settled = true;
404
528
  clearFinalDrainTimers();
529
+ clearWatchdogTailTimer();
405
530
  clearStdioGuard();
406
531
  clearTimeoutTimers();
407
532
  clearTurnBudgetTimers();
533
+ if (protocolHardKillTimer) {
534
+ clearTimeout(protocolHardKillTimer);
535
+ protocolHardKillTimer = undefined;
536
+ }
408
537
  if (activityTimer) {
409
538
  clearInterval(activityTimer);
410
539
  activityTimer = undefined;
@@ -504,7 +633,7 @@ async function runSingleAttempt(
504
633
  turnBudgetHardKillTimer.unref?.();
505
634
  };
506
635
 
507
- const updateTurnBudget = (turnCount: number, terminalAssistantStop: boolean) => {
636
+ const updateTurnBudget = (turnCount: number, terminalAssistantStop: boolean, toolWorkActiveOrStarting: boolean) => {
508
637
  const budget = options.turnBudget;
509
638
  if (!budget || result.timedOut || result.turnBudgetExceeded) return;
510
639
  if (turnCount < budget.maxTurns) {
@@ -516,10 +645,17 @@ async function runSingleAttempt(
516
645
  result.wrapUpRequested = true;
517
646
  appendRecentOutput(progress, [turnBudgetSoftNote(budget, turnCount)]);
518
647
  }
519
- result.turnBudget = turnBudgetState(budget, turnCount, false);
520
- if (shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) {
521
- requestTurnBudgetAbort(turnCount);
648
+ const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting, options.enforceHardTurnLimit);
649
+ if (decision === "defer") {
650
+ result.turnBudget = turnBudgetDeferredState(
651
+ budget,
652
+ turnCount,
653
+ result.turnBudget?.terminationDeferredAtTurn,
654
+ );
655
+ return;
522
656
  }
657
+ result.turnBudget = turnBudgetState(budget, turnCount, false);
658
+ if (decision === "abort") requestTurnBudgetAbort(turnCount);
523
659
  };
524
660
 
525
661
  const updateActivityState = (now: number): boolean => {
@@ -528,6 +664,7 @@ async function runSingleAttempt(
528
664
  config: controlConfig,
529
665
  startedAt: startTime,
530
666
  lastActivityAt: progress.lastActivityAt,
667
+ currentTool: progress.currentTool,
531
668
  now,
532
669
  });
533
670
  if (idleState === "needs_attention") {
@@ -562,22 +699,46 @@ async function runSingleAttempt(
562
699
  const fireUpdate = () => {
563
700
  if (!options.onUpdate || processClosed) return;
564
701
  progress.durationMs = Date.now() - startTime;
565
- const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages);
702
+ const output = (result.timedOut || result.turnBudgetExceeded) && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
566
703
  emitUpdateSnapshot(output || "(running...)");
567
704
  };
568
705
 
569
706
  const processLine = (line: string) => {
570
707
  if (!line.trim()) return;
571
708
  jsonlWriter.writeLine(line);
572
- let evt: { type?: string; message?: Message; toolName?: string; args?: unknown };
709
+ let evt: { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
573
710
  try {
574
- evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown };
711
+ evt = JSON.parse(line) as { type?: string; message?: Message; toolName?: string; args?: unknown; willRetry?: unknown };
575
712
  } catch {
576
713
  shared.transcriptWriter?.writeStdoutLine(line);
577
714
  // Non-JSON stdout lines are expected; only structured events are parsed.
578
715
  return;
579
716
  }
580
717
  shared.transcriptWriter?.writeChildEvent(evt);
718
+ if (evt.type === "agent_settled") agentSettledReceived = true;
719
+ applyChildLifecycle(projectChildLifecycle(evt));
720
+
721
+ if (isChildWatchdogStatusEvent(evt)) {
722
+ if (!childWatchdog) return;
723
+ const next = acceptChildWatchdogEvent({
724
+ current: childWatchdogState,
725
+ event: evt,
726
+ runId: options.runId,
727
+ agent: agent.name,
728
+ childIndex: options.index ?? 0,
729
+ });
730
+ if (!next) return;
731
+ updateChildWatchdogState(next);
732
+ if (childWatchdogIsActive(next)) {
733
+ clearFinalDrainTimers();
734
+ armWatchdogTail();
735
+ } else {
736
+ clearWatchdogTailTimer();
737
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
738
+ }
739
+ fireUpdate();
740
+ return;
741
+ }
581
742
 
582
743
  const now = Date.now();
583
744
  progress.durationMs = now - startTime;
@@ -588,11 +749,9 @@ async function runSingleAttempt(
588
749
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
589
750
  ? evt.args as Record<string, unknown>
590
751
  : {};
591
- let shouldDetachForBlockingIntercom = false;
752
+ if (options.structuredOutput && evt.toolName === "structured_output") structuredOutputToolInvoked = true;
592
753
  if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
593
754
  intercomStarted = true;
594
- shouldDetachForBlockingIntercom = (evt.toolName === "intercom" && toolArgs.action === "ask")
595
- || (evt.toolName === "contact_supervisor" && (toolArgs.reason === "need_decision" || toolArgs.reason === "interview_request"));
596
755
  }
597
756
  progress.toolCount++;
598
757
  if (options.toolBudget) {
@@ -606,9 +765,6 @@ async function runSingleAttempt(
606
765
  observedMutationAttempt = observedMutationAttempt || mutates;
607
766
  pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
608
767
  fireUpdate();
609
- if (shouldDetachForBlockingIntercom && !detached && !processClosed) {
610
- detachForIntercom();
611
- }
612
768
  }
613
769
 
614
770
  if (evt.type === "tool_execution_end") {
@@ -627,15 +783,20 @@ async function runSingleAttempt(
627
783
  }
628
784
 
629
785
  if (evt.type === "message_end" && evt.message) {
630
- result.messages.push(evt.message);
786
+ result.messages!.push(evt.message);
631
787
  if (evt.message.role === "assistant") {
632
788
  result.usage.turns++;
633
789
  progress.turnCount = result.usage.turns;
634
790
  const stopReason = (evt.message as { stopReason?: string }).stopReason;
635
- const hasToolCall = Array.isArray(evt.message.content)
636
- && evt.message.content.some((part) => (part as { type?: string }).type === "toolCall");
791
+ const toolCalls = Array.isArray(evt.message.content)
792
+ ? evt.message.content.filter((part) => (part as { type?: string }).type === "toolCall")
793
+ : [];
794
+ const hasToolCall = toolCalls.length > 0;
637
795
  const terminalAssistantStop = stopReason === "stop" && !hasToolCall;
638
- updateTurnBudget(result.usage.turns, terminalAssistantStop);
796
+ const terminalStructuredOutputCall = Boolean(options.structuredOutput)
797
+ && toolCalls.length === 1
798
+ && (toolCalls[0] as { name?: string }).name === "structured_output";
799
+ updateTurnBudget(result.usage.turns, terminalAssistantStop || terminalStructuredOutputCall, hasToolCall || Boolean(progress.currentTool));
639
800
  const u = evt.message.usage;
640
801
  if (u) {
641
802
  result.usage.input += u.input || 0;
@@ -653,7 +814,7 @@ async function runSingleAttempt(
653
814
  if (terminalAssistantStop) {
654
815
  if (!evt.message.errorMessage && assistantText.trim()) assistantError = undefined;
655
816
  cleanTerminalAssistantStopReceived ||= !evt.message.errorMessage;
656
- startFinalDrain();
817
+ applyChildLifecycle(projectChildLifecycle(evt, true));
657
818
  }
658
819
  }
659
820
  updateActivityState(now);
@@ -661,7 +822,7 @@ async function runSingleAttempt(
661
822
  }
662
823
 
663
824
  if (evt.type === "tool_result_end" && evt.message) {
664
- result.messages.push(evt.message);
825
+ result.messages!.push(evt.message);
665
826
  const resultText = extractTextFromContent(evt.message.content);
666
827
  if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
667
828
  result.toolBudgetBlocked = true;
@@ -731,17 +892,36 @@ async function runSingleAttempt(
731
892
  timeoutTimer.unref?.();
732
893
  }
733
894
 
734
- let stderrBuf = "";
895
+ const stderrTail = createBoundedByteTail();
896
+ const failProtocol = (limit: ProtocolOutputLimit): void => {
897
+ if (result.protocolError) return;
898
+ result.protocolError = limit;
899
+ result.error = formatProtocolOutputLimit(limit);
900
+ progress.status = "failed";
901
+ progress.error = result.error;
902
+ progress.durationMs = Date.now() - startTime;
903
+ fireUpdate();
904
+ if (!childExited) {
905
+ trySignalChild(proc, "SIGTERM");
906
+ protocolHardKillTimer = setTimeout(() => {
907
+ if (!childExited) trySignalChild(proc, "SIGKILL");
908
+ }, 3000);
909
+ protocolHardKillTimer.unref?.();
910
+ }
911
+ };
912
+ const stdoutReader = createBoundedLineReader({ onLine: processLine, onLimit: failProtocol });
913
+ const stderrReader = createBoundedLineReader({
914
+ stream: "stderr",
915
+ maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
916
+ onLine: (line) => shared.transcriptWriter?.writeStderrLine(line),
917
+ onLimit: (limit) => shared.transcriptWriter?.writeStderrLine(formatProtocolOutputLimit(limit)),
918
+ });
735
919
 
736
920
  const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
737
- proc.stdout.on("data", (d) => {
738
- buf += d.toString();
739
- const lines = buf.split("\n");
740
- buf = lines.pop() || "";
741
- lines.forEach(processLine);
742
- });
743
- proc.stderr.on("data", (d) => {
744
- stderrBuf += d.toString();
921
+ proc.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
922
+ proc.stderr.on("data", (chunk: Buffer) => {
923
+ stderrTail.push(chunk);
924
+ stderrReader.push(chunk);
745
925
  });
746
926
  proc.on("exit", () => {
747
927
  childExited = true;
@@ -753,38 +933,67 @@ async function runSingleAttempt(
753
933
  void jsonlWriter.close().catch(() => {
754
934
  // JSONL artifact flush is best effort.
755
935
  });
936
+ const toolDiagnosticError = readChildToolDiagnosticError(toolDiagnosticPath);
756
937
  cleanupTempDir(tempDir);
757
- if (buf.trim()) processLine(buf);
758
- if (stderrBuf.trim()) shared.transcriptWriter?.writeStderrText(stderrBuf);
759
- if (!result.error && assistantError) result.error = assistantError;
760
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !result.error;
761
- if (code !== 0 && stderrBuf.trim() && !result.error && !forcedDrainAfterFinalSuccess) {
762
- result.error = stderrBuf.trim();
938
+ stdoutReader.end();
939
+ stderrReader.end();
940
+ const stderr = stderrTail.text();
941
+ let closeError = result.error ?? toolDiagnosticError ?? assistantError;
942
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !closeError;
943
+ if (code !== 0 && stderr.trim() && !closeError && !forcedDrainAfterFinalSuccess) {
944
+ closeError = stderr.trim();
763
945
  }
764
946
  const finalCode = forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (code ?? 1) : (code ?? 0);
765
947
  if (detached) {
766
- result.exitCode = result.error && finalCode === 0 ? 1 : finalCode;
767
- progress.status = result.exitCode === 0 ? "completed" : "failed";
768
- progress.durationMs = Date.now() - startTime;
769
- if (result.error) progress.error = result.error;
770
- result.progressSummary = {
771
- toolCount: progress.toolCount,
772
- tokens: progress.tokens,
773
- durationMs: progress.durationMs,
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,
774
959
  };
775
- const finalOutput = getFinalOutput(result.messages);
776
- result.finalOutput = finalOutput.trim() || result.error || result.finalOutput || "Detached child exited without final output.";
777
- if (result.artifactPaths && options.artifactConfig?.enabled !== false && options.artifactConfig?.includeOutput !== false) {
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) {
778
986
  try {
779
- writeArtifact(result.artifactPaths.outputPath, result.finalOutput);
987
+ writeArtifact(recoveredResult.artifactPaths.outputPath, fullOutput);
780
988
  } catch {
781
989
  // Detached children may outlive test/temp cleanup; recovered status is best-effort.
782
990
  }
783
991
  }
784
- options.onDetachedExit?.(snapshotResult(result, snapshotProgress(progress)));
992
+ options.onDetachedExit?.(recoveredResult);
785
993
  finish(-2);
786
994
  return;
787
995
  }
996
+ if (!result.error && closeError) result.error = closeError;
788
997
  processClosed = true;
789
998
  finish(finalCode);
790
999
  });
@@ -795,7 +1004,8 @@ async function runSingleAttempt(
795
1004
  // JSONL artifact flush is best effort.
796
1005
  });
797
1006
  cleanupTempDir(tempDir);
798
- if (stderrBuf.trim()) shared.transcriptWriter?.writeStderrText(stderrBuf);
1007
+ stdoutReader.end();
1008
+ stderrReader.end();
799
1009
  if (!result.error) {
800
1010
  result.error = error instanceof Error ? error.message : String(error);
801
1011
  }
@@ -805,10 +1015,6 @@ async function runSingleAttempt(
805
1015
  if (options.signal) {
806
1016
  const kill = () => {
807
1017
  if (processClosed || detached) return;
808
- if (options.allowIntercomDetach && intercomStarted && !detached) {
809
- detachForIntercom();
810
- return;
811
- }
812
1018
  proc.kill("SIGTERM");
813
1019
  setTimeout(() => !proc.killed && proc.kill("SIGKILL"), 3000);
814
1020
  };
@@ -861,8 +1067,12 @@ async function runSingleAttempt(
861
1067
  return result;
862
1068
  }
863
1069
  if (result.detached) {
864
- result.exitCode = 0;
865
- result.finalOutput = "Detached for intercom coordination.";
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
+ }
866
1076
  return result;
867
1077
  }
868
1078
 
@@ -870,37 +1080,43 @@ async function runSingleAttempt(
870
1080
  result.exitCode = 1;
871
1081
  }
872
1082
  if (result.exitCode === 0 && !result.error) {
873
- const errInfo = detectSubagentError(result.messages);
874
- if (errInfo.hasError) {
875
- result.exitCode = errInfo.exitCode ?? 1;
876
- result.error = errInfo.details
877
- ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
878
- : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
879
- }
880
- }
881
- if (result.exitCode === 0 && !result.error) {
882
- const finalText = getFinalOutput(result.messages);
1083
+ const messages = result.messages ?? [];
1084
+ const finalText = getFinalOutput(messages);
883
1085
  const missingStructuredOutput = options.structuredOutput
884
1086
  ? !existsSync(options.structuredOutput.outputPath)
885
1087
  : false;
886
- if (!finalText?.trim() && (!options.structuredOutput || missingStructuredOutput)) {
1088
+ const errInfo = detectSubagentError(messages);
1089
+ const missingOutput = !finalText?.trim() && (!options.structuredOutput || missingStructuredOutput);
1090
+ if (missingOutput && (!errInfo.hasError || hasEmptyTerminalAssistantResponse(messages))) {
887
1091
  result.exitCode = 1;
888
1092
  result.error = "Subagent produced no output (possible model cold-start or empty response).";
1093
+ } else if (errInfo.hasError) {
1094
+ result.exitCode = errInfo.exitCode ?? 1;
1095
+ result.error = errInfo.details
1096
+ ? `${errInfo.errorType} failed (exit ${errInfo.exitCode}): ${errInfo.details}`
1097
+ : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
889
1098
  }
890
1099
  }
891
1100
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
892
- const structured = readStructuredOutput({
893
- schema: options.structuredOutput.schema,
894
- schemaPath: options.structuredOutput.schemaPath,
895
- outputPath: options.structuredOutput.outputPath,
896
- });
897
1101
  result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
898
1102
  result.structuredOutputPath = options.structuredOutput.outputPath;
899
- if (structured.error) {
1103
+ if (!structuredOutputToolInvoked) {
900
1104
  result.exitCode = 1;
901
- result.error = structured.error;
1105
+ result.error = MISSING_STRUCTURED_OUTPUT_CALL_ERROR;
1106
+ result.structuredOutputFailed = true;
902
1107
  } else {
903
- result.structuredOutput = structured.value;
1108
+ const structured = await readStructuredOutput({
1109
+ schema: options.structuredOutput.schema,
1110
+ schemaPath: options.structuredOutput.schemaPath,
1111
+ outputPath: options.structuredOutput.outputPath,
1112
+ });
1113
+ if (structured.error) {
1114
+ result.exitCode = 1;
1115
+ result.error = structured.error;
1116
+ result.structuredOutputFailed = true;
1117
+ } else {
1118
+ result.structuredOutput = structured.value;
1119
+ }
904
1120
  }
905
1121
  }
906
1122
 
@@ -919,7 +1135,7 @@ async function runSingleAttempt(
919
1135
  durationMs: progress.durationMs,
920
1136
  };
921
1137
 
922
- const acceptanceOutput = getFinalOutput(result.messages);
1138
+ const acceptanceOutput = getFinalOutput(result.messages ?? []);
923
1139
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
924
1140
  if (result.timedOut) {
925
1141
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
@@ -928,20 +1144,36 @@ async function runSingleAttempt(
928
1144
  : timeoutMessage;
929
1145
  } else if (result.turnBudgetExceeded && result.turnBudget) {
930
1146
  fullOutput = formatTurnBudgetOutput(turnBudgetExceededMessage(result.turnBudget, result.turnBudget.turnCount), fullOutput);
1147
+ } else if (result.turnBudget?.outcome === "termination-deferred") {
1148
+ const note = turnBudgetDeferredNote(result.turnBudget, result.turnBudget.terminationDeferredAtTurn ?? result.turnBudget.turnCount);
1149
+ fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
931
1150
  } else if (result.wrapUpRequested && result.turnBudget?.outcome === "wrap-up-requested") {
932
1151
  const note = turnBudgetSoftNote(result.turnBudget, result.turnBudget.wrapUpRequestedAtTurn ?? result.turnBudget.turnCount);
933
1152
  fullOutput = fullOutput.trim() ? `${note}\n\n${fullOutput}` : note;
934
1153
  }
935
- const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
1154
+ const completionGuardEnabled = isAgentContractV1(options.agentContract) ? agent.completionGuard === true : agent.completionGuard !== false;
1155
+ const completionGuard = result.exitCode === 0 && !result.error && completionGuardEnabled
936
1156
  ? evaluateCompletionMutationGuard({
937
1157
  agent: agent.name,
938
1158
  task: shared.originalTask ?? task,
939
- messages: result.messages,
1159
+ messages: result.messages ?? [],
940
1160
  tools: agent.tools,
941
1161
  mcpDirectTools: agent.mcpDirectTools,
942
1162
  })
943
1163
  : undefined;
944
- if (completionGuard?.triggered && !observedMutationAttempt) {
1164
+ const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
1165
+ if (completionGuard) {
1166
+ result.effects = {
1167
+ ...(result.effects ?? {}),
1168
+ fileMutation: {
1169
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" : "observed" : "not-applicable",
1170
+ expected: completionGuard.expectedMutation,
1171
+ attempted: completionGuard.attemptedMutation || observedMutationAttempt,
1172
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1173
+ },
1174
+ };
1175
+ }
1176
+ if (completionGuardTriggered && !isAgentContractV1(options.agentContract)) {
945
1177
  result.exitCode = 1;
946
1178
  result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
947
1179
  progress.status = "failed";
@@ -1000,20 +1232,24 @@ export async function runSync(
1000
1232
  task: string,
1001
1233
  options: RunSyncOptions,
1002
1234
  ): Promise<SingleResult> {
1235
+ options = {
1236
+ ...options,
1237
+ capabilityCeiling: options.capabilityCeiling ?? resolveCurrentSubagentCapabilityCeiling(options.parentSessionId),
1238
+ };
1003
1239
  const agent = agents.find((a) => a.name === agentName);
1004
1240
  if (!agent) {
1005
- return {
1241
+ return withRunContext({
1006
1242
  agent: agentName,
1007
1243
  task,
1008
1244
  exitCode: 1,
1009
1245
  messages: [],
1010
1246
  usage: emptyUsage(),
1011
1247
  error: `Unknown agent: ${agentName}`,
1012
- };
1248
+ }, options.context);
1013
1249
  }
1014
1250
  const outputModeValidationError = validateFileOnlyOutputMode(options.outputMode, options.outputPath, `Single run (${agentName})`);
1015
1251
  if (outputModeValidationError) {
1016
- return {
1252
+ return withRunContext({
1017
1253
  agent: agentName,
1018
1254
  task,
1019
1255
  exitCode: 1,
@@ -1021,34 +1257,42 @@ export async function runSync(
1021
1257
  usage: emptyUsage(),
1022
1258
  outputMode: options.outputMode,
1023
1259
  error: outputModeValidationError,
1024
- };
1260
+ }, options.context);
1025
1261
  }
1026
1262
 
1027
1263
  const shareEnabled = options.share === true;
1028
1264
  const effectiveAcceptance = resolveEffectiveAcceptance({
1029
1265
  explicit: options.acceptance,
1030
1266
  agentName,
1267
+ acceptanceRole: agent.acceptanceRole,
1031
1268
  task,
1032
1269
  mode: options.acceptanceContext?.mode ?? "single",
1033
1270
  async: options.acceptanceContext?.async,
1034
1271
  dynamic: options.acceptanceContext?.dynamic,
1035
1272
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
1273
+ agentContract: options.agentContract,
1036
1274
  });
1037
- const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
1275
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { reportOptional: isAgentContractV1(options.agentContract) });
1038
1276
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
1039
1277
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
1040
1278
  const skillNames = options.skills ?? agent.skills ?? [];
1041
1279
  const skillCwd = options.cwd ?? runtimeCwd;
1042
- const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, skillCwd, runtimeCwd);
1280
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(
1281
+ skillNames,
1282
+ skillCwd,
1283
+ runtimeCwd,
1284
+ agent.skillPath,
1285
+ agent.filePath ? path.dirname(agent.filePath) : skillCwd,
1286
+ );
1043
1287
  if (skillNames.some((skill) => skill.trim() === "pi-subagents") && missingSkills.includes("pi-subagents")) {
1044
- return {
1288
+ return withRunContext({
1045
1289
  agent: agentName,
1046
1290
  task,
1047
1291
  exitCode: 1,
1048
1292
  messages: [],
1049
1293
  usage: emptyUsage(),
1050
1294
  error: "Skills not found: pi-subagents",
1051
- };
1295
+ }, options.context);
1052
1296
  }
1053
1297
  let systemPrompt = agent.systemPrompt?.trim() || "";
1054
1298
  if (resolvedSkills.length > 0) {
@@ -1059,7 +1303,7 @@ export async function runSync(
1059
1303
  if (memoryInjection) {
1060
1304
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
1061
1305
  }
1062
- systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath);
1306
+ systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath, agent);
1063
1307
 
1064
1308
  const candidates = buildModelCandidates(
1065
1309
  options.modelOverride ?? agent.model,
@@ -1100,12 +1344,82 @@ export async function runSync(
1100
1344
  }
1101
1345
  }
1102
1346
 
1347
+ const persistResultMetadata = (target: SingleResult): void => {
1348
+ if (!artifactPathsResult || options.artifactConfig?.enabled === false || options.artifactConfig?.includeMetadata === false) return;
1349
+ writeMetadata(artifactPathsResult.metadataPath, {
1350
+ runId: options.runId,
1351
+ agent: agentName,
1352
+ 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(),
1374
+ });
1375
+ };
1376
+
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;
1402
+ }
1403
+ }
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);
1412
+ });
1413
+ },
1414
+ }
1415
+ : options;
1416
+
1103
1417
  let lastResult: SingleResult | undefined;
1104
1418
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
1105
1419
  for (let i = 0; i < modelsToTry.length; i++) {
1106
1420
  const candidate = modelsToTry[i];
1107
1421
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
1108
- const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
1422
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, detachedAwareOptions, {
1109
1423
  sessionEnabled,
1110
1424
  systemPrompt,
1111
1425
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -1114,6 +1428,7 @@ export async function runSync(
1114
1428
  artifactPaths: artifactPathsResult,
1115
1429
  transcriptWriter,
1116
1430
  attemptNotes,
1431
+ modelCandidates: candidates.map((candidate) => applyThinkingSuffix(candidate, options.thinkingOverride ?? agent.thinking, options.thinkingOverride !== undefined)),
1117
1432
  outputSnapshot,
1118
1433
  originalTask: task,
1119
1434
  });
@@ -1132,7 +1447,7 @@ export async function runSync(
1132
1447
  usage: { ...result.usage },
1133
1448
  };
1134
1449
  modelAttempts.push(attempt);
1135
- if (result.timedOut || result.turnBudgetExceeded) {
1450
+ if (result.detached || result.timedOut || result.turnBudgetExceeded) {
1136
1451
  break;
1137
1452
  }
1138
1453
  if (attemptSucceeded) {
@@ -1144,14 +1459,14 @@ export async function runSync(
1144
1459
  attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[i + 1]));
1145
1460
  }
1146
1461
 
1147
- const result = lastResult ?? {
1462
+ const result = withRunContext(lastResult ?? {
1148
1463
  agent: agentName,
1149
1464
  task,
1150
1465
  exitCode: 1,
1151
1466
  messages: [],
1152
1467
  usage: emptyUsage(),
1153
1468
  error: "Subagent did not produce a result.",
1154
- } satisfies SingleResult;
1469
+ } satisfies SingleResult, options.context);
1155
1470
 
1156
1471
  result.usage = aggregateUsage;
1157
1472
  result.attemptedModels = attemptedModels.length > 0 ? attemptedModels : undefined;
@@ -1174,29 +1489,13 @@ export async function runSync(
1174
1489
  if (artifactPathsResult && options.artifactConfig?.enabled !== false) {
1175
1490
  result.artifactPaths = artifactPathsResult;
1176
1491
  if (options.artifactConfig?.includeOutput !== false) {
1177
- writeArtifact(artifactPathsResult.outputPath, artifactOutputByResult.get(result) ?? result.finalOutput ?? "");
1178
- }
1179
- if (options.artifactConfig?.includeMetadata !== false) {
1180
- writeMetadata(artifactPathsResult.metadataPath, {
1181
- runId: options.runId,
1182
- agent: agentName,
1183
- task,
1184
- exitCode: result.exitCode,
1185
- usage: result.usage,
1186
- model: result.model,
1187
- attemptedModels: result.attemptedModels,
1188
- modelAttempts: result.modelAttempts,
1189
- durationMs: result.progressSummary?.durationMs,
1190
- toolCount: result.progressSummary?.toolCount,
1492
+ writeArtifact(artifactPathsResult.outputPath, formatOutputArtifactContent({
1493
+ output: artifactOutputByResult.get(result) ?? result.finalOutput ?? "",
1191
1494
  error: result.error,
1192
- ...(transcriptWriter ? { transcriptPath: artifactPathsResult.transcriptPath } : {}),
1193
- transcriptError: result.transcriptError,
1194
- skills: result.skills,
1195
- skillsWarning: result.skillsWarning,
1196
- timestamp: Date.now(),
1197
- });
1495
+ transcriptPath: result.transcriptPath,
1496
+ metadataPath: options.artifactConfig?.includeMetadata === false ? undefined : artifactPathsResult.metadataPath,
1497
+ }));
1198
1498
  }
1199
-
1200
1499
  if (options.maxOutput) {
1201
1500
  const config = { ...DEFAULT_MAX_OUTPUT, ...options.maxOutput };
1202
1501
  const truncationResult = truncateOutput(result.finalOutput ?? "", config, artifactPathsResult.outputPath);
@@ -1215,18 +1514,31 @@ export async function runSync(
1215
1514
  if (sessionFile) result.sessionFile = sessionFile;
1216
1515
  }
1217
1516
 
1218
- result.acceptance = result.timedOut
1219
- ? buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." })
1220
- : result.turnBudgetExceeded
1221
- ? buildSkippedAcceptanceLedger(effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." })
1222
- : await evaluateAcceptance({
1517
+ const childWrittenOutput = options.outputPath
1518
+ ? extractChildWrittenOutput(result.messages, options.outputPath, options.cwd ?? runtimeCwd)
1519
+ : 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({
1223
1530
  acceptance: effectiveAcceptance,
1224
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,
1225
1535
  cwd: options.cwd ?? runtimeCwd,
1536
+ reportOptional: isAgentContractV1(options.agentContract),
1226
1537
  });
1538
+ }
1227
1539
  const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1228
1540
  stripAcceptanceReportsFromMessages(result.messages);
1229
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut) {
1541
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut && !isAgentContractV1(options.agentContract)) {
1230
1542
  result.exitCode = 1;
1231
1543
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1232
1544
  if (result.progress) {
@@ -1234,6 +1546,8 @@ export async function runSync(
1234
1546
  result.progress.error = result.error;
1235
1547
  }
1236
1548
  }
1549
+ if (isAgentContractV1(options.agentContract)) attachContractProjections(result);
1550
+ persistResultMetadata(result);
1237
1551
 
1238
1552
  return result;
1239
1553
  }