@selesai/code 0.5.20 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  4. package/dist/extensions/pi-subagents/README.md +602 -220
  5. package/dist/extensions/pi-subagents/banner.png +0 -0
  6. package/dist/extensions/pi-subagents/index.ts +1 -0
  7. package/dist/extensions/pi-subagents/install.mjs +3 -3
  8. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  9. package/dist/extensions/pi-subagents/package.json +26 -14
  10. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  11. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  12. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  13. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  14. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  15. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  16. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  18. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  19. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  20. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  21. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  22. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  23. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  24. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  25. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  26. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  27. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  28. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  29. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  30. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  31. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  32. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  33. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  34. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  35. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  36. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  37. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  38. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  39. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  40. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  41. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  42. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  43. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  44. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  45. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  46. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  48. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  49. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  50. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  51. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  52. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  53. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  54. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  55. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  56. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  57. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  58. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  59. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  60. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  61. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  63. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  65. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  66. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  67. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  68. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  69. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  70. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  71. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  72. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  73. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  74. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  76. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  78. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  80. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  82. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  83. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  87. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  88. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  89. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  90. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  92. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  93. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  94. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  95. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  96. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  97. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  98. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  99. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  100. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  101. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  102. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  103. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  104. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  105. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  106. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  107. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  108. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  109. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  110. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  111. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  112. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  113. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  114. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  115. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  116. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  117. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  118. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  119. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  120. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  121. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  122. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  124. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  125. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  126. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  127. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  140. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  141. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  142. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  143. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  144. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  145. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  146. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  147. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  148. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  149. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  150. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  151. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  152. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  153. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  154. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  155. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  156. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  157. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  158. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  159. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  160. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  161. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  162. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  163. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  164. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  165. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  166. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  167. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  168. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  169. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  170. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  171. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  172. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  173. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  174. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  175. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  176. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  177. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  178. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  179. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  180. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  181. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  182. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  183. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  184. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  185. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  186. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  187. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  188. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  189. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  190. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  191. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  192. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  193. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  194. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  195. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  196. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  197. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  198. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  199. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  200. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  201. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  202. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  203. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  204. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  205. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  206. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  207. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  208. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  209. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  210. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  211. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  212. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  213. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  214. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  215. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  216. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  217. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  218. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  222. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  223. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  224. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  225. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  226. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  227. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  228. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  229. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  230. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  232. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  233. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  234. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  235. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  236. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  237. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  238. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  239. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  240. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  250. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  251. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  252. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  253. package/dist/extensions/question/batch.ts +39 -0
  254. package/dist/extensions/question/dialog-adapter.ts +23 -1
  255. package/dist/extensions/question/index.ts +237 -7
  256. package/dist/extensions/question/schemas.ts +28 -15
  257. package/dist/extensions/question/tests/batch.test.ts +26 -0
  258. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  259. package/dist/extensions/question/tui-adapter.ts +2 -3
  260. package/dist/extensions/question/types.ts +37 -0
  261. package/dist/extensions/question/ui-protocol.ts +3 -3
  262. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  263. package/package.json +1 -1
@@ -1,14 +1,15 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
2
3
  import * as fs from "node:fs";
3
4
  import * as path from "node:path";
4
5
  import { pathToFileURL } from "node:url";
5
6
  import type { Message } from "@earendil-works/pi-ai";
6
7
  import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
8
  import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
8
- import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
9
- import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
10
- import { SELESAI_PACKAGE, getSelesaiSpawnCommand, resolveInstalledSelesaiPackageRoot } from "../shared/pi-spawn.ts";
11
- import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
9
+ import { closeSteerInbox, consumeInterruptRequest, consumeSteerRequests, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest, enqueueStepSteer, steerAcksDir, steerCapabilityPath, stepSteerInboxDir, watchAsyncControlInbox, type SteerAck, type SteerCapability, type SteerRequest } from "./control-channel.ts";
10
+ import { appendJsonl as appendRawJsonl, formatOutputArtifactContent, getArtifactPaths } from "../../shared/artifacts.ts";
11
+ import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
12
+ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleOutput, formatSavedOutputReference, injectOutputPathSystemPrompt, injectSingleOutputInstruction, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
12
13
  import {
13
14
  type ActivityState,
14
15
  type ArtifactConfig,
@@ -28,6 +29,9 @@ import {
28
29
  type TurnBudgetState,
29
30
  type Usage,
30
31
  type WorkflowGraphSnapshot,
32
+ type SteeringStatus,
33
+ type SteeringTargetState,
34
+ type SteeringTargetStatus,
31
35
  DEFAULT_MAX_OUTPUT,
32
36
  type MaxOutputConfig,
33
37
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
@@ -54,14 +58,17 @@ import {
54
58
  DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
55
59
  Semaphore,
56
60
  } from "../shared/parallel-utils.ts";
57
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
61
+ import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, resolvePiLaunchToolPlan } from "../shared/pi-args.ts";
58
62
  import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
59
63
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
64
+ import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
60
65
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
61
66
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
62
67
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
68
+ import { markProcessTerminalCandidateLeaseRelease, writeProcessTerminalCandidate, type ProcessTerminalCandidate } from "./process-terminal.ts";
69
+ import { createSteeringStatus, recordSteeringRequest, steeringStatus, terminalSteeringNoticeState, updateSteeringTarget } from "./steering.ts";
63
70
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
64
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
71
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, hasEmptyTerminalAssistantResponse, readStatus } from "../../shared/utils.ts";
65
72
  import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
66
73
  import {
67
74
  createMutatingFailureState,
@@ -86,13 +93,29 @@ import {
86
93
  type WorktreeSetup,
87
94
  } from "../shared/worktree.ts";
88
95
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
96
+ import { launchBindingDigest } from "../../shared/launch-contract.ts";
89
97
  import { writeInitialProgressFile } from "../../shared/settings.ts";
90
98
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
91
- import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
99
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
100
+ import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
92
101
  import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
93
102
  import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
94
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
103
+ import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, turnBudgetDecision, turnBudgetDeferredNote, turnBudgetDeferredState, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
95
104
  import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
105
+ import { formatParallelHandoffError, formatParallelHandoffReference, parallelHandoffPath, writeParallelHandoffGroup } from "../shared/parallel-handoff.ts";
106
+ import { resolveWatchdogConfig } from "../../watchdog/settings.ts";
107
+ import { createBoundedByteTail, createBoundedLineReader, formatProtocolOutputLimit, MAX_CHILD_STDERR_BYTES, projectChildLifecycle, type ChildLifecycleAction, type ProtocolOutputLimit } from "../shared/child-protocol.ts";
108
+ import { acquireSessionLease, type SessionLeaseRequest } from "../shared/session-lease.ts";
109
+ import { decodeSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
110
+ import {
111
+ CHILD_WATCHDOG_CONFIG_ENV,
112
+ acceptChildWatchdogEvent,
113
+ childWatchdogIsActive,
114
+ decodeChildWatchdogConfig,
115
+ isChildWatchdogStatusEvent,
116
+ resolveChildWatchdogConfig,
117
+ type ChildWatchdogStateSnapshot,
118
+ } from "../../watchdog/child-status.ts";
96
119
 
97
120
  interface SubagentRunConfig {
98
121
  id: string;
@@ -126,19 +149,29 @@ interface SubagentRunConfig {
126
149
  deadlineAt?: number;
127
150
  turnBudget?: ResolvedTurnBudget;
128
151
  toolBudget?: ResolvedToolBudget;
152
+ revivalLease?: SessionLeaseRequest;
153
+ revivalLeaseToken?: string;
129
154
  /** Global cap on simultaneously-running subagent tasks within this run. */
130
155
  globalConcurrencyLimit?: number;
156
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
157
+ launchContractDigest?: string;
158
+ runnerProcessInstanceId?: string;
131
159
  }
132
160
 
133
161
  interface StepResult {
134
162
  agent: string;
163
+ context?: "fresh" | "fork";
164
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
165
+ capabilityAudit?: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit;
135
166
  output: string;
136
167
  error?: string;
168
+ protocolError?: ProtocolOutputLimit;
137
169
  success: boolean;
138
170
  exitCode?: number | null;
139
171
  skipped?: boolean;
140
172
  interrupted?: boolean;
141
173
  timedOut?: boolean;
174
+ stopped?: boolean;
142
175
  turnBudget?: TurnBudgetState;
143
176
  turnBudgetExceeded?: boolean;
144
177
  wrapUpRequested?: boolean;
@@ -154,10 +187,18 @@ interface StepResult {
154
187
  truncated?: boolean;
155
188
  transcriptPath?: string;
156
189
  transcriptError?: string;
190
+ agentContract?: import("../../shared/types.ts").AgentContract;
191
+ launchContractDigest?: string;
192
+ execution?: import("../../shared/types.ts").ExecutionProjection;
193
+ review?: import("../../shared/types.ts").ReviewProjection;
194
+ effects?: import("../../shared/types.ts").EffectsProjection;
157
195
  structuredOutput?: unknown;
158
196
  structuredOutputPath?: string;
159
197
  structuredOutputSchemaPath?: string;
160
198
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
199
+ watchdog?: import("../../shared/types.ts").ChildWatchdogProgress;
200
+ writerProcesses?: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>;
201
+ writerAttemptCount?: number;
161
202
  }
162
203
 
163
204
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -235,6 +276,15 @@ function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
235
276
  return event.type !== "message_update";
236
277
  }
237
278
 
279
+ function isBlockingSupervisorTool(toolName: string | undefined, args: unknown): boolean {
280
+ if (!args || typeof args !== "object" || Array.isArray(args)) return false;
281
+ if (toolName === "contact_supervisor") {
282
+ const reason = (args as Record<string, unknown>).reason;
283
+ return reason === "need_decision" || reason === "interview_request";
284
+ }
285
+ return toolName === "intercom" && (args as Record<string, unknown>).action === "ask";
286
+ }
287
+
238
288
  function findLatestSessionFile(sessionDir: string): string | null {
239
289
  try {
240
290
  const files = fs
@@ -254,19 +304,6 @@ function emptyUsage(): Usage {
254
304
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
255
305
  }
256
306
 
257
- function usageFromAttempts(attempts: ModelAttempt[] | undefined): Usage {
258
- const usage = emptyUsage();
259
- for (const attempt of attempts ?? []) {
260
- usage.input += attempt.usage?.input ?? 0;
261
- usage.output += attempt.usage?.output ?? 0;
262
- usage.cacheRead += attempt.usage?.cacheRead ?? 0;
263
- usage.cacheWrite += attempt.usage?.cacheWrite ?? 0;
264
- usage.cost += attempt.usage?.cost ?? 0;
265
- usage.turns += attempt.usage?.turns ?? 0;
266
- }
267
- return usage;
268
- }
269
-
270
307
  function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
271
308
  if (!attempts || attempts.length === 0) return null;
272
309
  let input = 0;
@@ -304,11 +341,13 @@ function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
304
341
  }
305
342
  }
306
343
 
307
- function isTerminalAssistantStop(message: Message): boolean {
308
- const stopReason = (message as { stopReason?: string }).stopReason;
309
- const hasToolCall = Array.isArray(message.content)
344
+ function assistantStartsToolCall(message: Message): boolean {
345
+ return Array.isArray(message.content)
310
346
  && message.content.some((part) => (part as { type?: string }).type === "toolCall");
311
- return stopReason === "stop" && !hasToolCall;
347
+ }
348
+
349
+ function isTerminalAssistantStop(message: Message): boolean {
350
+ return (message as { stopReason?: string }).stopReason === "stop" && !assistantStartsToolCall(message);
312
351
  }
313
352
 
314
353
  function resetStepLiveDetail(step: RunnerStatusStep): void {
@@ -348,6 +387,7 @@ interface ChildEvent {
348
387
  message?: ChildMessage;
349
388
  toolName?: string;
350
389
  args?: Record<string, unknown>;
390
+ willRetry?: unknown;
351
391
  }
352
392
 
353
393
  interface RunPiStreamingResult {
@@ -357,15 +397,21 @@ interface RunPiStreamingResult {
357
397
  usage: Usage;
358
398
  model?: string;
359
399
  error?: string;
400
+ protocolError?: ProtocolOutputLimit;
360
401
  finalOutput: string;
361
402
  interrupted?: boolean;
362
403
  timedOut?: boolean;
404
+ stopped?: boolean;
363
405
  turnBudget?: TurnBudgetState;
364
406
  turnBudgetExceeded?: boolean;
365
407
  wrapUpRequested?: boolean;
366
408
  toolBudget?: ToolBudgetState;
367
409
  toolBudgetBlocked?: boolean;
368
410
  observedMutationAttempt?: boolean;
411
+ watchdog?: ChildWatchdogStateSnapshot;
412
+ processInstanceId: string;
413
+ processCloseObservedAt?: number;
414
+ processSignal?: string | null;
369
415
  }
370
416
 
371
417
  function runPiStreaming(
@@ -382,12 +428,17 @@ function runPiStreaming(
382
428
  transcriptWriter?: ChildTranscriptWriter,
383
429
  registerTimeout?: (interrupt: (() => void) | undefined) => void,
384
430
  timeoutMessage?: string,
431
+ registerStop?: (stop: (() => void) | undefined) => void,
432
+ stopMessage?: string,
385
433
  registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
434
+ onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
386
435
  ): Promise<RunPiStreamingResult> {
387
436
  return new Promise((resolve) => {
437
+ const processInstanceId = randomUUID();
438
+ onWriterProcess?.({ state: "spawning" });
388
439
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
389
440
  const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
390
- const spawnSpec = getSelesaiSpawnCommand(args, {
441
+ const spawnSpec = getPiSpawnCommand(args, {
391
442
  ...(piPackageRoot ? { piPackageRoot } : {}),
392
443
  ...(piArgv1 ? { argv1: piArgv1 } : {}),
393
444
  });
@@ -397,21 +448,35 @@ function runPiStreaming(
397
448
  env: spawnEnv,
398
449
  windowsHide: true,
399
450
  });
400
- let stderr = "";
401
- let stdoutBuf = "";
402
- let stderrBuf = "";
451
+ const stderrTail = createBoundedByteTail();
452
+ const rawStdoutTail = createBoundedByteTail();
403
453
  const messages: Message[] = [];
404
454
  const usage = emptyUsage();
405
455
  let model: string | undefined;
406
- let error: string | undefined;
456
+ let writerRegistrationError: string | undefined;
457
+ if (typeof child.pid === "number") {
458
+ try {
459
+ onWriterProcess?.({ state: "running", pid: child.pid });
460
+ } catch (writerError) {
461
+ writerRegistrationError = `Failed to record revived Pi writer ownership: ${writerError instanceof Error ? writerError.message : String(writerError)}`;
462
+ trySignalChild(child, "SIGKILL");
463
+ }
464
+ }
465
+ let error: string | undefined = writerRegistrationError;
407
466
  let assistantError: string | undefined;
408
467
  let interrupted = false;
409
468
  let timedOut = false;
469
+ let stopped = false;
410
470
  let turnBudgetExceeded = false;
411
471
  let turnBudgetMessage: string | undefined;
412
472
  let turnBudget: TurnBudgetState | undefined;
413
473
  let observedMutationAttempt = false;
414
- const rawStdoutLines: string[] = [];
474
+ const childWatchdogConfig = decodeChildWatchdogConfig(env?.[CHILD_WATCHDOG_CONFIG_ENV]);
475
+ let childWatchdogState: ChildWatchdogStateSnapshot | undefined;
476
+ let applyChildLifecycle = (_action: ChildLifecycleAction): void => {};
477
+ const updateChildWatchdogState = (snapshot: ChildWatchdogStateSnapshot): void => {
478
+ childWatchdogState = snapshot;
479
+ };
415
480
 
416
481
  const writeOutputLine = (line: string) => {
417
482
  if (!line.trim()) return;
@@ -449,7 +514,7 @@ function runPiStreaming(
449
514
  try {
450
515
  event = JSON.parse(line) as ChildEvent;
451
516
  } catch {
452
- rawStdoutLines.push(line);
517
+ rawStdoutTail.push(`${line}\n`);
453
518
  writeOutputLine(line);
454
519
  appendChildLine("subagent.child.stdout", line);
455
520
  return;
@@ -457,6 +522,38 @@ function runPiStreaming(
457
522
 
458
523
  appendChildEvent(event);
459
524
  transcriptWriter?.writeChildEvent(event);
525
+ if (event.type === "agent_settled") agentSettledReceived = true;
526
+ applyChildLifecycle(projectChildLifecycle(event));
527
+
528
+ if (isChildWatchdogStatusEvent(event)) {
529
+ if (!childWatchdogConfig) return;
530
+ const next = acceptChildWatchdogEvent({
531
+ current: childWatchdogState,
532
+ event,
533
+ runId: childEventContext?.runId,
534
+ agent: childEventContext?.agent,
535
+ childIndex: childEventContext?.stepIndex,
536
+ });
537
+ if (!next) return;
538
+ updateChildWatchdogState(next);
539
+ onChildEvent?.(event);
540
+ if (childWatchdogIsActive(next)) {
541
+ if (finalDrainTimer) {
542
+ clearTimeout(finalDrainTimer);
543
+ finalDrainTimer = undefined;
544
+ }
545
+ if (finalHardKillTimer) {
546
+ clearTimeout(finalHardKillTimer);
547
+ finalHardKillTimer = undefined;
548
+ }
549
+ armWatchdogTail();
550
+ } else {
551
+ clearWatchdogTailTimer();
552
+ if (cleanTerminalAssistantStopReceived || agentSettledReceived) startFinalDrain();
553
+ }
554
+ return;
555
+ }
556
+
460
557
  onChildEvent?.(event);
461
558
 
462
559
  if (event.type === "tool_execution_start" && event.toolName) {
@@ -486,24 +583,11 @@ function runPiStreaming(
486
583
  if (isTerminalAssistantStop(event.message)) {
487
584
  if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
488
585
  cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
489
- startFinalDrain();
586
+ applyChildLifecycle(projectChildLifecycle(event, true));
490
587
  }
491
588
  }
492
589
  };
493
590
 
494
- const processStderrText = (text: string) => {
495
- stderr += text;
496
- stderrBuf += text;
497
- outputStream.write(text);
498
- if (!childEventContext) return;
499
- const lines = stderrBuf.split("\n");
500
- stderrBuf = lines.pop() || "";
501
- for (const line of lines) {
502
- if (!line.trim()) continue;
503
- appendChildLine("subagent.child.stderr", line);
504
- }
505
- };
506
-
507
591
  // Guard both cases that can leave the parent waiting on `close` forever:
508
592
  // a lingering stdio holder after `exit`, or a child that never exits.
509
593
  const FINAL_STOP_GRACE_MS = 1000;
@@ -512,35 +596,68 @@ function runPiStreaming(
512
596
  let childExited = false;
513
597
  let forcedTerminationSignal = false;
514
598
  let cleanTerminalAssistantStopReceived = false;
599
+ let agentSettledReceived = false;
515
600
  let finalDrainTimer: NodeJS.Timeout | undefined;
516
601
  let finalHardKillTimer: NodeJS.Timeout | undefined;
602
+ let watchdogTailTimer: NodeJS.Timeout | undefined;
517
603
  let timeoutHardKillTimer: NodeJS.Timeout | undefined;
518
604
  let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
519
605
  let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
606
+ let protocolHardKillTimer: NodeJS.Timeout | undefined;
607
+ let protocolError: ProtocolOutputLimit | undefined;
520
608
  let settled = false;
521
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
522
- child.stdout.on("data", (chunk: Buffer) => {
523
- const text = chunk.toString();
524
- stdoutBuf += text;
525
- const lines = stdoutBuf.split("\n");
526
- stdoutBuf = lines.pop() || "";
527
- for (const line of lines) processStdoutLine(line);
609
+ applyChildLifecycle = (action: ChildLifecycleAction): void => {
610
+ if (action === "cancel-drain") {
611
+ if (finalDrainTimer) {
612
+ clearTimeout(finalDrainTimer);
613
+ finalDrainTimer = undefined;
614
+ }
615
+ if (finalHardKillTimer) {
616
+ clearTimeout(finalHardKillTimer);
617
+ finalHardKillTimer = undefined;
618
+ }
619
+ clearWatchdogTailTimer();
620
+ return;
621
+ }
622
+ if (action === "start-drain") startFinalDrain();
623
+ };
624
+ const failProtocol = (limit: ProtocolOutputLimit): void => {
625
+ if (protocolError) return;
626
+ protocolError = limit;
627
+ error = formatProtocolOutputLimit(limit);
628
+ if (!childExited) {
629
+ trySignalChild(child, "SIGTERM");
630
+ protocolHardKillTimer = setTimeout(() => {
631
+ if (!settled) trySignalChild(child, "SIGKILL");
632
+ }, 3000);
633
+ protocolHardKillTimer.unref?.();
634
+ }
635
+ };
636
+ const stdoutReader = createBoundedLineReader({ onLine: processStdoutLine, onLimit: failProtocol });
637
+ const stderrReader = createBoundedLineReader({
638
+ stream: "stderr",
639
+ maxPendingLineBytes: MAX_CHILD_STDERR_BYTES,
640
+ onLine: (line) => appendChildLine("subagent.child.stderr", line),
641
+ onLimit: (limit) => appendChildLine("subagent.child.stderr", formatProtocolOutputLimit(limit)),
528
642
  });
529
-
643
+ const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
644
+ child.stdout.on("data", (chunk: Buffer) => stdoutReader.push(chunk));
530
645
  child.stderr.on("data", (chunk: Buffer) => {
531
- processStderrText(chunk.toString());
646
+ stderrTail.push(chunk);
647
+ stderrReader.push(chunk);
648
+ outputStream.write(chunk);
532
649
  });
533
650
  registerInterrupt?.(() => {
534
- if (settled || timedOut) return;
651
+ if (settled || timedOut || stopped) return;
535
652
  interrupted = true;
536
653
  if (!error) error = "Interrupted. Waiting for explicit next action.";
537
654
  trySignalChild(child, "SIGINT");
538
655
  setTimeout(() => {
539
- if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
656
+ if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
540
657
  }, 1000).unref?.();
541
658
  });
542
659
  registerTimeout?.(() => {
543
- if (settled || timedOut) return;
660
+ if (settled || timedOut || stopped) return;
544
661
  timedOut = true;
545
662
  interrupted = false;
546
663
  error = timeoutMessage ?? "Subagent timed out.";
@@ -550,8 +667,19 @@ function runPiStreaming(
550
667
  }, TIMEOUT_HARD_KILL_MS);
551
668
  timeoutHardKillTimer.unref?.();
552
669
  });
670
+ registerStop?.(() => {
671
+ if (settled || timedOut || stopped) return;
672
+ stopped = true;
673
+ interrupted = false;
674
+ error = stopMessage ?? "Subagent stopped by user.";
675
+ trySignalChild(child, "SIGTERM");
676
+ timeoutHardKillTimer = setTimeout(() => {
677
+ if (!settled) trySignalChild(child, "SIGKILL");
678
+ }, TIMEOUT_HARD_KILL_MS);
679
+ timeoutHardKillTimer.unref?.();
680
+ });
553
681
  registerTurnBudgetAbort?.((message, state) => {
554
- if (settled || timedOut || turnBudgetExceeded) return;
682
+ if (settled || timedOut || stopped || turnBudgetExceeded) return;
555
683
  turnBudgetExceeded = true;
556
684
  turnBudgetMessage = message;
557
685
  turnBudget = state;
@@ -559,11 +687,11 @@ function runPiStreaming(
559
687
  error = message;
560
688
  trySignalChild(child, "SIGINT");
561
689
  turnBudgetTerminationTimer = setTimeout(() => {
562
- if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
690
+ if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGTERM");
563
691
  }, 1000);
564
692
  turnBudgetTerminationTimer.unref?.();
565
693
  turnBudgetHardKillTimer = setTimeout(() => {
566
- if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
694
+ if (!settled && !timedOut && !stopped) trySignalChild(child, "SIGKILL");
567
695
  }, 4000);
568
696
  turnBudgetHardKillTimer.unref?.();
569
697
  });
@@ -576,6 +704,7 @@ function runPiStreaming(
576
704
  clearTimeout(finalHardKillTimer);
577
705
  finalHardKillTimer = undefined;
578
706
  }
707
+ clearWatchdogTailTimer();
579
708
  if (timeoutHardKillTimer) {
580
709
  clearTimeout(timeoutHardKillTimer);
581
710
  timeoutHardKillTimer = undefined;
@@ -588,16 +717,24 @@ function runPiStreaming(
588
717
  clearTimeout(turnBudgetHardKillTimer);
589
718
  turnBudgetHardKillTimer = undefined;
590
719
  }
720
+ if (protocolHardKillTimer) {
721
+ clearTimeout(protocolHardKillTimer);
722
+ protocolHardKillTimer = undefined;
723
+ }
591
724
  };
592
725
  function startFinalDrain(): void {
726
+ if (childWatchdogIsActive(childWatchdogState)) {
727
+ armWatchdogTail();
728
+ return;
729
+ }
593
730
  if (childExited || finalDrainTimer || settled) return;
594
731
  finalDrainTimer = setTimeout(() => {
595
732
  if (settled) return;
596
733
  const termSent = trySignalChild(child, "SIGTERM");
597
734
  if (!termSent) return;
598
735
  forcedTerminationSignal = true;
599
- if (!cleanTerminalAssistantStopReceived && !error && !assistantError) {
600
- error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
736
+ if (!cleanTerminalAssistantStopReceived && !agentSettledReceived && !error && !assistantError) {
737
+ error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its terminal event. Forcing termination.`;
601
738
  }
602
739
  finalHardKillTimer = setTimeout(() => {
603
740
  if (settled) return;
@@ -607,63 +744,108 @@ function runPiStreaming(
607
744
  }, FINAL_STOP_GRACE_MS);
608
745
  finalDrainTimer.unref?.();
609
746
  }
747
+ function clearWatchdogTailTimer(): void {
748
+ if (watchdogTailTimer) {
749
+ clearTimeout(watchdogTailTimer);
750
+ watchdogTailTimer = undefined;
751
+ }
752
+ }
753
+ function armWatchdogTail(): void {
754
+ if ((!cleanTerminalAssistantStopReceived && !agentSettledReceived) || watchdogTailTimer || settled) return;
755
+ watchdogTailTimer = setTimeout(() => {
756
+ watchdogTailTimer = undefined;
757
+ updateChildWatchdogState({
758
+ phase: "stale",
759
+ seq: (childWatchdogState?.seq ?? 0) + 1,
760
+ lastUpdate: Date.now(),
761
+ followUpPending: false,
762
+ reason: "child watchdog tail timeout",
763
+ timedOut: true,
764
+ });
765
+ startFinalDrain();
766
+ }, childWatchdogConfig?.watchdogTailTimeoutMs ?? 120_000);
767
+ watchdogTailTimer.unref?.();
768
+ }
610
769
  child.on("exit", () => {
611
770
  childExited = true;
612
771
  clearDrainTimers();
613
772
  });
614
773
  child.on("close", (exitCode, signal) => {
615
774
  settled = true;
775
+ const processCloseObservedAt = Date.now();
776
+ try {
777
+ onWriterProcess?.({ state: "none" });
778
+ } catch {
779
+ // The runner still owns and releases the lease during finalization.
780
+ }
616
781
  registerInterrupt?.(undefined);
617
782
  registerTimeout?.(undefined);
783
+ registerStop?.(undefined);
618
784
  registerTurnBudgetAbort?.(undefined);
619
785
  clearDrainTimers();
620
786
  clearStdioGuard();
621
- if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
622
- if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
787
+ stdoutReader.end();
788
+ stderrReader.end();
623
789
  outputStream.end();
624
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
790
+ const stderr = stderrTail.text();
791
+ const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
625
792
  const finalError = error ?? assistantError;
626
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
793
+ const forcedDrainAfterFinalSuccess = forcedTerminationSignal && (cleanTerminalAssistantStopReceived || agentSettledReceived) && !finalError;
627
794
  resolve({
628
795
  stderr,
629
- exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
796
+ exitCode: timedOut || stopped ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
630
797
  messages,
631
798
  usage,
632
799
  model,
633
- error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
634
- finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
800
+ error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
801
+ protocolError,
802
+ finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
635
803
  interrupted,
636
804
  timedOut,
805
+ stopped,
637
806
  turnBudget,
638
807
  turnBudgetExceeded,
639
- wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
808
+ wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
640
809
  observedMutationAttempt,
810
+ watchdog: childWatchdogState,
811
+ processInstanceId,
812
+ processCloseObservedAt,
813
+ processSignal: signal,
641
814
  });
642
815
  });
643
816
 
644
817
  child.on("error", (spawnError) => {
645
818
  settled = true;
819
+ try {
820
+ onWriterProcess?.({ state: "none" });
821
+ } catch {
822
+ // The runner still owns and releases the lease during finalization.
823
+ }
646
824
  registerInterrupt?.(undefined);
647
825
  registerTimeout?.(undefined);
826
+ registerStop?.(undefined);
648
827
  registerTurnBudgetAbort?.(undefined);
649
828
  clearDrainTimers();
650
829
  clearStdioGuard();
830
+ stdoutReader.end();
831
+ stderrReader.end();
651
832
  outputStream.end();
652
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
833
+ const stderr = stderrTail.text();
834
+ const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
653
835
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
654
- resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
836
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
655
837
  });
656
838
  });
657
839
  }
658
840
 
659
- function resolveSelesaiPackageRootFallback(): string {
660
- const root = resolveInstalledSelesaiPackageRoot();
841
+ function resolvePiPackageRootFallback(): string {
842
+ const root = resolveInstalledPiPackageRoot();
661
843
  if (root) return root;
662
- throw new Error(`Could not resolve ${SELESAI_PACKAGE} package root`);
844
+ throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
663
845
  }
664
846
 
665
847
  async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {
666
- const pkgRoot = piPackageRoot ?? resolveSelesaiPackageRootFallback();
848
+ const pkgRoot = piPackageRoot ?? resolvePiPackageRootFallback();
667
849
  const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
668
850
  const moduleUrl = pathToFileURL(exportModulePath).href;
669
851
  const mod = await import(moduleUrl);
@@ -777,20 +959,27 @@ interface SingleStepContext {
777
959
  flatStepCount: number;
778
960
  outputFile: string;
779
961
  steerInboxDir?: string;
962
+ steerCapabilityPath?: string;
963
+ steerAckDir?: string;
780
964
  transcriptPath?: string;
781
965
  piPackageRoot?: string;
782
966
  piArgv1?: string;
783
967
  registerInterrupt?: (interrupt: (() => void) | undefined) => void;
784
968
  registerTimeout?: (interrupt: (() => void) | undefined) => void;
969
+ registerStop?: (stop: (() => void) | undefined) => void;
785
970
  registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
786
971
  timeoutSignal?: AbortSignal;
972
+ stopSignal?: AbortSignal;
787
973
  timeoutMessage?: string;
974
+ stopMessage?: string;
788
975
  turnBudget?: ResolvedTurnBudget;
789
976
  childIntercomTarget?: string;
790
977
  orchestratorIntercomTarget?: string;
791
978
  nestedRoute?: NestedRouteInfo;
979
+ capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
792
980
  onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
793
981
  onChildEvent?: (event: ChildEvent) => void;
982
+ onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void;
794
983
  skipAcceptance?: () => boolean;
795
984
  }
796
985
 
@@ -800,6 +989,9 @@ async function runSingleStep(
800
989
  ctx: SingleStepContext,
801
990
  ): Promise<{
802
991
  agent: string;
992
+ context?: "fresh" | "fork";
993
+ agentContract?: import("../../shared/types.ts").AgentContract;
994
+ launchContractDigest?: string;
803
995
  output: string;
804
996
  exitCode: number | null;
805
997
  error?: string;
@@ -811,6 +1003,7 @@ async function runSingleStep(
811
1003
  transcriptError?: string;
812
1004
  interrupted?: boolean;
813
1005
  timedOut?: boolean;
1006
+ stopped?: boolean;
814
1007
  turnBudget?: TurnBudgetState;
815
1008
  turnBudgetExceeded?: boolean;
816
1009
  wrapUpRequested?: boolean;
@@ -819,13 +1012,18 @@ async function runSingleStep(
819
1012
  sessionFile?: string;
820
1013
  intercomTarget?: string;
821
1014
  completionGuardTriggered?: boolean;
1015
+ effects?: import("../../shared/types.ts").EffectsProjection;
1016
+ execution?: import("../../shared/types.ts").ExecutionProjection;
1017
+ review?: import("../../shared/types.ts").ReviewProjection;
822
1018
  structuredOutput?: unknown;
823
1019
  structuredOutputPath?: string;
824
1020
  structuredOutputSchemaPath?: string;
825
1021
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
1022
+ writerAttemptCount?: number;
826
1023
  }> {
827
1024
  if (step.importAsyncRoot) {
828
1025
  let importTimedOut = false;
1026
+ let importStopped = false;
829
1027
  ctx.registerTimeout?.(() => {
830
1028
  importTimedOut = true;
831
1029
  let pid: number | undefined;
@@ -840,36 +1038,54 @@ async function runSingleStep(
840
1038
  // The parent runner's own timeout result is authoritative for the attached step.
841
1039
  }
842
1040
  });
1041
+ ctx.registerStop?.(() => {
1042
+ importStopped = true;
1043
+ let pid: number | undefined;
1044
+ try {
1045
+ pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
1046
+ } catch {
1047
+ pid = undefined;
1048
+ }
1049
+ try {
1050
+ deliverStopRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-stop" });
1051
+ } catch {
1052
+ // The parent runner's own stopped result is authoritative for the attached step.
1053
+ }
1054
+ });
843
1055
  try {
844
1056
  const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
845
- shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
846
- timeoutMessage: ctx.timeoutMessage,
1057
+ shouldAbort: () => importTimedOut || importStopped || ctx.timeoutSignal?.aborted === true || ctx.stopSignal?.aborted === true || ctx.skipAcceptance?.() === true,
1058
+ timeoutMessage: importStopped || ctx.stopSignal?.aborted === true ? ctx.stopMessage : ctx.timeoutMessage,
847
1059
  });
848
1060
  try {
849
1061
  fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
850
1062
  } catch {
851
1063
  // Output files are observability only for imported roots.
852
1064
  }
853
- const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
1065
+ const stopped = importStopped || imported.stopped === true || ctx.stopSignal?.aborted === true;
1066
+ const timedOut = !stopped && (importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true);
1067
+ const message = stopped ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.";
854
1068
  return {
855
1069
  agent: imported.agent,
856
- output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
857
- exitCode: timedOut ? 1 : imported.exitCode,
858
- error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
1070
+ output: timedOut || stopped ? message : imported.output,
1071
+ exitCode: timedOut || stopped ? 1 : imported.exitCode,
1072
+ error: timedOut || stopped ? message : imported.error,
859
1073
  timedOut: timedOut ? true : undefined,
1074
+ stopped: stopped ? true : undefined,
860
1075
  sessionFile: imported.sessionFile,
861
1076
  intercomTarget: imported.intercomTarget,
862
1077
  model: imported.model,
863
1078
  attemptedModels: imported.attemptedModels,
864
1079
  modelAttempts: imported.modelAttempts,
865
1080
  totalCost: imported.totalCost,
866
- structuredOutput: timedOut ? undefined : imported.structuredOutput,
867
- structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
868
- structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
869
- acceptance: timedOut ? undefined : imported.acceptance,
1081
+ structuredOutput: timedOut || stopped ? undefined : imported.structuredOutput,
1082
+ structuredOutputPath: timedOut || stopped ? undefined : imported.structuredOutputPath,
1083
+ structuredOutputSchemaPath: timedOut || stopped ? undefined : imported.structuredOutputSchemaPath,
1084
+ acceptance: timedOut || stopped ? undefined : imported.acceptance,
870
1085
  };
871
1086
  } finally {
872
1087
  ctx.registerTimeout?.(undefined);
1088
+ ctx.registerStop?.(undefined);
873
1089
  }
874
1090
  }
875
1091
 
@@ -878,10 +1094,10 @@ async function runSingleStep(
878
1094
  : undefined);
879
1095
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
880
1096
  let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
881
- task = resolveOutputReferences(task, ctx.outputs ?? {});
1097
+ if (ctx.outputs) task = resolveOutputReferences(task, ctx.outputs);
882
1098
  const taskForCompletionGuard = task;
883
1099
  if (step.effectiveAcceptance) {
884
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
1100
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { reportOptional: isAgentContractV1(step.agentContract) });
885
1101
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
886
1102
  }
887
1103
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -915,7 +1131,10 @@ async function runSingleStep(
915
1131
  ? [step.model]
916
1132
  : [undefined];
917
1133
  const attemptedModels: string[] = [];
1134
+ let capabilityAudit: import("../shared/capability-ceiling.ts").SubagentCapabilityAudit | undefined;
918
1135
  const modelAttempts: ModelAttempt[] = [];
1136
+ const writerProcesses: Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }> = [];
1137
+ let writerAttemptCount = 0;
919
1138
  const attemptNotes: string[] = [];
920
1139
  const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
921
1140
  let finalResult: RunPiStreamingResult | undefined;
@@ -924,6 +1143,7 @@ async function runSingleStep(
924
1143
  let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
925
1144
  let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
926
1145
  let toolBudgetBlocked = false;
1146
+ let actualLaunchContractDigest = step.launchContractDigest;
927
1147
 
928
1148
  for (let index = 0; index < candidates.length; index++) {
929
1149
  if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
@@ -937,7 +1157,16 @@ async function runSingleStep(
937
1157
  // Missing/stale structured-output files are handled after the child exits.
938
1158
  }
939
1159
  }
940
- const { args, env, tempDir } = buildPiArgs({
1160
+ const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
1161
+ const childWatchdog = watchdogConfig.ok
1162
+ ? resolveChildWatchdogConfig({
1163
+ config: watchdogConfig.config,
1164
+ agent: step.agent,
1165
+ runId: ctx.id,
1166
+ childIndex: ctx.flatIndex,
1167
+ })
1168
+ : undefined;
1169
+ const { args, env, tempDir, toolDiagnosticPath, capabilityAudit: attemptCapabilityAudit } = buildPiArgs({
941
1170
  parentSessionId: step.parentSessionId,
942
1171
  baseArgs: ["--offline", "--mode", "json", "-p"],
943
1172
  task,
@@ -954,6 +1183,7 @@ async function runSingleStep(
954
1183
  systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
955
1184
  systemPromptMode: step.systemPromptMode,
956
1185
  mcpDirectTools: step.mcpDirectTools,
1186
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
957
1187
  cwd: step.cwd ?? ctx.cwd,
958
1188
  promptFileStem: step.agent,
959
1189
  intercomSessionName: ctx.childIntercomTarget,
@@ -966,9 +1196,46 @@ async function runSingleStep(
966
1196
  parentRootRunId: ctx.nestedRoute?.rootRunId,
967
1197
  parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
968
1198
  steerInboxDir: ctx.steerInboxDir,
1199
+ steerCapabilityPath: ctx.steerCapabilityPath,
1200
+ steerAckDir: ctx.steerAckDir,
969
1201
  structuredOutput: effectiveStructuredOutput,
970
1202
  toolBudget: step.toolBudget,
1203
+ childWatchdog,
1204
+ waitToolEnabled: step.waitToolEnabled,
971
1205
  });
1206
+ if (step.definitionDigest) {
1207
+ const toolPlan = resolvePiLaunchToolPlan({
1208
+ tools: step.tools,
1209
+ extensions: step.extensions,
1210
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
1211
+ mcpDirectTools: step.mcpDirectTools,
1212
+ cwd: step.cwd ?? ctx.cwd,
1213
+ requireReadTool: Boolean(step.skills?.length),
1214
+ structuredOutput: Boolean(effectiveStructuredOutput),
1215
+ capabilityCeiling: step.capabilityCeiling ?? ctx.capabilityCeiling,
1216
+ inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
1217
+ });
1218
+ actualLaunchContractDigest = launchBindingDigest({
1219
+ definitionDigest: step.definitionDigest,
1220
+ task: step.launchBindingTask ?? task,
1221
+ ...(candidate ? { model: candidate } : {}),
1222
+ modelCandidates: candidates,
1223
+ ...(resolveEffectiveThinking(candidate, step.thinking) ? { thinking: resolveEffectiveThinking(candidate, step.thinking) } : {}),
1224
+ systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
1225
+ systemPromptMode: step.systemPromptMode,
1226
+ inheritProjectContext: step.inheritProjectContext,
1227
+ inheritSkills: step.inheritSkills,
1228
+ skills: step.skills,
1229
+ tools: toolPlan.effectiveToolAllowlist,
1230
+ extensions: toolPlan.extensionArgs,
1231
+ mcpDirectTools: toolPlan.effectiveMcpTools,
1232
+ ...(step.outputPath ? { outputPath: step.outputPath } : {}),
1233
+ ...(step.outputMode ? { outputMode: step.outputMode } : {}),
1234
+ ...(step.structuredOutputSchema ? { structuredOutputSchema: step.structuredOutputSchema } : {}),
1235
+ });
1236
+ }
1237
+ capabilityAudit = attemptCapabilityAudit;
1238
+ writerAttemptCount += 1;
972
1239
  const run = await runPiStreaming(
973
1240
  args,
974
1241
  step.cwd ?? ctx.cwd,
@@ -983,8 +1250,21 @@ async function runSingleStep(
983
1250
  transcriptWriter,
984
1251
  ctx.registerTimeout,
985
1252
  ctx.timeoutMessage,
1253
+ ctx.registerStop,
1254
+ ctx.stopMessage,
986
1255
  ctx.registerTurnBudgetAbort,
1256
+ ctx.onWriterProcess,
987
1257
  );
1258
+ if (run.processCloseObservedAt !== undefined) {
1259
+ writerProcesses.push({
1260
+ processInstanceId: run.processInstanceId,
1261
+ kind: "pi-writer",
1262
+ attempt: index,
1263
+ closeObservedAt: run.processCloseObservedAt,
1264
+ exitCode: run.exitCode,
1265
+ signal: run.processSignal ?? null,
1266
+ });
1267
+ }
988
1268
  if (run.turnBudget) turnBudget = run.turnBudget;
989
1269
  else if (ctx.turnBudget) {
990
1270
  const assistantMessages = run.messages.filter((message) => message.role === "assistant");
@@ -993,26 +1273,38 @@ async function runSingleStep(
993
1273
  if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
994
1274
  turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
995
1275
  } else if (turnCount >= ctx.turnBudget.maxTurns) {
996
- turnBudget = turnBudgetState(
1276
+ const decision = turnBudgetDecision(
997
1277
  ctx.turnBudget,
998
1278
  turnCount,
999
- shouldAbortForTurnBudget(ctx.turnBudget, turnCount, lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false),
1279
+ lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false,
1280
+ lastAssistantMessage ? assistantStartsToolCall(lastAssistantMessage) : false,
1000
1281
  );
1282
+ turnBudget = decision === "defer"
1283
+ ? turnBudgetDeferredState(ctx.turnBudget, turnCount)
1284
+ : turnBudgetState(ctx.turnBudget, turnCount, decision === "abort");
1001
1285
  }
1002
1286
  }
1287
+ const toolAvailabilityError = run.exitCode === 0 && !run.error
1288
+ ? readChildToolDiagnosticError(toolDiagnosticPath)
1289
+ : undefined;
1003
1290
  cleanupTempDir(tempDir);
1004
1291
 
1005
- const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
1292
+ const hiddenError = run.exitCode === 0 && !run.error && !toolAvailabilityError ? detectSubagentError(run.messages) : null;
1006
1293
  const missingStructuredOutput = effectiveStructuredOutput
1007
1294
  ? !fs.existsSync(effectiveStructuredOutput.outputPath)
1008
1295
  : false;
1009
- const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
1296
+ const emptyOutputError = run.exitCode === 0
1297
+ && !run.error
1298
+ && !toolAvailabilityError
1299
+ && !run.finalOutput.trim()
1300
+ && (!effectiveStructuredOutput || missingStructuredOutput)
1301
+ && (!hiddenError?.hasError || hasEmptyTerminalAssistantResponse(run.messages))
1010
1302
  ? "Subagent produced no output (possible model cold-start or empty response)."
1011
1303
  : undefined;
1012
1304
  let structuredOutput: unknown;
1013
1305
  let structuredError: string | undefined;
1014
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
1015
- const structured = readStructuredOutput({
1306
+ if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError) {
1307
+ const structured = await readStructuredOutput({
1016
1308
  schema: effectiveStructuredOutput.schema,
1017
1309
  schemaPath: effectiveStructuredOutput.schemaPath,
1018
1310
  outputPath: effectiveStructuredOutput.outputPath,
@@ -1020,7 +1312,8 @@ async function runSingleStep(
1020
1312
  if (structured.error) structuredError = structured.error;
1021
1313
  else structuredOutput = structured.value;
1022
1314
  }
1023
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
1315
+ const completionGuardEnabled = isAgentContractV1(step.agentContract) ? step.completionGuard === true : step.completionGuard !== false;
1316
+ const completionGuard = run.exitCode === 0 && !run.error && !toolAvailabilityError && !hiddenError?.hasError && !emptyOutputError && completionGuardEnabled
1024
1317
  ? evaluateCompletionMutationGuard({
1025
1318
  agent: step.agent,
1026
1319
  task: taskForCompletionGuard,
@@ -1030,27 +1323,33 @@ async function runSingleStep(
1030
1323
  })
1031
1324
  : undefined;
1032
1325
  const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1033
- const completionGuardError = completionGuardTriggered
1326
+ const fileMutationEffect = completionGuard
1327
+ ? {
1328
+ status: completionGuard.expectedMutation ? completionGuardTriggered ? "missing" as const : "observed" as const : "not-applicable" as const,
1329
+ expected: completionGuard.expectedMutation,
1330
+ attempted: completionGuard.attemptedMutation || run.observedMutationAttempt === true,
1331
+ ...(completionGuardTriggered ? { message: "Subagent completed without making edits for an implementation task." } : {}),
1332
+ }
1333
+ : undefined;
1334
+ const completionGuardError = completionGuardTriggered && !isAgentContractV1(step.agentContract)
1034
1335
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1035
1336
  : undefined;
1036
- const effectiveExitCode = completionGuardTriggered
1337
+ const effectiveExitCode = toolAvailabilityError || (completionGuardTriggered && !isAgentContractV1(step.agentContract)) || structuredError || emptyOutputError
1037
1338
  ? 1
1038
- : structuredError
1039
- ? 1
1040
- : hiddenError?.hasError
1339
+ : hiddenError?.hasError
1041
1340
  ? (hiddenError.exitCode ?? 1)
1042
- : emptyOutputError
1341
+ : run.error && run.exitCode === 0
1043
1342
  ? 1
1044
- : run.error && run.exitCode === 0
1045
- ? 1
1046
- : run.exitCode;
1047
- const error = completionGuardError
1343
+ : run.exitCode;
1344
+ const error = toolAvailabilityError
1345
+ ?? completionGuardError
1048
1346
  ?? structuredError
1347
+ ?? emptyOutputError
1049
1348
  ?? (hiddenError?.hasError
1050
1349
  ? hiddenError.details
1051
1350
  ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1052
1351
  : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1053
- : emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
1352
+ : run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined));
1054
1353
  const attempt: ModelAttempt = {
1055
1354
  model: candidate ?? run.model ?? step.model ?? "default",
1056
1355
  success: effectiveExitCode === 0 && !error,
@@ -1068,9 +1367,9 @@ async function runSingleStep(
1068
1367
  toolBudgetBlocked = Boolean(blockedMessage);
1069
1368
  toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1070
1369
  }
1071
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
1370
+ finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput, ...(step.agentContract ? { agentContract: step.agentContract } : {}), ...(fileMutationEffect ? { effects: { fileMutation: fileMutationEffect } } : {}) } as RunPiStreamingResult & { structuredOutput?: unknown; agentContract?: import("../../shared/types.ts").AgentContract; effects?: import("../../shared/types.ts").EffectsProjection };
1072
1371
  if (run.turnBudgetExceeded) break;
1073
- if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
1372
+ if (run.stopped || run.timedOut || ctx.timeoutSignal?.aborted || ctx.stopSignal?.aborted || ctx.skipAcceptance?.()) break;
1074
1373
  if (attempt.success || completionGuardTriggered) break;
1075
1374
  if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
1076
1375
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
@@ -1081,21 +1380,29 @@ async function runSingleStep(
1081
1380
  const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1082
1381
  ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
1083
1382
  : { fullOutput: outputForPersistence };
1084
- const output = resolvedOutput.fullOutput;
1383
+ const output = stripAcceptanceReport(resolvedOutput.fullOutput);
1085
1384
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
1086
1385
  let outputForSummary = output;
1087
- if (attemptNotes.length > 0) {
1088
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1089
- }
1090
- if (!finalResult?.timedOut && finalResult?.turnBudgetExceeded && turnBudget) {
1386
+ if (attemptNotes.length > 0) {
1387
+ outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1388
+ }
1389
+ if (finalResult?.stopped && !outputForSummary.trim()) {
1390
+ outputForSummary = ctx.stopMessage ?? "Subagent stopped by user.";
1391
+ } else if (!finalResult?.timedOut && !finalResult?.stopped && finalResult?.turnBudgetExceeded && turnBudget) {
1091
1392
  outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
1092
- } else if (!finalResult?.timedOut && turnBudget?.outcome === "wrap-up-requested") {
1393
+ } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "termination-deferred") {
1394
+ const note = turnBudgetDeferredNote(turnBudget, turnBudget.terminationDeferredAtTurn ?? turnBudget.turnCount);
1395
+ outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
1396
+ } else if (!finalResult?.timedOut && !finalResult?.stopped && turnBudget?.outcome === "wrap-up-requested") {
1093
1397
  const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
1094
1398
  outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
1095
1399
  }
1096
1400
  const outputForAcceptance = rawOutput;
1097
- const finalizedOutput = finalizeSingleOutput({
1098
- fullOutput: outputForSummary,
1401
+ const childWrittenOutput = step.outputPath
1402
+ ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
1403
+ : undefined;
1404
+ const finalizedOutput = finalizeSingleOutput({
1405
+ fullOutput: outputForSummary,
1099
1406
  outputPath: step.outputPath,
1100
1407
  outputMode: step.outputMode,
1101
1408
  exitCode: finalResult?.exitCode ?? 1,
@@ -1104,32 +1411,52 @@ async function runSingleStep(
1104
1411
  saveError: resolvedOutput.saveError,
1105
1412
  });
1106
1413
  outputForSummary = finalizedOutput.displayOutput;
1107
- const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
1108
- ? await evaluateAcceptance({
1109
- acceptance: step.effectiveAcceptance,
1110
- output: outputForAcceptance,
1111
- cwd: step.cwd ?? ctx.cwd,
1112
- signal: ctx.timeoutSignal,
1113
- abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
1114
- })
1414
+ const acceptance = step.effectiveAcceptance && !finalResult?.stopped && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.stopSignal?.aborted && !ctx.skipAcceptance?.()
1415
+ ? await evaluateAcceptance({
1416
+ acceptance: step.effectiveAcceptance,
1417
+ output: outputForAcceptance,
1418
+ fileOutput: childWrittenOutput !== undefined && step.outputPath
1419
+ ? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
1420
+ : undefined,
1421
+ cwd: step.cwd ?? ctx.cwd,
1422
+ signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
1423
+ abortMessage: ctx.stopSignal?.aborted ? ctx.stopMessage ?? "Subagent stopped by user." : ctx.timeoutMessage ?? "Subagent timed out.",
1424
+ reportOptional: isAgentContractV1(step.agentContract),
1425
+ })
1115
1426
  : undefined;
1116
- const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
1427
+ const stoppedAfterAcceptance = finalResult?.stopped === true || ctx.stopSignal?.aborted === true;
1428
+ const timedOutAfterAcceptance = !stoppedAfterAcceptance && (finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true);
1117
1429
  const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
1118
- const effectiveAcceptance = timedOutAfterAcceptance || turnBudgetExceeded ? undefined : acceptance;
1430
+ const effectiveAcceptance = step.effectiveAcceptance
1431
+ ? stoppedAfterAcceptance
1432
+ ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "stopped", message: "Acceptance was not evaluated because the subagent was stopped." })
1433
+ : timedOutAfterAcceptance
1434
+ ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "timeout", message: "Acceptance was not evaluated because the subagent timed out." })
1435
+ : turnBudgetExceeded
1436
+ ? buildSkippedAcceptanceLedger(step.effectiveAcceptance, { id: "turn-budget", message: "Acceptance was not evaluated because the subagent exceeded its turn budget." })
1437
+ : acceptance
1438
+ : undefined;
1119
1439
  const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1120
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
1121
- const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1122
- const effectiveFinalError = timedOutAfterAcceptance
1123
- ? ctx.timeoutMessage ?? "Subagent timed out."
1124
- : turnBudgetExceeded
1125
- ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1126
- : acceptanceCanFailRun
1127
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1128
- : finalResult?.error;
1440
+ const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !stoppedAfterAcceptance && !turnBudgetExceeded && !isAgentContractV1(step.agentContract);
1441
+ const effectiveFinalExitCode = timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1442
+ const effectiveFinalError = stoppedAfterAcceptance
1443
+ ? ctx.stopMessage ?? "Subagent stopped by user."
1444
+ : timedOutAfterAcceptance
1445
+ ? ctx.timeoutMessage ?? "Subagent timed out."
1446
+ : turnBudgetExceeded
1447
+ ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1448
+ : acceptanceCanFailRun
1449
+ ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1450
+ : finalResult?.error;
1129
1451
 
1130
1452
  if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1131
1453
  if (ctx.artifactConfig?.includeOutput !== false) {
1132
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1454
+ fs.writeFileSync(artifactPaths.outputPath, formatOutputArtifactContent({
1455
+ output,
1456
+ error: effectiveFinalError,
1457
+ transcriptPath: transcriptWriter ? artifactPaths.transcriptPath : undefined,
1458
+ metadataPath: ctx.artifactConfig?.includeMetadata === false ? undefined : artifactPaths.metadataPath,
1459
+ }), "utf-8");
1133
1460
  }
1134
1461
  if (ctx.artifactConfig?.includeMetadata !== false) {
1135
1462
  fs.writeFileSync(
@@ -1142,6 +1469,10 @@ async function runSingleStep(
1142
1469
  model: finalResult?.model,
1143
1470
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1144
1471
  modelAttempts,
1472
+ error: effectiveFinalError,
1473
+ acceptance: effectiveAcceptance,
1474
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1475
+ launchContractDigest: actualLaunchContractDigest,
1145
1476
  ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1146
1477
  transcriptError: transcriptWriter?.getError(),
1147
1478
  skills: step.skills,
@@ -1152,11 +1483,15 @@ async function runSingleStep(
1152
1483
  }
1153
1484
  }
1154
1485
 
1155
- return {
1486
+ const result = {
1156
1487
  agent: step.agent,
1488
+ context: step.context,
1489
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1490
+ launchContractDigest: actualLaunchContractDigest,
1157
1491
  output: outputForSummary,
1158
1492
  exitCode: effectiveFinalExitCode,
1159
1493
  error: effectiveFinalError,
1494
+ protocolError: finalResult?.protocolError,
1160
1495
  sessionFile: step.sessionFile,
1161
1496
  intercomTarget: ctx.childIntercomTarget,
1162
1497
  model: finalResult?.model,
@@ -1166,25 +1501,45 @@ async function runSingleStep(
1166
1501
  artifactPaths,
1167
1502
  transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1168
1503
  transcriptError: transcriptWriter?.getError(),
1169
- interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
1504
+ interrupted: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
1170
1505
  timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
1506
+ stopped: stoppedAfterAcceptance ? true : finalResult?.stopped,
1171
1507
  turnBudget,
1172
1508
  turnBudgetExceeded: turnBudgetExceeded || undefined,
1173
- wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
1509
+ wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined,
1174
1510
  toolBudget,
1175
1511
  toolBudgetBlocked: toolBudgetBlocked || undefined,
1176
1512
  completionGuardTriggered: completionGuardTriggeredFinal,
1177
- structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1178
- structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1179
- structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1513
+ ...((finalResult as (RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }) | undefined)?.effects ? { effects: (finalResult as RunPiStreamingResult & { effects?: import("../../shared/types.ts").EffectsProjection }).effects } : {}),
1514
+ structuredOutput: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1515
+ structuredOutputPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1516
+ structuredOutputSchemaPath: timedOutAfterAcceptance || stoppedAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1180
1517
  acceptance: effectiveAcceptance,
1518
+ watchdog: finalResult?.watchdog,
1519
+ ...(capabilityAudit ? { capabilityCeiling: capabilityAudit.ceiling, capabilityAudit } : {}),
1520
+ writerProcesses,
1521
+ writerAttemptCount,
1181
1522
  };
1523
+ return isAgentContractV1(step.agentContract) ? attachContractProjections(result as unknown as import("../../shared/types.ts").SingleResult) as unknown as typeof result : result;
1182
1524
  }
1183
1525
 
1184
1526
  type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1185
1527
  exitCode?: number | null;
1186
1528
  };
1187
1529
 
1530
+ function appendCapabilityCeilingAppliedEvent(eventsPath: string, runId: string, stepIndex: number, agent: string, result: StepResult): void {
1531
+ if (!result.capabilityCeiling) return;
1532
+ appendJsonl(eventsPath, JSON.stringify({
1533
+ type: "subagent.capability-ceiling.applied",
1534
+ ts: Date.now(),
1535
+ runId,
1536
+ stepIndex,
1537
+ agent,
1538
+ capabilityCeiling: result.capabilityCeiling,
1539
+ ...(result.capabilityAudit ? { capabilityAudit: result.capabilityAudit } : {}),
1540
+ }));
1541
+ }
1542
+
1188
1543
  type RunnerStatusPayload = Omit<AsyncStatus, "steps" | "parallelGroups" | "pid" | "cwd" | "currentStep" | "chainStepCount" | "lastUpdate"> & {
1189
1544
  pid: number;
1190
1545
  cwd: string;
@@ -1220,7 +1575,7 @@ function markParallelGroupSetupFailure(input: {
1220
1575
  input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
1221
1576
  input.statusPayload.steps[flatTaskIndex].durationMs = 0;
1222
1577
  input.statusPayload.steps[flatTaskIndex].exitCode = 1;
1223
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1578
+ input.results.push({ agent: input.group.parallel[taskIndex].agent, context: input.group.parallel[taskIndex].context, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1224
1579
  }
1225
1580
  input.statusPayload.currentStep = input.groupStartFlatIndex;
1226
1581
  input.statusPayload.lastUpdate = input.failedAt;
@@ -1285,19 +1640,15 @@ function prepareParallelTaskRun(
1285
1640
  };
1286
1641
  }
1287
1642
 
1288
- function appendParallelWorktreeSummary(
1289
- previousOutput: string,
1290
- worktreeSetup: WorktreeSetup | undefined,
1643
+ function captureParallelWorktreeDiffs(
1644
+ worktreeSetup: WorktreeSetup,
1291
1645
  asyncDir: string,
1292
1646
  stepIndex: number,
1293
1647
  group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
1294
- ): string {
1295
- if (!worktreeSetup) return previousOutput;
1648
+ ): { diffs: ReturnType<typeof diffWorktrees>; summary: string } {
1296
1649
  const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
1297
1650
  const diffs = diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
1298
- const diffSummary = formatWorktreeDiffSummary(diffs);
1299
- if (!diffSummary) return previousOutput;
1300
- return `${previousOutput}\n\n${diffSummary}`;
1651
+ return { diffs, summary: formatWorktreeDiffSummary(diffs) };
1301
1652
  }
1302
1653
 
1303
1654
  function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
@@ -1325,7 +1676,26 @@ function resolveAsyncStepTranscriptPath(input: {
1325
1676
 
1326
1677
  type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
1327
1678
 
1328
- async function runSubagent(config: SubagentRunConfig): Promise<void> {
1679
+ function combinedAbortSignal(signals: Array<AbortSignal | undefined>): AbortSignal | undefined {
1680
+ const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
1681
+ if (activeSignals.length === 0) return undefined;
1682
+ if (activeSignals.length === 1) return activeSignals[0];
1683
+ const controller = new AbortController();
1684
+ const abort = (): void => controller.abort();
1685
+ for (const signal of activeSignals) {
1686
+ if (signal.aborted) {
1687
+ abort();
1688
+ break;
1689
+ }
1690
+ signal.addEventListener("abort", abort, { once: true });
1691
+ }
1692
+ return controller.signal;
1693
+ }
1694
+
1695
+ async function runSubagent(
1696
+ config: SubagentRunConfig,
1697
+ onWriterProcess?: (writer: { state: "none" | "spawning" } | { state: "running"; pid: number }) => void,
1698
+ ): Promise<void> {
1329
1699
  const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1330
1700
  config;
1331
1701
  const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
@@ -1341,16 +1711,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1341
1711
  const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
1342
1712
  const activeChildInterrupts = new Map<number, () => void>();
1343
1713
  const activeChildTimeouts = new Map<number, () => void>();
1714
+ const activeChildStops = new Map<number, () => void>();
1344
1715
  const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
1345
1716
  const pendingStepSteers: SteerRequest[] = [];
1717
+ const steeringCapabilities = new Map<number, SteerCapability>();
1346
1718
  let interrupted = false;
1347
1719
  let currentActivityState: ActivityState | undefined;
1348
1720
  let activityTimer: NodeJS.Timeout | undefined;
1349
1721
  let timeoutTimer: NodeJS.Timeout | undefined;
1350
1722
  let timedOut = false;
1723
+ let stopped = false;
1351
1724
  let turnBudgetExceeded = false;
1352
1725
  const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
1726
+ const stopMessage = "Subagent stopped by user.";
1353
1727
  const timeoutAbortController = new AbortController();
1728
+ const stopAbortController = new AbortController();
1354
1729
  let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1355
1730
  let latestSessionFile: string | undefined;
1356
1731
 
@@ -1368,10 +1743,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1368
1743
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1369
1744
  initialStatusSteps.push({
1370
1745
  agent: task.agent,
1746
+ ...(task.context ? { context: task.context } : {}),
1371
1747
  phase: task.phase,
1372
1748
  label: task.label,
1373
1749
  outputName: task.outputName,
1374
1750
  structured: task.structured,
1751
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
1752
+ ...(task.launchContractDigest ? { launchContractDigest: task.launchContractDigest } : {}),
1753
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
1375
1754
  status: "pending",
1376
1755
  ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1377
1756
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
@@ -1389,10 +1768,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1389
1768
  parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1390
1769
  initialStatusSteps.push({
1391
1770
  agent: `expand:${step.parallel.agent}`,
1771
+ ...(step.parallel.context ? { context: step.parallel.context } : {}),
1392
1772
  phase: step.phase ?? step.parallel.phase,
1393
1773
  label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1394
1774
  outputName: step.collect.as,
1395
1775
  structured: Boolean(step.collect.outputSchema),
1776
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1777
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1396
1778
  status: "pending",
1397
1779
  ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1398
1780
  recentTools: [],
@@ -1404,10 +1786,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1404
1786
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1405
1787
  initialStatusSteps.push({
1406
1788
  agent: step.agent,
1789
+ ...(step.context ? { context: step.context } : {}),
1407
1790
  phase: step.phase,
1408
1791
  label: step.label,
1409
1792
  outputName: step.outputName,
1410
1793
  structured: step.structured,
1794
+ ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1795
+ ...(step.launchContractDigest ? { launchContractDigest: step.launchContractDigest } : {}),
1796
+ ...(step.capabilityCeiling ? { capabilityCeiling: step.capabilityCeiling } : {}),
1411
1797
  status: "pending",
1412
1798
  ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1413
1799
  ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
@@ -1425,12 +1811,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1425
1811
  const sessionEnabled = Boolean(config.sessionDir)
1426
1812
  || shareEnabled
1427
1813
  || flatSteps.some((step) => Boolean(step.sessionFile));
1814
+ if (config.runnerProcessInstanceId) {
1815
+ for (const step of initialStatusSteps) {
1816
+ step.processTerminal = { version: 1, state: "pending", runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId };
1817
+ }
1818
+ }
1428
1819
  const statusPayload: RunnerStatusPayload = {
1429
1820
  lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1430
1821
  runId: id,
1431
1822
  ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1432
1823
  mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
1824
+ ...(config.nestedRoute ? { isNested: true } : {}),
1433
1825
  state: "running",
1826
+ steering: createSteeringStatus(),
1434
1827
  lastActivityAt: overallStartTime,
1435
1828
  startedAt: overallStartTime,
1436
1829
  lastUpdate: overallStartTime,
@@ -1444,6 +1837,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1444
1837
  chainStepCount: steps.length,
1445
1838
  parallelGroups,
1446
1839
  workflowGraph: config.workflowGraph,
1840
+ ...(config.launchContractDigest ? { launchContractDigest: config.launchContractDigest } : {}),
1841
+ ...(config.capabilityCeiling ? { capabilityCeiling: config.capabilityCeiling } : {}),
1842
+ ...(config.runnerProcessInstanceId ? { processTerminal: { version: 1 as const, state: "pending" as const, runId: id, runnerProcessInstanceId: config.runnerProcessInstanceId } } : {}),
1447
1843
  steps: initialStatusSteps,
1448
1844
  artifactsDir,
1449
1845
  sessionDir: config.sessionDir,
@@ -1477,9 +1873,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1477
1873
  const refreshWorkflowGraph = (): void => {
1478
1874
  if (!config.workflowGraph) return;
1479
1875
  const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
1480
- const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
1876
+ const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "stopped" | "detached" => {
1481
1877
  if (status === "complete" || status === "completed") return "completed";
1482
- if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
1878
+ if (status === "running" || status === "failed" || status === "paused" || status === "stopped" || status === "pending") return status;
1483
1879
  return "pending";
1484
1880
  };
1485
1881
  const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
@@ -1496,10 +1892,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1496
1892
  if (node.children?.length) {
1497
1893
  if (node.children.every((child) => child.status === "completed")) node.status = "completed";
1498
1894
  else if (node.children.some((child) => child.status === "running")) node.status = "running";
1895
+ else if (node.children.some((child) => child.status === "stopped")) node.status = "stopped";
1499
1896
  else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
1500
1897
  else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
1501
1898
  }
1502
- if (node.error) node.status = "failed";
1899
+ if (node.error && node.status !== "stopped") node.status = "failed";
1503
1900
  };
1504
1901
  for (const node of graph.nodes) updateNode(node);
1505
1902
  statusPayload.workflowGraph = graph;
@@ -1525,6 +1922,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1525
1922
  activeChildTimeouts.set(flatIndex, interrupt);
1526
1923
  if (timedOut) interrupt();
1527
1924
  };
1925
+ const registerStepStop = (flatIndex: number, stop: (() => void) | undefined): void => {
1926
+ if (!stop) {
1927
+ activeChildStops.delete(flatIndex);
1928
+ return;
1929
+ }
1930
+ activeChildStops.set(flatIndex, stop);
1931
+ if (stopped) stop();
1932
+ };
1528
1933
  const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
1529
1934
  if (!abort) {
1530
1935
  activeChildTurnBudgetAborts.delete(flatIndex);
@@ -1538,6 +1943,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1538
1943
  const timeoutActiveChildren = (): void => {
1539
1944
  for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
1540
1945
  };
1946
+ const stopActiveChildren = (): void => {
1947
+ for (const stop of [...activeChildStops.values()]) stop();
1948
+ };
1541
1949
  const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
1542
1950
  for (const child of children ?? []) {
1543
1951
  yield child;
@@ -1576,6 +1984,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1576
1984
  }
1577
1985
  }
1578
1986
  };
1987
+ const stopNestedAsyncDescendants = (): void => {
1988
+ if (!config.nestedRoute) return;
1989
+ let registry: ReturnType<typeof projectNestedEvents>;
1990
+ try {
1991
+ registry = projectNestedEvents(config.nestedRoute);
1992
+ } catch (error) {
1993
+ appendJsonl(eventsPath, JSON.stringify({
1994
+ type: "subagent.nested.stop_failed",
1995
+ ts: Date.now(),
1996
+ runId: id,
1997
+ message: error instanceof Error ? error.message : String(error),
1998
+ }));
1999
+ return;
2000
+ }
2001
+ for (const run of nestedRuns(registry.children)) {
2002
+ if (run.state !== "running" && run.state !== "queued") continue;
2003
+ const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
2004
+ if (!nestedAsyncDir) continue;
2005
+ try {
2006
+ deliverStopRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-stop" });
2007
+ } catch (error) {
2008
+ appendJsonl(eventsPath, JSON.stringify({
2009
+ type: "subagent.nested.stop_failed",
2010
+ ts: Date.now(),
2011
+ runId: id,
2012
+ targetRunId: run.id,
2013
+ message: error instanceof Error ? error.message : String(error),
2014
+ }));
2015
+ }
2016
+ }
2017
+ };
1579
2018
  const timeoutNestedAsyncDescendants = (): void => {
1580
2019
  if (!config.nestedRoute) return;
1581
2020
  let registry: ReturnType<typeof projectNestedEvents>;
@@ -1607,19 +2046,29 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1607
2046
  }
1608
2047
  }
1609
2048
  };
1610
- const pausedStepResult = (agent: string): SingleStepResult => ({
2049
+ const pausedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1611
2050
  agent,
2051
+ context,
1612
2052
  output: "Paused after interrupt. Waiting for explicit next action.",
1613
2053
  exitCode: 0,
1614
2054
  interrupted: true,
1615
2055
  });
1616
- const timedOutStepResult = (agent: string): SingleStepResult => ({
2056
+ const timedOutStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
1617
2057
  agent,
2058
+ context,
1618
2059
  output: timeoutMessage ?? "Subagent timed out.",
1619
2060
  error: timeoutMessage ?? "Subagent timed out.",
1620
2061
  exitCode: 1,
1621
2062
  timedOut: true,
1622
2063
  });
2064
+ const stoppedStepResult = (agent: string, context?: "fresh" | "fork"): SingleStepResult => ({
2065
+ agent,
2066
+ context,
2067
+ output: stopMessage,
2068
+ error: stopMessage,
2069
+ exitCode: 1,
2070
+ stopped: true,
2071
+ });
1623
2072
  const consumePendingAppendRequests = (): void => {
1624
2073
  if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1625
2074
  const requests = consumeChainAppendRequests(asyncDir);
@@ -1659,7 +2108,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1659
2108
  }));
1660
2109
  }
1661
2110
  };
1662
- const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
2111
+ const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running" | "stopped", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1663
2112
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1664
2113
  if (!groupNode) return;
1665
2114
  groupNode.status = status;
@@ -1684,6 +2133,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1684
2133
  const activeLongRunningSteps = new Set<number>();
1685
2134
  const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1686
2135
  const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
2136
+ const supervisorAttentionSteps = new Map<number, ActivityState | undefined>();
1687
2137
  const mutatingFailureWindowMs = 5 * 60_000;
1688
2138
  const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
1689
2139
  if (!controlConfig.enabled) return;
@@ -1714,6 +2164,15 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1714
2164
  statusPayload.currentToolStartedAt = activeStep?.currentToolStartedAt;
1715
2165
  statusPayload.currentPath = activeStep?.currentPath;
1716
2166
  };
2167
+ const syncAggregateActivityState = (): void => {
2168
+ const nextRunState = statusPayload.steps.some((step) => step.activityState === "needs_attention")
2169
+ ? "needs_attention"
2170
+ : statusPayload.steps.some((step) => step.activityState === "active_long_running")
2171
+ ? "active_long_running"
2172
+ : undefined;
2173
+ currentActivityState = nextRunState;
2174
+ statusPayload.activityState = nextRunState;
2175
+ };
1717
2176
  const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
1718
2177
  if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
1719
2178
  const step = statusPayload.steps[flatIndex];
@@ -1750,48 +2209,125 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1750
2209
  appendControlEvent(event);
1751
2210
  return true;
1752
2211
  };
2212
+ const steeringMarkerPath = (requestId: string): string => path.join(asyncDir, "control", "steer-recovery", `${Buffer.from(requestId).toString("base64url")}.json`);
2213
+ const markSteeringAttention = (index: number): void => {
2214
+ const step = statusPayload.steps[index];
2215
+ if (step) step.activityState = "needs_attention";
2216
+ statusPayload.activityState = "needs_attention";
2217
+ };
2218
+ const emitSteeringEvent = (type: string, request: SteerRequest, index?: number, extra: Record<string, unknown> = {}): void => {
2219
+ appendJsonl(eventsPath, JSON.stringify({ type, ts: Date.now(), runId: id, requestId: request.id, ...(index !== undefined ? { index } : {}), ...extra }));
2220
+ };
2221
+ const emitSteeringNotice = (requestId: string, state: "failed" | "partial" | "recovered", message: string): void => {
2222
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.steering.notice", ts: Date.now(), runId: id, requestId, state, message, ...(config.sessionId ? { currentSessionId: config.sessionId } : {}) }));
2223
+ };
2224
+ const recordSteeringLifecycle = (request: SteerRequest, targets: Array<{ index: number; state: SteeringTargetState; reason?: string }>): void => {
2225
+ const lifecycle = steeringStatus(statusPayload);
2226
+ recordSteeringRequest(lifecycle, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets });
2227
+ for (const target of targets) {
2228
+ const step = statusPayload.steps[target.index];
2229
+ if (!step) continue;
2230
+ step.steering ??= createSteeringStatus();
2231
+ recordSteeringRequest(step.steering, { id: request.id, requestedAt: request.ts, source: request.source, message: request.message, targets: [target] });
2232
+ }
2233
+ };
2234
+ const updateSteeringLifecycleTarget = (
2235
+ requestId: string,
2236
+ index: number,
2237
+ state: SteeringTargetState,
2238
+ now: number,
2239
+ fields: Pick<SteeringTargetStatus, "reason" | "replacementRunId"> = {},
2240
+ ): SteeringTargetStatus | undefined => {
2241
+ const updated = updateSteeringTarget(steeringStatus(statusPayload), requestId, index, state, now, fields);
2242
+ const step = statusPayload.steps[index];
2243
+ if (step?.steering) updateSteeringTarget(step.steering, requestId, index, state, now, fields);
2244
+ return updated;
2245
+ };
2246
+ const emitTerminalSteeringNotice = (requestId: string, failureMessage: string): void => {
2247
+ const state = terminalSteeringNoticeState(steeringStatus(statusPayload), requestId);
2248
+ if (state === "partial") emitSteeringNotice(requestId, "partial", `Steering partially delivered for run ${id}.`);
2249
+ else if (state === "failed") emitSteeringNotice(requestId, "failed", failureMessage);
2250
+ };
1753
2251
  const deliverSteerRequest = (request: SteerRequest): void => {
1754
- if (statusPayload.state !== "running") return;
2252
+ if (statusPayload.state !== "running") {
2253
+ const reason = `run became ${statusPayload.state} before steering request was consumed`;
2254
+ const indexes = request.targetIndex !== undefined
2255
+ ? [request.targetIndex]
2256
+ : request.targetIndexes?.length
2257
+ ? request.targetIndexes
2258
+ : statusPayload.steps.map((_, index) => index);
2259
+ const targets = indexes.map((index) => ({ index, state: "failed" as const, reason }));
2260
+ recordSteeringLifecycle(request, targets);
2261
+ emitSteeringEvent("subagent.steer.requested", request, undefined, { targets });
2262
+ for (const target of targets) emitSteeringEvent("subagent.steer.failed", request, target.index, { reason });
2263
+ emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: ${reason}.`);
2264
+ statusPayload.lastUpdate = Date.now();
2265
+ writeStatusPayload();
2266
+ return;
2267
+ }
1755
2268
  const runningIndexes = statusPayload.steps
1756
2269
  .map((step, index) => ({ step, index }))
1757
2270
  .filter(({ step }) => step.status === "running")
1758
2271
  .map(({ index }) => index);
1759
- const targets = request.targetIndex !== undefined ? [request.targetIndex] : runningIndexes;
2272
+ const targets = request.targetIndex !== undefined
2273
+ ? [request.targetIndex]
2274
+ : request.targetIndexes?.length
2275
+ ? request.targetIndexes
2276
+ : runningIndexes.length > 0
2277
+ ? runningIndexes
2278
+ : statusPayload.mode === "single" && statusPayload.steps[0]?.status === "pending"
2279
+ ? [0]
2280
+ : [];
1760
2281
  const now = Date.now();
1761
- const accepted: number[] = [];
1762
- const rejected: Array<{ index: number; reason: string }> = [];
1763
- for (const index of targets) {
2282
+ const targetStates = targets.map((index) => {
1764
2283
  const step = statusPayload.steps[index];
1765
- if (!step) {
1766
- rejected.push({ index, reason: "child index out of range" });
1767
- continue;
1768
- }
1769
- if (step.status !== "running") {
1770
- rejected.push({ index, reason: `child is ${step.status}` });
1771
- continue;
2284
+ if (!step) return { index, state: "failed" as const, reason: "child index out of range" };
2285
+ if (step.status === "pending") return { index, state: "scheduled" as const };
2286
+ if (step.status !== "running") return { index, state: "failed" as const, reason: `child is ${step.status}` };
2287
+ if (steeringCapabilities.get(index)?.supported === false) return { index, state: "failed" as const, reason: "child Pi session does not support steering" };
2288
+ return { index, state: "routed" as const };
2289
+ });
2290
+ recordSteeringLifecycle(request, targetStates);
2291
+ emitSteeringEvent("subagent.steer.requested", request, undefined, { targets: targetStates });
2292
+ for (const target of targetStates) {
2293
+ if (target.state === "routed") {
2294
+ try {
2295
+ enqueueStepSteer(asyncDir, target.index, request);
2296
+ updateSteeringLifecycleTarget(request.id, target.index, "routed", now);
2297
+ emitSteeringEvent("subagent.steer.routed", request, target.index);
2298
+ } catch (error) {
2299
+ markSteeringAttention(target.index);
2300
+ updateSteeringLifecycleTarget(request.id, target.index, "failed", now, { reason: error instanceof Error ? error.message : String(error) });
2301
+ emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: error instanceof Error ? error.message : String(error) });
2302
+ }
2303
+ } else if (target.state === "failed") {
2304
+ markSteeringAttention(target.index);
2305
+ emitSteeringEvent("subagent.steer.failed", request, target.index, { reason: target.reason });
2306
+ } else {
2307
+ emitSteeringEvent("subagent.steer.scheduled", request, target.index);
1772
2308
  }
1773
- enqueueStepSteer(asyncDir, index, request);
1774
- step.steerCount = (step.steerCount ?? 0) + 1;
1775
- step.lastSteerAt = now;
1776
- accepted.push(index);
1777
2309
  }
1778
- if (accepted.length > 0) {
1779
- statusPayload.steerCount = (statusPayload.steerCount ?? 0) + accepted.length;
1780
- statusPayload.lastSteerAt = now;
1781
- statusPayload.lastUpdate = now;
1782
- writeStatusPayload();
2310
+ emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: no requested child remained steerable.`);
2311
+ statusPayload.lastUpdate = now;
2312
+ writeStatusPayload();
2313
+ };
2314
+ const consumeSteerAck = (ack: SteerAck): void => {
2315
+ const lifecycle = steeringStatus(statusPayload);
2316
+ const request = lifecycle.recent.find((candidate) => candidate.id === ack.requestId);
2317
+ if (!request || !request.targets.some((target) => target.index === ack.index)) return;
2318
+ const late = fs.existsSync(steeringMarkerPath(ack.requestId));
2319
+ const now = Date.now();
2320
+ if (ack.state === "delivered") {
2321
+ updateSteeringLifecycleTarget(ack.requestId, ack.index, late ? "late" : "delivered", now, { reason: late ? "acknowledged after recovery commit" : undefined });
2322
+ emitSteeringEvent("subagent.steer.delivered", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { late, message: ack.message });
2323
+ } else {
2324
+ markSteeringAttention(ack.index);
2325
+ updateSteeringLifecycleTarget(ack.requestId, ack.index, "failed", now, { reason: ack.message });
2326
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: ack.requestId, ts: now, message: ack.message }, ack.index, { reason: ack.message });
1783
2327
  }
1784
- appendJsonl(eventsPath, JSON.stringify({
1785
- type: "subagent.steer.requested",
1786
- ts: now,
1787
- runId: id,
1788
- requestId: request.id,
1789
- message: request.message,
1790
- ...(request.source ? { source: request.source } : {}),
1791
- ...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
1792
- acceptedIndexes: accepted,
1793
- ...(rejected.length ? { rejected } : {}),
1794
- }));
2328
+ emitTerminalSteeringNotice(ack.requestId, `Steering failed for run ${id}: ${ack.message}`);
2329
+ statusPayload.lastUpdate = now;
2330
+ writeStatusPayload();
1795
2331
  };
1796
2332
  const flushPendingStepSteers = (flatIndex: number): void => {
1797
2333
  const remaining: SteerRequest[] = [];
@@ -1810,25 +2346,51 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1810
2346
  statusPayload.lastUpdate = now;
1811
2347
  writeStatusPayload();
1812
2348
  };
1813
- const updateStepTurnBudget = (flatIndex: number, turnCount: number, now: number, terminalAssistantStop: boolean): void => {
2349
+ const updateStepTurnBudget = (
2350
+ flatIndex: number,
2351
+ turnCount: number,
2352
+ now: number,
2353
+ terminalAssistantStop: boolean,
2354
+ toolWorkActiveOrStarting: boolean,
2355
+ ): void => {
1814
2356
  const budget = config.turnBudget;
1815
2357
  const step = statusPayload.steps[flatIndex];
1816
- if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
2358
+ if (!budget || !step || timedOut || stopped || turnBudgetExceeded || step.turnBudgetExceeded) return;
1817
2359
  if (turnCount < budget.maxTurns) {
1818
2360
  const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
1819
2361
  step.turnBudget = state;
1820
2362
  statusPayload.turnBudget = state;
1821
2363
  return;
1822
2364
  }
1823
- const state = turnBudgetState(budget, turnCount, false);
1824
- step.turnBudget = state;
1825
- statusPayload.turnBudget = state;
1826
2365
  if (!step.wrapUpRequested) {
1827
2366
  step.wrapUpRequested = true;
1828
2367
  statusPayload.wrapUpRequested = true;
1829
2368
  appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
1830
2369
  }
1831
- if (!shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) return;
2370
+ const decision = turnBudgetDecision(budget, turnCount, terminalAssistantStop, toolWorkActiveOrStarting);
2371
+ if (decision === "defer") {
2372
+ const firstDeferredTurn = step.turnBudget?.terminationDeferredAtTurn;
2373
+ const deferredState = turnBudgetDeferredState(budget, turnCount, firstDeferredTurn);
2374
+ step.turnBudget = deferredState;
2375
+ statusPayload.turnBudget = deferredState;
2376
+ if (firstDeferredTurn === undefined) {
2377
+ appendJsonl(eventsPath, JSON.stringify({
2378
+ type: "subagent.step.turn_budget_deferred",
2379
+ ts: now,
2380
+ runId: id,
2381
+ stepIndex: flatIndex,
2382
+ agent: step.agent,
2383
+ turnCount,
2384
+ maxTurns: budget.maxTurns,
2385
+ graceTurns: budget.graceTurns,
2386
+ }));
2387
+ }
2388
+ return;
2389
+ }
2390
+ const state = turnBudgetState(budget, turnCount, false);
2391
+ step.turnBudget = state;
2392
+ statusPayload.turnBudget = state;
2393
+ if (decision !== "abort") return;
1832
2394
  const exceededState = turnBudgetState(budget, turnCount, true);
1833
2395
  const message = turnBudgetExceededMessage(budget, turnCount);
1834
2396
  step.turnBudget = exceededState;
@@ -1849,6 +2411,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1849
2411
  if (!step) return;
1850
2412
  const now = Date.now();
1851
2413
  statusPayload.currentStep = flatIndex;
2414
+ if (isChildWatchdogStatusEvent(event)) {
2415
+ const next = acceptChildWatchdogEvent({
2416
+ current: step.watchdog,
2417
+ event,
2418
+ runId: id,
2419
+ agent: step.agent,
2420
+ childIndex: flatIndex,
2421
+ });
2422
+ if (!next) return;
2423
+ step.watchdog = next;
2424
+ step.lastActivityAt = now;
2425
+ statusPayload.lastActivityAt = now;
2426
+ statusPayload.lastUpdate = now;
2427
+ writeStatusPayload();
2428
+ return;
2429
+ }
1852
2430
  if (event.type === "tool_execution_start" && event.toolName) {
1853
2431
  const mutates = isMutatingTool(event.toolName, event.args);
1854
2432
  const currentPath = resolveCurrentPath(event.toolName, event.args);
@@ -1865,15 +2443,45 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1865
2443
  pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
1866
2444
  statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
1867
2445
  syncTopLevelCurrentTool();
2446
+ if (controlConfig.enabled && isBlockingSupervisorTool(event.toolName, event.args) && step.activityState !== "needs_attention") {
2447
+ const previous = step.activityState;
2448
+ step.activityState = "needs_attention";
2449
+ supervisorAttentionSteps.set(flatIndex, previous);
2450
+ currentActivityState = "needs_attention";
2451
+ statusPayload.activityState = "needs_attention";
2452
+ appendControlEvent(buildControlEvent({
2453
+ type: "needs_attention",
2454
+ from: previous,
2455
+ to: "needs_attention",
2456
+ runId: id,
2457
+ agent: step.agent,
2458
+ index: flatIndex,
2459
+ ts: now,
2460
+ message: `${step.agent} is waiting for a supervisor reply`,
2461
+ reason: "supervisor_request",
2462
+ turns: step.turnCount,
2463
+ tokens: step.tokens?.total,
2464
+ toolCount: step.toolCount,
2465
+ currentTool: step.currentTool,
2466
+ currentToolDurationMs: 0,
2467
+ currentPath: step.currentPath,
2468
+ }));
2469
+ }
1868
2470
  } else if (event.type === "tool_execution_end") {
1869
2471
  if (step.currentTool) {
1870
2472
  step.recentTools ??= [];
1871
2473
  step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
1872
2474
  }
2475
+ const supervisorPreviousActivity = supervisorAttentionSteps.get(flatIndex);
2476
+ const clearedSupervisorAttention = supervisorAttentionSteps.delete(flatIndex);
1873
2477
  step.currentTool = undefined;
1874
2478
  step.currentToolArgs = undefined;
1875
2479
  step.currentToolStartedAt = undefined;
1876
2480
  step.currentPath = undefined;
2481
+ if (clearedSupervisorAttention && step.activityState === "needs_attention") {
2482
+ step.activityState = supervisorPreviousActivity;
2483
+ syncAggregateActivityState();
2484
+ }
1877
2485
  syncTopLevelCurrentTool();
1878
2486
  } else if (event.type === "tool_result_end" && event.message) {
1879
2487
  const toolSnapshot = pendingToolResults[flatIndex];
@@ -1938,7 +2546,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1938
2546
  statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
1939
2547
  }
1940
2548
  statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
1941
- updateStepTurnBudget(flatIndex, step.turnCount, now, isTerminalAssistantStop(event.message));
2549
+ updateStepTurnBudget(
2550
+ flatIndex,
2551
+ step.turnCount,
2552
+ now,
2553
+ isTerminalAssistantStop(event.message),
2554
+ assistantStartsToolCall(event.message) || Boolean(step.currentTool),
2555
+ );
1942
2556
  }
1943
2557
  syncTopLevelCurrentTool();
1944
2558
  step.lastActivityAt = now;
@@ -1964,6 +2578,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1964
2578
  config: controlConfig,
1965
2579
  startedAt: step.startedAt ?? overallStartTime,
1966
2580
  lastActivityAt,
2581
+ currentTool: step.currentTool,
1967
2582
  now,
1968
2583
  });
1969
2584
  if (idleState === "needs_attention") {
@@ -2039,8 +2654,40 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2039
2654
  interruptNestedAsyncDescendants();
2040
2655
  interruptActiveChildren();
2041
2656
  };
2657
+ const stopRunner = () => {
2658
+ if (stopped || timedOut || interrupted || statusPayload.state !== "running") return;
2659
+ stopped = true;
2660
+ const now = Date.now();
2661
+ statusPayload.state = "stopped";
2662
+ statusPayload.stopped = true;
2663
+ statusPayload.error = stopMessage;
2664
+ currentActivityState = undefined;
2665
+ statusPayload.activityState = undefined;
2666
+ statusPayload.lastUpdate = now;
2667
+ for (const step of statusPayload.steps) {
2668
+ if (step.status !== "running" && step.status !== "pending") continue;
2669
+ step.status = "stopped";
2670
+ step.error = stopMessage;
2671
+ step.exitCode = 1;
2672
+ step.stopped = true;
2673
+ step.activityState = undefined;
2674
+ step.endedAt = now;
2675
+ step.durationMs = step.startedAt ? now - step.startedAt : 0;
2676
+ step.lastActivityAt = now;
2677
+ }
2678
+ writeStatusPayload();
2679
+ appendJsonl(eventsPath, JSON.stringify({
2680
+ type: "subagent.run.stopped",
2681
+ ts: now,
2682
+ runId: id,
2683
+ message: stopMessage,
2684
+ }));
2685
+ stopAbortController.abort();
2686
+ stopNestedAsyncDescendants();
2687
+ stopActiveChildren();
2688
+ };
2042
2689
  const timeoutRunner = () => {
2043
- if (timedOut || interrupted || statusPayload.state !== "running") return;
2690
+ if (timedOut || stopped || interrupted || statusPayload.state !== "running") return;
2044
2691
  timedOut = true;
2045
2692
  const now = Date.now();
2046
2693
  const message = timeoutMessage ?? "Subagent timed out.";
@@ -2081,12 +2728,36 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2081
2728
  const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
2082
2729
  onInterrupt: interruptRunner,
2083
2730
  onTimeout: timeoutRunner,
2731
+ onStop: stopRunner,
2084
2732
  onSteer: (request) => {
2085
2733
  const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
2086
- if (targetStep?.status === "pending") pendingStepSteers.push(request);
2087
- else if (request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) deliverSteerRequest(request);
2088
- else pendingStepSteers.push(request);
2734
+ if (targetStep?.status === "pending") {
2735
+ deliverSteerRequest(request);
2736
+ pendingStepSteers.push(request);
2737
+ } else if (request.targetIndexes !== undefined || request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) {
2738
+ deliverSteerRequest(request);
2739
+ } else {
2740
+ deliverSteerRequest(request);
2741
+ pendingStepSteers.push(request);
2742
+ }
2743
+ },
2744
+ onSteerCapability: (capability) => {
2745
+ steeringCapabilities.set(capability.index, capability);
2746
+ if (!capability.supported) {
2747
+ const now = Date.now();
2748
+ const lifecycle = steeringStatus(statusPayload);
2749
+ for (const request of lifecycle.recent) {
2750
+ if (!request.targets.some((target) => target.index === capability.index && (target.state === "routed" || target.state === "scheduled"))) continue;
2751
+ markSteeringAttention(capability.index);
2752
+ updateSteeringLifecycleTarget(request.id, capability.index, "failed", now, { reason: "child Pi session does not support steering" });
2753
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child Pi session does not support steering" }, capability.index, { reason: "child Pi session does not support steering" });
2754
+ emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child ${capability.index} does not support steering.`);
2755
+ }
2756
+ statusPayload.lastUpdate = now;
2757
+ writeStatusPayload();
2758
+ }
2089
2759
  },
2760
+ onSteerAck: consumeSteerAck,
2090
2761
  });
2091
2762
  if (config.deadlineAt !== undefined) {
2092
2763
  const remainingMs = Math.max(0, config.deadlineAt - Date.now());
@@ -2110,7 +2781,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2110
2781
  let stepCursor = 0;
2111
2782
 
2112
2783
  while (true) {
2113
- if (interrupted || timedOut || turnBudgetExceeded) break;
2784
+ if (interrupted || timedOut || stopped || turnBudgetExceeded) break;
2114
2785
  consumePendingAppendRequests();
2115
2786
  if (stepCursor >= steps.length) break;
2116
2787
  const stepIndex = stepCursor++;
@@ -2121,7 +2792,10 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2121
2792
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
2122
2793
  try {
2123
2794
  materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
2124
- if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2795
+ if (materialized.parallel.length > 1 && step.parallel.outputPath && !step.parallel.namespaceOutputPath) {
2796
+ throw new DynamicFanoutError(`Dynamic chain step ${stepIndex + 1} materialized ${materialized.parallel.length} items that resolve output to the same path: ${step.parallel.outputPath}. Remove the explicit output path or use an inherited relative agent output so each item can be isolated.`);
2797
+ }
2798
+ if (materialized.collectedOnEmpty) await validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2125
2799
  } catch (error) {
2126
2800
  const now = Date.now();
2127
2801
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
@@ -2140,10 +2814,21 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2140
2814
  statusPayload.lastUpdate = now;
2141
2815
  markDynamicGraphGroup(stepIndex, "failed", message);
2142
2816
  writeStatusPayload();
2143
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
2817
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1 });
2144
2818
  break;
2145
2819
  }
2146
2820
 
2821
+ const effectiveDynamicGroupAcceptance = resolveEffectiveAcceptance({
2822
+ explicit: step.acceptanceInput,
2823
+ agentName: step.parallel.agent,
2824
+ acceptanceRole: step.acceptanceRole,
2825
+ task: materialized.parallel.map((task) => task.task ?? step.parallel.task).join("\n") || step.parallel.task,
2826
+ mode: config.mode,
2827
+ async: true,
2828
+ dynamicGroup: true,
2829
+ agentContract: step.agentContract,
2830
+ });
2831
+
2147
2832
  if (materialized.parallel.length === 0) {
2148
2833
  const now = Date.now();
2149
2834
  const collection = materialized.collectedOnEmpty ?? [];
@@ -2162,37 +2847,41 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2162
2847
  placeholder.durationMs = 0;
2163
2848
  }
2164
2849
  previousOutput = "Dynamic fanout produced 0 results.";
2165
- const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
2850
+ const groupAcceptance = effectiveDynamicGroupAcceptance.explicit && !timedOut && !stopped
2166
2851
  ? await evaluateAcceptance({
2167
- acceptance: step.effectiveAcceptance,
2852
+ acceptance: effectiveDynamicGroupAcceptance,
2168
2853
  output: "",
2169
2854
  report: aggregateAcceptanceReport({
2170
2855
  results: [],
2171
2856
  notes: "Dynamic fanout produced 0 results.",
2172
2857
  }),
2173
2858
  cwd,
2174
- signal: timeoutAbortController.signal,
2175
- abortMessage: timeoutMessage ?? "Subagent timed out.",
2859
+ signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
2860
+ abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
2861
+ reportOptional: isAgentContractV1(step.agentContract),
2176
2862
  })
2177
2863
  : undefined;
2178
- const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2179
- const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2864
+ const groupStopped = stopped || stopAbortController.signal.aborted;
2865
+ const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
2866
+ const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
2180
2867
  if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
2181
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2182
- if (groupTimedOut || groupAcceptanceFailure) {
2183
- const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
2184
- statusPayload.state = "failed";
2868
+ const groupAcceptanceFailure = effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2869
+ if (groupTimedOut || groupStopped || groupAcceptanceFailure) {
2870
+ const errorMessage = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
2871
+ statusPayload.state = groupStopped ? "stopped" : "failed";
2185
2872
  statusPayload.error = errorMessage;
2873
+ statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
2186
2874
  if (placeholder) {
2187
- placeholder.status = "failed";
2875
+ placeholder.status = groupStopped ? "stopped" : "failed";
2188
2876
  placeholder.error = errorMessage;
2189
2877
  placeholder.exitCode = 1;
2190
2878
  placeholder.timedOut = groupTimedOut ? true : undefined;
2879
+ placeholder.stopped = groupStopped ? true : undefined;
2191
2880
  }
2192
- markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
2881
+ markDynamicGraphGroup(stepIndex, groupStopped ? "stopped" : "failed", errorMessage, effectiveGroupAcceptance);
2193
2882
  statusPayload.lastUpdate = Date.now();
2194
2883
  writeStatusPayload();
2195
- results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
2884
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, stopped: groupStopped ? true : undefined, acceptance: effectiveGroupAcceptance });
2196
2885
  break;
2197
2886
  }
2198
2887
  flatIndex++;
@@ -2206,9 +2895,26 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2206
2895
  const thinkingOverride = step.thinkingOverrides?.[itemIndex];
2207
2896
  const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
2208
2897
  const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
2898
+ const outputPath = step.parallel.namespaceOutputPath && step.parallel.outputPath
2899
+ ? path.join(path.dirname(step.parallel.outputPath), `dynamic-${stepIndex}`, `${itemIndex}-${step.parallel.agent}`, path.basename(step.parallel.outputPath))
2900
+ : step.parallel.outputPath;
2901
+ const taskText = task.task ?? step.parallel.task;
2902
+ const materializedTask = step.parallel.namespaceOutputPath ? injectSingleOutputInstruction(taskText, outputPath, step.parallel) : taskText;
2209
2903
  return {
2210
2904
  ...step.parallel,
2211
- task: task.task ?? step.parallel.task,
2905
+ task: materializedTask,
2906
+ effectiveAcceptance: resolveEffectiveAcceptance({
2907
+ explicit: step.parallel.acceptanceInput,
2908
+ agentName: step.parallel.agent,
2909
+ acceptanceRole: step.parallel.acceptanceRole,
2910
+ task: materializedTask,
2911
+ mode: config.mode,
2912
+ async: true,
2913
+ dynamic: step.parallel.acceptanceInput === undefined,
2914
+ agentContract: step.parallel.agentContract ?? step.agentContract,
2915
+ }),
2916
+ systemPrompt: step.parallel.namespaceOutputPath ? injectOutputPathSystemPrompt(step.parallel.systemPrompt ?? "", outputPath, step.parallel) : step.parallel.systemPrompt,
2917
+ outputPath,
2212
2918
  label: task.label ?? step.parallel.label,
2213
2919
  ...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
2214
2920
  ...(thinkingOverride ? {
@@ -2225,10 +2931,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2225
2931
  const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2226
2932
  return {
2227
2933
  agent: task.agent,
2934
+ ...(task.context ? { context: task.context } : {}),
2228
2935
  phase: task.phase ?? step.phase,
2229
2936
  label: task.label,
2230
2937
  outputName: undefined,
2231
2938
  structured: Boolean(task.structuredOutputSchema),
2939
+ ...(task.agentContract ? { agentContract: task.agentContract } : {}),
2940
+ ...(task.capabilityCeiling ? { capabilityCeiling: task.capabilityCeiling } : {}),
2232
2941
  status: "pending",
2233
2942
  ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2234
2943
  ...(transcriptPath ? { transcriptPath } : {}),
@@ -2288,8 +2997,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2288
2997
  let aborted = false;
2289
2998
  const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
2290
2999
  const fi = groupStartFlatIndex + taskIdx;
2291
- if (timedOut) return timedOutStepResult(task.agent);
2292
- if (interrupted) return pausedStepResult(task.agent);
3000
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
3001
+ if (stopped) return stoppedStepResult(task.agent, task.context);
3002
+ if (interrupted) return pausedStepResult(task.agent, task.context);
2293
3003
  if (aborted && failFast) {
2294
3004
  const skippedAt = Date.now();
2295
3005
  statusPayload.steps[fi].status = "failed";
@@ -2300,7 +3010,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2300
3010
  statusPayload.steps[fi].exitCode = -1;
2301
3011
  statusPayload.lastUpdate = skippedAt;
2302
3012
  writeStatusPayload();
2303
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3013
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2304
3014
  }
2305
3015
  const taskStartTime = Date.now();
2306
3016
  statusPayload.currentStep = fi;
@@ -2324,28 +3034,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2324
3034
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
2325
3035
  outputFile: path.join(asyncDir, `output-${fi}.log`),
2326
3036
  steerInboxDir: stepSteerInboxDir(asyncDir, fi),
3037
+ steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
3038
+ steerAckDir: steerAcksDir(asyncDir, fi),
2327
3039
  piPackageRoot: config.piPackageRoot,
2328
3040
  piArgv1: config.piArgv1,
2329
3041
  childIntercomTarget: config.childIntercomTargets?.[fi],
2330
3042
  orchestratorIntercomTarget: config.controlIntercomTarget,
2331
3043
  nestedRoute: config.nestedRoute,
3044
+ capabilityCeiling: config.capabilityCeiling,
2332
3045
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2333
3046
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3047
+ registerStop: (stop) => registerStepStop(fi, stop),
2334
3048
  registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2335
3049
  timeoutSignal: timeoutAbortController.signal,
3050
+ stopSignal: stopAbortController.signal,
2336
3051
  timeoutMessage,
3052
+ stopMessage,
2337
3053
  turnBudget: config.turnBudget,
2338
3054
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2339
3055
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2340
- skipAcceptance: () => timedOut,
3056
+ onWriterProcess,
3057
+ skipAcceptance: () => timedOut || stopped,
2341
3058
  });
2342
3059
  const taskEndTime = Date.now();
2343
3060
  const childInterrupted = singleResult.interrupted === true;
2344
- statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3061
+ const childStopped = singleResult.stopped === true;
3062
+ statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2345
3063
  statusPayload.steps[fi].endedAt = taskEndTime;
2346
3064
  statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
2347
- statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3065
+ statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2348
3066
  statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
3067
+ statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
2349
3068
  statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2350
3069
  statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2351
3070
  statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
@@ -2361,35 +3080,51 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2361
3080
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2362
3081
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2363
3082
  statusPayload.steps[fi].totalCost = singleResult.totalCost;
2364
- statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3083
+ statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2365
3084
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2366
3085
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3086
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
3087
+ statusPayload.steps[fi].launchContractDigest = singleResult.launchContractDigest;
3088
+ statusPayload.steps[fi].effects = singleResult.effects;
3089
+ statusPayload.steps[fi].execution = singleResult.execution;
3090
+ statusPayload.steps[fi].review = singleResult.review;
2367
3091
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2368
3092
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2369
3093
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2370
3094
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
3095
+ statusPayload.steps[fi].watchdog = singleResult.watchdog;
3096
+ statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3097
+ statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3098
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3099
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
2371
3100
  statusPayload.lastUpdate = taskEndTime;
2372
3101
  writeStatusPayload();
3102
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
2373
3103
  appendJsonl(eventsPath, JSON.stringify({
2374
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
3104
+ type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2375
3105
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2376
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
3106
+ exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
2377
3107
  }));
2378
3108
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
2379
- return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
3109
+ return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2380
3110
  }, globalSemaphore);
2381
3111
 
2382
3112
  flatIndex += dynamicSteps.length;
2383
3113
  for (const pr of parallelResults) {
2384
3114
  results.push({
2385
3115
  agent: pr.agent,
3116
+ context: pr.context,
3117
+ agentContract: pr.agentContract,
3118
+ launchContractDigest: pr.launchContractDigest,
2386
3119
  output: pr.output,
2387
3120
  error: pr.error,
2388
- success: pr.interrupted !== true && pr.exitCode === 0,
3121
+ protocolError: pr.protocolError,
3122
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
2389
3123
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2390
3124
  skipped: pr.skipped,
2391
3125
  interrupted: pr.interrupted,
2392
3126
  timedOut: pr.timedOut,
3127
+ stopped: pr.stopped,
2393
3128
  turnBudget: pr.turnBudget,
2394
3129
  turnBudgetExceeded: pr.turnBudgetExceeded,
2395
3130
  wrapUpRequested: pr.wrapUpRequested,
@@ -2404,17 +3139,34 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2404
3139
  artifactPaths: pr.artifactPaths,
2405
3140
  transcriptPath: pr.transcriptPath,
2406
3141
  transcriptError: pr.transcriptError,
3142
+ effects: pr.effects,
3143
+ execution: pr.execution,
3144
+ review: pr.review,
2407
3145
  structuredOutput: pr.structuredOutput,
2408
3146
  structuredOutputPath: pr.structuredOutputPath,
2409
3147
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2410
3148
  acceptance: pr.acceptance,
3149
+ watchdog: pr.watchdog,
3150
+ capabilityCeiling: pr.capabilityCeiling,
3151
+ capabilityAudit: pr.capabilityAudit,
2411
3152
  });
2412
3153
  }
2413
3154
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
2414
3155
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
2415
- if (failures.length === 0) {
3156
+ const acceptanceFailures = parallelResults
3157
+ .map((result, originalIndex) => ({ result, originalIndex, task: dynamicSteps[originalIndex] }))
3158
+ .filter(({ result, task }) => isAgentContractV1(task?.agentContract ?? step.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3159
+ if (acceptanceFailures.length > 0) {
3160
+ const message = acceptanceFailures
3161
+ .map(({ result, originalIndex }) => `Dynamic item ${originalIndex + 1} (${result.agent}, key ${materialized.items[originalIndex]?.key ?? originalIndex}) acceptance rejected: ${acceptanceFailureMessage(result.acceptance) ?? "acceptance rejected"}`)
3162
+ .join("\n");
3163
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3164
+ statusPayload.error = message;
3165
+ markDynamicGraphGroup(stepIndex, "failed", message);
3166
+ }
3167
+ if (failures.length === 0 && acceptanceFailures.length === 0) {
2416
3168
  try {
2417
- validateDynamicCollection(step.collect.outputSchema, collection);
3169
+ await validateDynamicCollection(step.collect.outputSchema, collection);
2418
3170
  outputs[step.collect.as] = {
2419
3171
  text: JSON.stringify(collection),
2420
3172
  structured: collection,
@@ -2422,24 +3174,26 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2422
3174
  stepIndex,
2423
3175
  };
2424
3176
  statusPayload.outputs = outputs;
2425
- const groupAcceptance = step.effectiveAcceptance && !timedOut
3177
+ const groupAcceptance = !timedOut && !stopped
2426
3178
  ? await evaluateAcceptance({
2427
- acceptance: step.effectiveAcceptance,
3179
+ acceptance: effectiveDynamicGroupAcceptance,
2428
3180
  output: "",
2429
3181
  report: aggregateAcceptanceReport({
2430
3182
  results: parallelResults,
2431
3183
  notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
2432
3184
  }),
2433
3185
  cwd,
2434
- signal: timeoutAbortController.signal,
2435
- abortMessage: timeoutMessage ?? "Subagent timed out.",
3186
+ signal: combinedAbortSignal([timeoutAbortController.signal, stopAbortController.signal]),
3187
+ abortMessage: stopAbortController.signal.aborted ? stopMessage : timeoutMessage ?? "Subagent timed out.",
3188
+ reportOptional: isAgentContractV1(step.agentContract),
2436
3189
  })
2437
3190
  : undefined;
2438
- const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2439
- const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2440
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2441
- const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2442
- markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
3191
+ const groupStopped = stopped || stopAbortController.signal.aborted;
3192
+ const groupTimedOut = !groupStopped && (timedOut || timeoutAbortController.signal.aborted);
3193
+ const effectiveGroupAcceptance = groupTimedOut || groupStopped ? undefined : groupAcceptance;
3194
+ const groupAcceptanceFailure = effectiveDynamicGroupAcceptance.explicit && effectiveGroupAcceptance && (!isAgentContractV1(step.agentContract) || step.gateOn === "acceptance") ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
3195
+ const groupError = groupStopped ? stopMessage : groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
3196
+ markDynamicGraphGroup(stepIndex, groupError ? groupStopped ? "stopped" : "failed" : "completed", groupError, effectiveGroupAcceptance);
2443
3197
  if (groupError) {
2444
3198
  results.push({
2445
3199
  agent: step.parallel.agent,
@@ -2448,14 +3202,16 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2448
3202
  success: false,
2449
3203
  exitCode: 1,
2450
3204
  timedOut: groupTimedOut ? true : undefined,
3205
+ stopped: groupStopped ? true : undefined,
2451
3206
  structuredOutput: collection,
2452
3207
  acceptance: effectiveGroupAcceptance,
2453
3208
  });
2454
3209
  statusPayload.error = groupError;
3210
+ statusPayload.stopped = groupStopped ? true : statusPayload.stopped;
2455
3211
  }
2456
3212
  } catch (error) {
2457
3213
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
2458
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
3214
+ results.push({ agent: step.parallel.agent, context: step.parallel.context, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
2459
3215
  statusPayload.error = message;
2460
3216
  markDynamicGraphGroup(stepIndex, "failed", message);
2461
3217
  }
@@ -2475,7 +3231,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2475
3231
  ts: Date.now(),
2476
3232
  runId: id,
2477
3233
  stepIndex,
2478
- success: failures.length === 0,
3234
+ success: failures.length === 0 && acceptanceFailures.length === 0,
2479
3235
  }));
2480
3236
  if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
2481
3237
  statusPayload.lastUpdate = Date.now();
@@ -2491,6 +3247,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2491
3247
  const groupStartFlatIndex = flatIndex;
2492
3248
  let aborted = false;
2493
3249
  let worktreeSetup: WorktreeSetup | undefined;
3250
+ let worktreeFinalized = false;
2494
3251
  if (group.worktree) {
2495
3252
  const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
2496
3253
  if (worktreeTaskCwdConflict) {
@@ -2559,8 +3316,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2559
3316
  concurrency,
2560
3317
  async (task, taskIdx) => {
2561
3318
  const fi = groupStartFlatIndex + taskIdx;
2562
- if (timedOut) return timedOutStepResult(task.agent);
2563
- if (interrupted) return pausedStepResult(task.agent);
3319
+ if (timedOut) return timedOutStepResult(task.agent, task.context);
3320
+ if (stopped) return stoppedStepResult(task.agent, task.context);
3321
+ if (interrupted) return pausedStepResult(task.agent, task.context);
2564
3322
  if (aborted && failFast) {
2565
3323
  const skippedAt = Date.now();
2566
3324
  statusPayload.steps[fi].status = "failed";
@@ -2575,7 +3333,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2575
3333
  appendJsonl(eventsPath, JSON.stringify({
2576
3334
  type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
2577
3335
  }));
2578
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
3336
+ return { agent: task.agent, context: task.context, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2579
3337
  }
2580
3338
 
2581
3339
  const taskStartTime = Date.now();
@@ -2611,20 +3369,27 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2611
3369
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
2612
3370
  outputFile: path.join(asyncDir, `output-${fi}.log`),
2613
3371
  steerInboxDir: stepSteerInboxDir(asyncDir, fi),
3372
+ steerCapabilityPath: steerCapabilityPath(asyncDir, fi),
3373
+ steerAckDir: steerAcksDir(asyncDir, fi),
2614
3374
  piPackageRoot: config.piPackageRoot,
2615
3375
  piArgv1: config.piArgv1,
2616
3376
  childIntercomTarget: config.childIntercomTargets?.[fi],
2617
3377
  orchestratorIntercomTarget: config.controlIntercomTarget,
2618
3378
  nestedRoute: config.nestedRoute,
3379
+ capabilityCeiling: config.capabilityCeiling,
2619
3380
  registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2620
3381
  registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
3382
+ registerStop: (stop) => registerStepStop(fi, stop),
2621
3383
  registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2622
3384
  timeoutSignal: timeoutAbortController.signal,
3385
+ stopSignal: stopAbortController.signal,
2623
3386
  timeoutMessage,
3387
+ stopMessage,
2624
3388
  turnBudget: config.turnBudget,
2625
3389
  onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2626
3390
  onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2627
- skipAcceptance: () => timedOut,
3391
+ onWriterProcess,
3392
+ skipAcceptance: () => timedOut || stopped,
2628
3393
  });
2629
3394
  if (task.sessionFile) {
2630
3395
  latestSessionFile = task.sessionFile;
@@ -2633,12 +3398,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2633
3398
  const taskEndTime = Date.now();
2634
3399
  const taskDuration = taskEndTime - taskStartTime;
2635
3400
  const childInterrupted = singleResult.interrupted === true;
3401
+ const childStopped = singleResult.stopped === true;
2636
3402
 
2637
- statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3403
+ statusPayload.steps[fi].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2638
3404
  statusPayload.steps[fi].endedAt = taskEndTime;
2639
3405
  statusPayload.steps[fi].durationMs = taskDuration;
2640
- statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3406
+ statusPayload.steps[fi].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2641
3407
  statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
3408
+ statusPayload.steps[fi].stopped = stopped || childStopped ? true : undefined;
2642
3409
  statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2643
3410
  statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2644
3411
  statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
@@ -2654,20 +3421,30 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2654
3421
  statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2655
3422
  statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2656
3423
  statusPayload.steps[fi].totalCost = singleResult.totalCost;
2657
- statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3424
+ statusPayload.steps[fi].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2658
3425
  statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2659
3426
  statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
3427
+ statusPayload.steps[fi].agentContract = singleResult.agentContract;
3428
+ statusPayload.steps[fi].effects = singleResult.effects;
3429
+ statusPayload.steps[fi].execution = singleResult.execution;
3430
+ statusPayload.steps[fi].review = singleResult.review;
2660
3431
  statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2661
3432
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2662
3433
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2663
3434
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
3435
+ statusPayload.steps[fi].watchdog = singleResult.watchdog;
3436
+ statusPayload.steps[fi].capabilityCeiling = singleResult.capabilityCeiling;
3437
+ statusPayload.steps[fi].capabilityAudit = singleResult.capabilityAudit;
3438
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3439
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
2664
3440
  statusPayload.lastUpdate = taskEndTime;
2665
3441
  writeStatusPayload();
3442
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, fi, task.agent, singleResult);
2666
3443
 
2667
3444
  appendJsonl(eventsPath, JSON.stringify({
2668
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
3445
+ type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2669
3446
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2670
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
3447
+ exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
2671
3448
  }));
2672
3449
  if (singleResult.completionGuardTriggered) {
2673
3450
  const event = buildControlEvent({
@@ -2684,7 +3461,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2684
3461
  }
2685
3462
 
2686
3463
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
2687
- return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
3464
+ return stopped || childStopped ? { ...singleResult, output: stopMessage, error: stopMessage, exitCode: 1, interrupted: false, timedOut: false, stopped: true, skipped: false } : timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2688
3465
  },
2689
3466
  globalSemaphore,
2690
3467
  );
@@ -2712,13 +3489,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2712
3489
  for (const pr of parallelResults) {
2713
3490
  results.push({
2714
3491
  agent: pr.agent,
3492
+ context: pr.context,
3493
+ agentContract: pr.agentContract,
3494
+ launchContractDigest: pr.launchContractDigest,
2715
3495
  output: pr.output,
2716
3496
  error: pr.error,
2717
- success: pr.interrupted !== true && pr.exitCode === 0,
3497
+ protocolError: pr.protocolError,
3498
+ success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
2718
3499
  exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2719
3500
  skipped: pr.skipped,
2720
3501
  interrupted: pr.interrupted,
2721
3502
  timedOut: pr.timedOut,
3503
+ stopped: pr.stopped,
2722
3504
  turnBudget: pr.turnBudget,
2723
3505
  turnBudgetExceeded: pr.turnBudgetExceeded,
2724
3506
  wrapUpRequested: pr.wrapUpRequested,
@@ -2733,12 +3515,16 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2733
3515
  artifactPaths: pr.artifactPaths,
2734
3516
  transcriptPath: pr.transcriptPath,
2735
3517
  transcriptError: pr.transcriptError,
2736
- structuredOutput: pr.structuredOutput,
2737
- structuredOutputPath: pr.structuredOutputPath,
2738
- structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2739
- acceptance: pr.acceptance,
2740
- });
2741
- }
3518
+ effects: pr.effects,
3519
+ execution: pr.execution,
3520
+ review: pr.review,
3521
+ structuredOutput: pr.structuredOutput,
3522
+ structuredOutputPath: pr.structuredOutputPath,
3523
+ structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
3524
+ acceptance: pr.acceptance,
3525
+ watchdog: pr.watchdog,
3526
+ });
3527
+ }
2742
3528
  for (let t = 0; t < group.parallel.length; t++) {
2743
3529
  const outputName = group.parallel[t]?.outputName;
2744
3530
  if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
@@ -2751,29 +3537,70 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2751
3537
 
2752
3538
  previousOutput = aggregateParallelOutputs(
2753
3539
  parallelResults.map((r) => ({
2754
- agent: r.agent,
2755
- output: r.output,
2756
- exitCode: r.exitCode,
2757
- error: r.error,
2758
- model: r.model,
2759
- attemptedModels: r.attemptedModels,
2760
- })),
3540
+ agent: r.agent,
3541
+ output: r.output,
3542
+ exitCode: r.exitCode,
3543
+ error: r.error,
3544
+ model: r.model,
3545
+ attemptedModels: r.attemptedModels,
3546
+ })),
2761
3547
  );
2762
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
3548
+ if (worktreeSetup) {
3549
+ const captured = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
3550
+ if (captured.summary) previousOutput = `${previousOutput}\n\n${captured.summary}`;
3551
+ worktreeFinalized = true;
3552
+ const cleanup = cleanupWorktrees(worktreeSetup);
3553
+ try {
3554
+ statusPayload.parallelHandoff = writeParallelHandoffGroup({
3555
+ manifestPath: parallelHandoffPath(asyncDir),
3556
+ runId: id,
3557
+ mode: (config.resultMode ?? statusPayload.mode) === "parallel" ? "parallel" : "chain",
3558
+ source: "async",
3559
+ cwd,
3560
+ stepIndex,
3561
+ flatStartIndex: groupStartFlatIndex,
3562
+ setup: worktreeSetup,
3563
+ diffs: captured.diffs,
3564
+ cleanup,
3565
+ results: parallelResults.map((result) => ({
3566
+ agent: result.agent,
3567
+ status: result.stopped ? "stopped" : result.interrupted ? "paused" : result.exitCode === 0 ? "completed" : "failed",
3568
+ summary: result.output || result.error || "(no output)",
3569
+ ...(result.artifactPaths?.outputPath ? { outputPath: result.artifactPaths.outputPath } : {}),
3570
+ ...(result.structuredOutput !== undefined ? { structuredOutput: result.structuredOutput } : {}),
3571
+ ...(result.structuredOutputPath ? { structuredOutputPath: result.structuredOutputPath } : {}),
3572
+ ...(result.sessionFile ? { sessionPath: result.sessionFile } : {}),
3573
+ })),
3574
+ });
3575
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffReference(statusPayload.parallelHandoff)}`;
3576
+ } catch (error) {
3577
+ previousOutput = `${previousOutput}\n\n${formatParallelHandoffError(error)}`;
3578
+ }
3579
+ writeStatusPayload();
3580
+ }
2763
3581
 
2764
3582
  appendJsonl(eventsPath, JSON.stringify({
2765
3583
  type: "subagent.parallel.completed",
2766
3584
  ts: Date.now(),
2767
3585
  runId: id,
2768
3586
  stepIndex,
2769
- success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
3587
+ success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1)
3588
+ && parallelResults.every((result, index) => !(isAgentContractV1(group.parallel[index]?.agentContract) && group.parallel[index]?.gateOn === "acceptance" && result.acceptance?.status === "rejected")),
2770
3589
  }));
2771
3590
 
3591
+ const acceptanceGateFailure = parallelResults
3592
+ .map((result, index) => ({ result, index, task: group.parallel[index] }))
3593
+ .find(({ result, task }) => isAgentContractV1(task?.agentContract) && task?.gateOn === "acceptance" && result.acceptance?.status === "rejected");
3594
+ if (acceptanceGateFailure) {
3595
+ statusPayload.error = acceptanceFailureMessage(acceptanceGateFailure.result.acceptance) ?? "Parallel acceptance gate rejected the step.";
3596
+ writeStatusPayload();
3597
+ break;
3598
+ }
2772
3599
  if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
2773
3600
  break;
2774
3601
  }
2775
3602
  } finally {
2776
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
3603
+ if (worktreeSetup && !worktreeFinalized) cleanupWorktrees(worktreeSetup);
2777
3604
  }
2778
3605
  } else {
2779
3606
  const seqStep = step as SubagentStep;
@@ -2802,38 +3629,50 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2802
3629
  flushPendingStepSteers(flatIndex);
2803
3630
  const singleResult = await runSingleStep(seqStep, {
2804
3631
  previousOutput, placeholder, cwd, sessionEnabled,
2805
- outputs,
3632
+ outputs: statusPayload.mode === "single" ? undefined : outputs,
2806
3633
  sessionDir: config.sessionDir,
2807
3634
  artifactsDir, artifactConfig, id,
2808
3635
  flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2809
3636
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2810
3637
  steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
3638
+ steerCapabilityPath: steerCapabilityPath(asyncDir, flatIndex),
3639
+ steerAckDir: steerAcksDir(asyncDir, flatIndex),
2811
3640
  piPackageRoot: config.piPackageRoot,
2812
3641
  piArgv1: config.piArgv1,
2813
3642
  childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2814
3643
  orchestratorIntercomTarget: config.controlIntercomTarget,
2815
3644
  nestedRoute: config.nestedRoute,
3645
+ capabilityCeiling: config.capabilityCeiling,
2816
3646
  registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
2817
3647
  registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
3648
+ registerStop: (stop) => registerStepStop(flatIndex, stop),
2818
3649
  registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
2819
3650
  timeoutSignal: timeoutAbortController.signal,
3651
+ stopSignal: stopAbortController.signal,
2820
3652
  timeoutMessage,
3653
+ stopMessage,
2821
3654
  turnBudget: config.turnBudget,
2822
3655
  onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2823
3656
  onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2824
- skipAcceptance: () => timedOut,
3657
+ onWriterProcess,
3658
+ skipAcceptance: () => timedOut || stopped,
2825
3659
  });
2826
3660
  if (seqStep.sessionFile) {
2827
3661
  latestSessionFile = seqStep.sessionFile;
2828
3662
  }
2829
3663
 
2830
3664
  previousOutput = singleResult.output;
3665
+ const childStopped = singleResult.stopped === true;
2831
3666
  results.push({
2832
3667
  agent: singleResult.agent,
2833
- output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
2834
- error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
2835
- success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
2836
- exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
3668
+ context: singleResult.context,
3669
+ agentContract: singleResult.agentContract,
3670
+ launchContractDigest: singleResult.launchContractDigest,
3671
+ output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3672
+ error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3673
+ protocolError: singleResult.protocolError,
3674
+ success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
3675
+ exitCode: stopped || childStopped ? 1 : timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
2837
3676
  sessionFile: singleResult.sessionFile,
2838
3677
  intercomTarget: singleResult.intercomTarget,
2839
3678
  model: singleResult.model,
@@ -2843,12 +3682,19 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2843
3682
  artifactPaths: singleResult.artifactPaths,
2844
3683
  transcriptPath: singleResult.transcriptPath,
2845
3684
  transcriptError: singleResult.transcriptError,
3685
+ effects: singleResult.effects,
3686
+ execution: singleResult.execution,
3687
+ review: singleResult.review,
2846
3688
  structuredOutput: singleResult.structuredOutput,
2847
3689
  structuredOutputPath: singleResult.structuredOutputPath,
2848
3690
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2849
3691
  acceptance: singleResult.acceptance,
3692
+ watchdog: singleResult.watchdog,
3693
+ capabilityCeiling: singleResult.capabilityCeiling,
3694
+ capabilityAudit: singleResult.capabilityAudit,
2850
3695
  interrupted: singleResult.interrupted,
2851
3696
  timedOut: timedOut || singleResult.timedOut ? true : undefined,
3697
+ stopped: stopped || childStopped ? true : undefined,
2852
3698
  turnBudget: singleResult.turnBudget,
2853
3699
  turnBudgetExceeded: singleResult.turnBudgetExceeded,
2854
3700
  wrapUpRequested: singleResult.wrapUpRequested,
@@ -2887,11 +3733,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2887
3733
 
2888
3734
  const stepEndTime = Date.now();
2889
3735
  const childInterrupted = singleResult.interrupted === true;
2890
- statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
3736
+ statusPayload.steps[flatIndex].status = stopped || childStopped ? "stopped" : timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2891
3737
  statusPayload.steps[flatIndex].endedAt = stepEndTime;
2892
3738
  statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2893
- statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
3739
+ statusPayload.steps[flatIndex].exitCode = stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2894
3740
  statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
3741
+ statusPayload.steps[flatIndex].stopped = stopped || childStopped ? true : undefined;
2895
3742
  statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
2896
3743
  statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2897
3744
  statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
@@ -2907,27 +3754,37 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2907
3754
  statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
2908
3755
  statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
2909
3756
  statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
2910
- statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
3757
+ statusPayload.steps[flatIndex].error = stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2911
3758
  statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
2912
3759
  statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
3760
+ statusPayload.steps[flatIndex].agentContract = singleResult.agentContract;
3761
+ statusPayload.steps[flatIndex].effects = singleResult.effects;
3762
+ statusPayload.steps[flatIndex].execution = singleResult.execution;
3763
+ statusPayload.steps[flatIndex].review = singleResult.review;
2913
3764
  statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2914
3765
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2915
3766
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2916
3767
  statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
3768
+ statusPayload.steps[flatIndex].watchdog = singleResult.watchdog;
3769
+ statusPayload.steps[flatIndex].capabilityCeiling = singleResult.capabilityCeiling;
3770
+ statusPayload.steps[flatIndex].capabilityAudit = singleResult.capabilityAudit;
3771
+ if (singleResult.capabilityCeiling) statusPayload.capabilityCeiling = singleResult.capabilityCeiling;
3772
+ if (singleResult.capabilityAudit) statusPayload.capabilityAudit = singleResult.capabilityAudit;
2917
3773
  if (stepTokens) {
2918
3774
  statusPayload.steps[flatIndex].tokens = stepTokens;
2919
3775
  statusPayload.totalTokens = { ...previousCumulativeTokens };
2920
3776
  }
2921
3777
  statusPayload.lastUpdate = stepEndTime;
2922
3778
  writeStatusPayload();
3779
+ appendCapabilityCeilingAppliedEvent(eventsPath, id, flatIndex, seqStep.agent, singleResult);
2923
3780
 
2924
3781
  appendJsonl(eventsPath, JSON.stringify({
2925
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
3782
+ type: stopped || childStopped ? "subagent.step.stopped" : timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2926
3783
  ts: stepEndTime,
2927
3784
  runId: id,
2928
3785
  stepIndex: flatIndex,
2929
3786
  agent: seqStep.agent,
2930
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
3787
+ exitCode: stopped || childStopped ? 1 : timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
2931
3788
  durationMs: stepEndTime - stepStartTime,
2932
3789
  tokens: stepTokens,
2933
3790
  }));
@@ -2946,6 +3803,11 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2946
3803
  }
2947
3804
 
2948
3805
  flatIndex++;
3806
+ if (isAgentContractV1(seqStep.agentContract) && seqStep.gateOn === "acceptance" && singleResult.acceptance?.status === "rejected") {
3807
+ statusPayload.error = acceptanceFailureMessage(singleResult.acceptance) ?? "Chain acceptance gate rejected the step.";
3808
+ writeStatusPayload();
3809
+ break;
3810
+ }
2949
3811
  if (singleResult.exitCode !== 0) {
2950
3812
  break;
2951
3813
  }
@@ -2972,16 +3834,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2972
3834
  costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
2973
3835
  }), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
2974
3836
  const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
2975
- const totalChildUsage = results.reduce<Usage>((sum, result) => {
2976
- const usage = usageFromAttempts(result.modelAttempts);
2977
- sum.input += usage.input;
2978
- sum.output += usage.output;
2979
- sum.cacheRead += usage.cacheRead;
2980
- sum.cacheWrite += usage.cacheWrite;
2981
- sum.cost += usage.cost;
2982
- sum.turns += usage.turns;
2983
- return sum;
2984
- }, emptyUsage());
2985
3837
  const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2986
3838
  const agentName = finalFlatAgents.length === 1
2987
3839
  ? finalFlatAgents[0]!
@@ -3026,11 +3878,29 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
3026
3878
  clearTimeout(timeoutTimer);
3027
3879
  timeoutTimer = undefined;
3028
3880
  }
3881
+ statusPayload.state = stopped ? "stopped" : timedOut || turnBudgetExceeded || statusPayload.error ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3882
+ closeSteerInbox(asyncDir, statusPayload.state);
3029
3883
  disposeControlInbox();
3884
+ for (const request of consumeSteerRequests(asyncDir)) deliverSteerRequest(request);
3030
3885
  const effectiveSessionFile = sessionFile ?? latestSessionFile;
3886
+ const steeringLifecycle = steeringStatus(statusPayload);
3887
+ for (const request of steeringLifecycle.recent) {
3888
+ let changed = false;
3889
+ for (const target of request.targets) {
3890
+ if (target.state !== "scheduled" && target.state !== "routed") continue;
3891
+ changed = true;
3892
+ updateSteeringLifecycleTarget(request.id, target.index, "failed", Date.now(), { reason: "child terminated before steering delivery" });
3893
+ markSteeringAttention(target.index);
3894
+ emitSteeringEvent("subagent.steer.failed", { type: "steer", id: request.id, ts: request.requestedAt, message: "child terminated before steering delivery" }, target.index, { reason: "child terminated before steering delivery" });
3895
+ }
3896
+ if (changed) emitTerminalSteeringNotice(request.id, `Steering failed for run ${id}: child terminated before delivery.`);
3897
+ }
3031
3898
  const runEndedAt = Date.now();
3032
- statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3033
3899
  statusPayload.activityState = undefined;
3900
+ if (stopped) {
3901
+ statusPayload.stopped = true;
3902
+ statusPayload.error = stopMessage;
3903
+ }
3034
3904
  if (timedOut) {
3035
3905
  statusPayload.timedOut = true;
3036
3906
  statusPayload.error = timeoutMessage ?? "Subagent timed out.";
@@ -3091,9 +3961,9 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
3091
3961
  id,
3092
3962
  agent: agentName,
3093
3963
  mode: resultMode,
3094
- success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
3095
- state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
3096
- summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
3964
+ success: !stopped && !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
3965
+ state: stopped ? "stopped" : timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
3966
+ summary: stopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
3097
3967
  ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
3098
3968
  ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
3099
3969
  ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
@@ -3101,15 +3971,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
3101
3971
  ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
3102
3972
  ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
3103
3973
  ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
3104
- ...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
3974
+ ...(stopped ? { stopped: true, error: stopMessage } : timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
3105
3975
  results: results.map((r) => ({
3106
3976
  agent: r.agent,
3977
+ context: r.context,
3107
3978
  output: r.output,
3108
3979
  error: r.error,
3980
+ protocolError: r.protocolError,
3109
3981
  success: r.success,
3110
3982
  skipped: r.skipped || undefined,
3111
3983
  interrupted: r.interrupted || undefined,
3112
3984
  timedOut: r.timedOut || undefined,
3985
+ stopped: r.stopped || undefined,
3113
3986
  turnBudget: r.turnBudget,
3114
3987
  turnBudgetExceeded: r.turnBudgetExceeded || undefined,
3115
3988
  wrapUpRequested: r.wrapUpRequested || undefined,
@@ -3125,23 +3998,34 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
3125
3998
  truncated: r.truncated,
3126
3999
  transcriptPath: r.transcriptPath,
3127
4000
  transcriptError: r.transcriptError,
4001
+ agentContract: r.agentContract,
4002
+ launchContractDigest: r.launchContractDigest,
4003
+ execution: r.execution,
4004
+ review: r.review,
4005
+ effects: r.effects,
3128
4006
  structuredOutput: r.structuredOutput,
3129
4007
  structuredOutputPath: r.structuredOutputPath,
3130
4008
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
3131
4009
  acceptance: r.acceptance,
4010
+ watchdog: r.watchdog,
4011
+ capabilityCeiling: r.capabilityCeiling,
4012
+ capabilityAudit: r.capabilityAudit,
3132
4013
  })),
3133
4014
  outputs,
3134
4015
  workflowGraph: statusPayload.workflowGraph,
3135
- exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
4016
+ parallelHandoff: statusPayload.parallelHandoff,
4017
+ capabilityCeiling: statusPayload.capabilityCeiling,
4018
+ capabilityAudit: statusPayload.capabilityAudit,
4019
+ exitCode: stopped || timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
3136
4020
  timestamp: runEndedAt,
3137
4021
  durationMs: runEndedAt - overallStartTime,
3138
4022
  totalTokens: statusPayload.totalTokens,
3139
- totalChildUsage,
3140
4023
  totalCost: finalTotalCost,
3141
4024
  truncated,
3142
4025
  artifactsDir,
3143
4026
  cwd,
3144
4027
  asyncDir,
4028
+ launchContractDigest: config.launchContractDigest,
3145
4029
  sessionId: config.sessionId,
3146
4030
  sessionFile: effectiveSessionFile,
3147
4031
  intercomTarget: config.controlIntercomTarget,
@@ -3154,6 +4038,118 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
3154
4038
  } catch (err) {
3155
4039
  console.error(`Failed to write result file ${resultPath}:`, err);
3156
4040
  }
4041
+ if (config.runnerProcessInstanceId) {
4042
+ const writers: Record<string, Array<{ processInstanceId: string; kind: "pi-writer"; attempt: number; closeObservedAt: number; exitCode: number | null; signal: string | null }>> = {};
4043
+ const expectedWriters: Record<string, number> = {};
4044
+ for (const [index, result] of results.entries()) {
4045
+ writers[String(index)] = result.writerProcesses ?? [];
4046
+ expectedWriters[String(index)] = result.writerAttemptCount ?? 0;
4047
+ }
4048
+ const candidate: ProcessTerminalCandidate = {
4049
+ version: 1,
4050
+ runId: id,
4051
+ runnerProcessInstanceId: config.runnerProcessInstanceId,
4052
+ writers,
4053
+ expectedWriters,
4054
+ ...(config.revivalLease?.sessionFile ? { sessionFile: config.revivalLease.sessionFile } : {}),
4055
+ ...(config.revivalLeaseToken ? { revivalLeaseToken: config.revivalLeaseToken } : {}),
4056
+ };
4057
+ try {
4058
+ writeProcessTerminalCandidate(asyncDir, candidate);
4059
+ } catch (error) {
4060
+ console.error(`Failed to write process-terminal candidate for '${id}':`, error);
4061
+ }
4062
+ }
4063
+ }
4064
+
4065
+ async function waitForStartupControl(
4066
+ controlPath: string,
4067
+ token: string,
4068
+ action: "ack" | "proceed",
4069
+ timeoutMs = 30_000,
4070
+ ): Promise<void> {
4071
+ const deadline = Date.now() + timeoutMs;
4072
+ while (Date.now() <= deadline) {
4073
+ if (fs.existsSync(controlPath)) {
4074
+ let payload: { action?: unknown; token?: unknown };
4075
+ try {
4076
+ payload = JSON.parse(fs.readFileSync(controlPath, "utf-8")) as { action?: unknown; token?: unknown };
4077
+ } catch (error) {
4078
+ throw new Error(`Failed to read runner startup control '${controlPath}': ${error instanceof Error ? error.message : String(error)}`);
4079
+ }
4080
+ if (payload.token !== token) throw new Error("Runner startup control token does not match the acquired session lease.");
4081
+ if (payload.action === action) return;
4082
+ if (payload.action !== "ack" && payload.action !== "proceed") throw new Error("Runner startup control action is invalid.");
4083
+ }
4084
+ await new Promise((resolve) => setTimeout(resolve, 20));
4085
+ }
4086
+ throw new Error(`Timed out after ${timeoutMs}ms waiting for runner startup control '${action}'.`);
4087
+ }
4088
+
4089
+ async function runConfiguredSubagent(config: SubagentRunConfig): Promise<void> {
4090
+ let lease: ReturnType<typeof acquireSessionLease> | undefined;
4091
+ let startupCommitted = config.revivalLease === undefined;
4092
+ const startupPath = path.join(config.asyncDir, "runner-startup.json");
4093
+ const startupAckPath = path.join(config.asyncDir, "runner-startup-ack.json");
4094
+ const startupProceedPath = path.join(config.asyncDir, "runner-startup-proceed.json");
4095
+ const releaseOnExit = (): void => {
4096
+ try {
4097
+ lease?.release();
4098
+ } catch {
4099
+ // Exit cleanup is best effort; a dead-owner lease is reclaimed on the next revival.
4100
+ }
4101
+ };
4102
+ process.once("exit", releaseOnExit);
4103
+ try {
4104
+ if (config.revivalLease) {
4105
+ lease = acquireSessionLease(config.revivalLease);
4106
+ config.revivalLeaseToken = lease.owner.token;
4107
+ writeAtomicJson(startupPath, { state: "ready", token: lease.owner.token, pid: process.pid, owner: lease.owner });
4108
+ await waitForStartupControl(startupAckPath, lease.owner.token, "ack");
4109
+ writeAtomicJson(startupPath, { state: "acknowledged", token: lease.owner.token, pid: process.pid });
4110
+ await waitForStartupControl(startupProceedPath, lease.owner.token, "proceed");
4111
+ startupCommitted = true;
4112
+ for (const controlPath of [startupAckPath, startupProceedPath]) {
4113
+ try {
4114
+ fs.rmSync(controlPath, { force: true });
4115
+ } catch {
4116
+ // Startup control cleanup is best effort after the parent commits the run.
4117
+ }
4118
+ }
4119
+ }
4120
+ await runSubagent(config, lease ? (writer) => lease!.updateWriter(writer) : undefined);
4121
+ } catch (error) {
4122
+ if (config.revivalLease && !startupCommitted) {
4123
+ try {
4124
+ writeAtomicJson(startupPath, { state: "error", pid: process.pid, error: error instanceof Error ? error.message : String(error) });
4125
+ } catch {
4126
+ // The parent will time out and terminate this runner if the handshake cannot be written.
4127
+ }
4128
+ }
4129
+ throw error;
4130
+ } finally {
4131
+ process.off("exit", releaseOnExit);
4132
+ if (lease) {
4133
+ let acknowledged = false;
4134
+ try {
4135
+ acknowledged = lease.release();
4136
+ } catch (error) {
4137
+ console.error("Failed to release session revival lease:", error);
4138
+ }
4139
+ try {
4140
+ markProcessTerminalCandidateLeaseRelease(config.asyncDir, lease.owner.token, acknowledged);
4141
+ } catch (error) {
4142
+ console.error("Failed to record session revival lease release:", error);
4143
+ }
4144
+ }
4145
+ }
4146
+ }
4147
+
4148
+ function startConfiguredSubagent(config: SubagentRunConfig): void {
4149
+ runConfiguredSubagent(config).catch((runErr) => {
4150
+ console.error("Subagent runner error:", runErr);
4151
+ process.exit(1);
4152
+ });
3157
4153
  }
3158
4154
 
3159
4155
  const configArg = process.argv[2];
@@ -3166,10 +4162,7 @@ if (configArg) {
3166
4162
  } catch {
3167
4163
  // Temp config cleanup is best effort.
3168
4164
  }
3169
- runSubagent(config).catch((runErr) => {
3170
- console.error("Subagent runner error:", runErr);
3171
- process.exit(1);
3172
- });
4165
+ startConfiguredSubagent(config);
3173
4166
  } catch (err) {
3174
4167
  console.error("Subagent runner error:", err);
3175
4168
  process.exit(1);
@@ -3183,10 +4176,7 @@ if (configArg) {
3183
4176
  process.stdin.on("end", () => {
3184
4177
  try {
3185
4178
  const config = JSON.parse(input) as SubagentRunConfig;
3186
- runSubagent(config).catch((runErr) => {
3187
- console.error("Subagent runner error:", runErr);
3188
- process.exit(1);
3189
- });
4179
+ startConfiguredSubagent(config);
3190
4180
  } catch (err) {
3191
4181
  console.error("Subagent runner error:", err);
3192
4182
  process.exit(1);