@selesai/code 0.9.11 → 0.9.12

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 (580) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/config.d.ts +4 -6
  3. package/dist/config.js +10 -14
  4. package/dist/core/agent-session.d.ts +0 -1
  5. package/dist/core/agent-session.js +3 -49
  6. package/dist/core/extensions/types.d.ts +0 -2
  7. package/dist/core/settings-manager.d.ts +0 -2
  8. package/dist/core/settings-manager.js +0 -3
  9. package/dist/core/tools/index.d.ts +0 -1
  10. package/dist/core/tools/index.js +0 -1
  11. package/dist/defaults/settings.json +0 -1
  12. package/dist/extensions/model-prompt-injector/index.test.ts +0 -10
  13. package/dist/extensions/model-prompt-injector/index.ts +0 -3
  14. package/dist/extensions/pi-intercom/CHANGELOG.md +53 -16
  15. package/dist/extensions/pi-intercom/README.md +41 -8
  16. package/dist/extensions/pi-intercom/banner.png +0 -0
  17. package/dist/extensions/pi-intercom/broker/broker.ts +498 -215
  18. package/dist/extensions/pi-intercom/broker/client.ts +39 -12
  19. package/dist/extensions/pi-intercom/broker/extension.test.ts +102 -3
  20. package/dist/extensions/pi-intercom/broker/paths.test.ts +2 -2
  21. package/dist/extensions/pi-intercom/broker/protocol.ts +26 -0
  22. package/dist/extensions/pi-intercom/broker/spawn.test.ts +5 -20
  23. package/dist/extensions/pi-intercom/config.test.ts +41 -0
  24. package/dist/extensions/pi-intercom/config.ts +21 -0
  25. package/dist/extensions/pi-intercom/extension-api.ts +40 -0
  26. package/dist/extensions/pi-intercom/index.ts +362 -26
  27. package/dist/extensions/pi-intercom/intercom.integration.test.ts +819 -13
  28. package/dist/extensions/pi-intercom/package.json +1 -1
  29. package/dist/extensions/pi-intercom/project-agent.ts +2 -2
  30. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +8 -110
  31. package/dist/extensions/pi-intercom/types.ts +32 -5
  32. package/dist/extensions/pi-intercom/ui/inline-message.ts +6 -0
  33. package/dist/extensions/pi-subagents/.github/assets/subagent-fleet-inspector.png +0 -0
  34. package/dist/extensions/pi-subagents/.github/workflows/release.yml +28 -0
  35. package/dist/extensions/pi-subagents/.github/workflows/test.yml +51 -0
  36. package/dist/extensions/pi-subagents/.greptile/config.json +33 -0
  37. package/dist/extensions/pi-subagents/.greptile/files.json +8 -0
  38. package/dist/extensions/pi-subagents/.greptile/rules.md +16 -0
  39. package/dist/extensions/pi-subagents/CHANGELOG.md +309 -87
  40. package/dist/extensions/pi-subagents/README.md +15 -17
  41. package/dist/extensions/pi-subagents/agents/oracle.md +3 -1
  42. package/dist/extensions/pi-subagents/agents/researcher.md +32 -13
  43. package/dist/extensions/pi-subagents/agents/reviewer.md +13 -2
  44. package/dist/extensions/pi-subagents/agents/scout.md +2 -2
  45. package/dist/extensions/pi-subagents/agents/worker.md +2 -1
  46. package/dist/extensions/pi-subagents/async-retention-discovery-worker.mjs +180 -0
  47. package/dist/extensions/pi-subagents/docs/configuration.md +106 -27
  48. package/dist/extensions/pi-subagents/docs/extension-api.md +85 -21
  49. package/dist/extensions/pi-subagents/docs/missions.md +41 -4
  50. package/dist/extensions/pi-subagents/docs/models.md +52 -9
  51. package/dist/extensions/pi-subagents/docs/observability.md +103 -16
  52. package/dist/extensions/pi-subagents/docs/tool-reference.md +60 -45
  53. package/dist/extensions/pi-subagents/docs/watchdog.md +60 -3
  54. package/dist/extensions/pi-subagents/docs/workflows.md +181 -7
  55. package/dist/extensions/pi-subagents/index.ts +10 -1
  56. package/dist/extensions/pi-subagents/install.mjs +4 -4
  57. package/dist/extensions/pi-subagents/package-lock.json +1483 -1470
  58. package/dist/extensions/pi-subagents/package.json +6 -3
  59. package/dist/extensions/pi-subagents/prompts/council.md +60 -0
  60. package/dist/extensions/pi-subagents/prompts/parallel-review.md +5 -1
  61. package/dist/extensions/pi-subagents/prompts/review-loop.md +9 -5
  62. package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +254 -0
  63. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +18 -17
  64. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
  65. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +107 -140
  66. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
  67. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
  68. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +44 -92
  69. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +261 -368
  70. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +6 -2
  71. package/dist/extensions/pi-subagents/src/agents/agents.ts +388 -85
  72. package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +14 -0
  73. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +418 -0
  74. package/dist/extensions/pi-subagents/src/agents/skills.ts +1 -1
  75. package/dist/extensions/pi-subagents/src/api/agents.ts +7 -0
  76. package/dist/extensions/pi-subagents/src/api/external-job-provider.ts +195 -0
  77. package/dist/extensions/pi-subagents/src/api/preflight.ts +88 -27
  78. package/dist/extensions/pi-subagents/src/api/project-panes.ts +2 -0
  79. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -0
  80. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -11
  81. package/dist/extensions/pi-subagents/src/extension/doctor.ts +14 -6
  82. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -4
  83. package/dist/extensions/pi-subagents/src/extension/index.ts +270 -187
  84. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +65 -28
  85. package/dist/extensions/pi-subagents/src/extension/rpc.ts +120 -11
  86. package/dist/extensions/pi-subagents/src/extension/schemas.ts +46 -64
  87. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +51 -37
  88. package/dist/extensions/pi-subagents/src/extension/tool-result.ts +19 -0
  89. package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -2
  90. package/dist/extensions/pi-subagents/src/inspectors/herdr/client.ts +3 -3
  91. package/dist/extensions/pi-subagents/src/inspectors/herdr/focus.ts +55 -0
  92. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -1
  93. package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +230 -46
  94. package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +26 -4
  95. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +32 -10
  96. package/dist/extensions/pi-subagents/src/missions/lifecycle.ts +6 -1
  97. package/dist/extensions/pi-subagents/src/missions/store.ts +4 -9
  98. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +2 -2
  99. package/dist/extensions/pi-subagents/src/policy/authority.ts +2 -0
  100. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +3 -1
  101. package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +31 -8
  102. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +210 -52
  103. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +63 -33
  104. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +71 -17
  105. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +886 -0
  106. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +14 -5
  107. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +49 -56
  108. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +1 -0
  109. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +3 -33
  110. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +5 -0
  111. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +5 -1
  112. package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +22 -12
  113. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +105 -71
  114. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +68 -19
  115. package/dist/extensions/pi-subagents/src/runs/background/index-segment.ts +59 -0
  116. package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +443 -0
  117. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +94 -10
  118. package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +158 -90
  119. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +141 -53
  120. package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +8 -5
  121. package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +14 -4
  122. package/dist/extensions/pi-subagents/src/runs/background/run-id-query.ts +7 -0
  123. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +13 -14
  124. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +46 -25
  125. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +823 -0
  126. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +9 -3
  127. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +11 -1
  128. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +594 -216
  129. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +17 -13
  130. package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +138 -0
  131. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +23 -4
  132. package/dist/extensions/pi-subagents/src/runs/background/wait-subscriptions.ts +82 -4
  133. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +5 -3
  134. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +21 -14
  135. package/dist/extensions/pi-subagents/src/runs/foreground/async-stop-action.ts +23 -2
  136. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +153 -58
  137. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +7 -9
  138. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1283 -1657
  139. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +274 -0
  140. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +6 -5
  141. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +59 -10
  142. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  143. package/dist/extensions/pi-subagents/src/runs/shared/chain-outputs.ts +1 -3
  144. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +36 -0
  145. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +52 -2
  146. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  147. package/dist/extensions/pi-subagents/src/runs/shared/extension-bindings.ts +78 -0
  148. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +2 -0
  149. package/dist/extensions/pi-subagents/src/runs/shared/external-job-bridge.ts +460 -0
  150. package/dist/extensions/pi-subagents/src/runs/shared/external-job-runner.ts +389 -0
  151. package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +10 -0
  152. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
  153. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +243 -0
  154. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +201 -85
  155. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +106 -39
  156. package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +145 -0
  157. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +150 -27
  158. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +46 -4
  159. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +19 -15
  160. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +5 -1
  161. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +86 -2
  162. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +9 -9
  163. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +0 -6
  164. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +18 -4
  165. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +51 -7
  166. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +62 -21
  167. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +1 -3
  168. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -3
  169. package/dist/extensions/pi-subagents/src/runs/shared/workflow-graph.ts +1 -23
  170. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +14 -2
  171. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +2 -2
  172. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +22 -2
  173. package/dist/extensions/pi-subagents/src/shared/capacity-resilient-json.ts +102 -0
  174. package/dist/extensions/pi-subagents/src/shared/completion-owner.ts +14 -0
  175. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +49 -1
  176. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +59 -1
  177. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +12 -5
  178. package/dist/extensions/pi-subagents/src/shared/model-info.ts +20 -0
  179. package/dist/extensions/pi-subagents/src/shared/prompt-resources.ts +0 -40
  180. package/dist/extensions/pi-subagents/src/shared/settings.ts +11 -30
  181. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +52 -0
  182. package/dist/extensions/pi-subagents/src/shared/types.ts +228 -49
  183. package/dist/extensions/pi-subagents/src/shared/utils.ts +11 -3
  184. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +10 -0
  185. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +0 -162
  186. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +2 -5
  187. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +105 -13
  188. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +6 -36
  189. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +829 -0
  190. package/dist/extensions/pi-subagents/src/tui/fleet.ts +1415 -0
  191. package/dist/extensions/pi-subagents/src/tui/render.ts +78 -7
  192. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +1 -0
  194. package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +59 -51
  195. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +8 -2
  196. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +2 -1
  197. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +4 -4
  198. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +471 -86
  199. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +140 -0
  200. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
  201. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/cli.d.ts +3 -0
  202. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/register-bedrock.d.ts +2 -0
  203. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/bun/restore-sandbox-env.d.ts +17 -0
  204. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/args.d.ts +57 -0
  205. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/config-selector.d.ts +16 -0
  206. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/file-processor.d.ts +15 -0
  207. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/initial-message.d.ts +18 -0
  208. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/list-models.d.ts +9 -0
  209. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/project-trust.d.ts +10 -0
  210. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/session-picker.d.ts +10 -0
  211. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli/startup-ui.d.ts +20 -0
  212. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/cli.d.ts +3 -0
  213. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/config.d.ts +96 -0
  214. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-runtime.d.ts +119 -0
  215. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session-services.d.ts +85 -0
  216. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/agent-session.d.ts +620 -0
  217. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-guidance.d.ts +5 -0
  218. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/auth-storage.d.ts +56 -0
  219. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/bash-executor.d.ts +32 -0
  220. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/cache-stats.d.ts +49 -0
  221. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/branch-summarization.d.ts +93 -0
  222. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/compaction.d.ts +128 -0
  223. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/index.d.ts +7 -0
  224. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/compaction/utils.d.ts +38 -0
  225. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/defaults.d.ts +3 -0
  226. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/diagnostics.d.ts +15 -0
  227. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/event-bus.d.ts +9 -0
  228. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/exec.d.ts +29 -0
  229. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/experimental.d.ts +2 -0
  230. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/ansi-to-html.d.ts +22 -0
  231. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/index.d.ts +37 -0
  232. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/export-html/tool-renderer.d.ts +34 -0
  233. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/index.d.ts +12 -0
  234. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/loader.d.ts +23 -0
  235. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/runner.d.ts +171 -0
  236. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +1261 -0
  237. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/wrapper.d.ts +20 -0
  238. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/footer-data-provider.d.ts +54 -0
  239. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/http-dispatcher.d.ts +22 -0
  240. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/index.d.ts +13 -0
  241. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/keybindings.d.ts +358 -0
  242. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/messages.d.ts +77 -0
  243. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-config.d.ts +512 -0
  244. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-registry.d.ts +43 -0
  245. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-resolver.d.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/model-runtime.d.ts +82 -0
  247. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/models-store.d.ts +17 -0
  248. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/output-guard.d.ts +7 -0
  249. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/package-manager.d.ts +210 -0
  250. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/project-trust.d.ts +15 -0
  251. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/prompt-templates.d.ts +54 -0
  252. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-attribution.d.ts +4 -0
  253. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/provider-composer.d.ts +55 -0
  254. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/radius.d.ts +2 -0
  255. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/remote-catalog-provider.d.ts +5 -0
  256. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resolve-config-value.d.ts +30 -0
  257. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/resource-loader.d.ts +206 -0
  258. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/runtime-credentials.d.ts +15 -0
  259. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/sdk.d.ts +106 -0
  260. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-cwd.d.ts +19 -0
  261. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/session-manager.d.ts +356 -0
  262. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/settings-manager.d.ts +297 -0
  263. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/skills.d.ts +60 -0
  264. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/slash-commands.d.ts +15 -0
  265. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/source-info.d.ts +18 -0
  266. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/system-prompt.d.ts +28 -0
  267. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/telemetry.d.ts +3 -0
  268. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/timings.d.ts +10 -0
  269. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/bash.d.ts +68 -0
  270. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit-diff.d.ts +106 -0
  271. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/edit.d.ts +51 -0
  272. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/file-mutation-queue.d.ts +6 -0
  273. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/find.d.ts +35 -0
  274. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/grep.d.ts +37 -0
  275. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/index.d.ts +40 -0
  276. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/ls.d.ts +37 -0
  277. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/output-accumulator.d.ts +52 -0
  278. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/path-utils.d.ts +10 -0
  279. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/read.d.ts +35 -0
  280. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/render-utils.d.ts +24 -0
  281. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
  282. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/truncate.d.ts +70 -0
  283. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/tools/write.d.ts +26 -0
  284. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/trust-manager.d.ts +36 -0
  285. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/usage-totals.d.ts +19 -0
  286. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/index.d.ts +3 -0
  287. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/client.d.ts +61 -0
  288. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/huggingface.d.ts +23 -0
  289. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/index.d.ts +3 -0
  290. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/provider.d.ts +10 -0
  291. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/extensions/llama/ui.d.ts +42 -0
  292. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/index.d.ts +35 -0
  293. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/main.d.ts +12 -0
  294. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/migrations.d.ts +33 -0
  295. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/index.d.ts +9 -0
  296. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/armin.d.ts +34 -0
  297. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/assistant-message.d.ts +22 -0
  298. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bash-execution.d.ts +34 -0
  299. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/bordered-loader.d.ts +16 -0
  300. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/branch-summary-message.d.ts +16 -0
  301. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/compaction-summary-message.d.ts +16 -0
  302. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/config-selector.d.ts +102 -0
  303. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/countdown-timer.d.ts +14 -0
  304. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-editor.d.ts +21 -0
  305. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-entry.d.ts +19 -0
  306. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/custom-message.d.ts +20 -0
  307. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/daxnuts.d.ts +23 -0
  308. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/diff.d.ts +12 -0
  309. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/dynamic-border.d.ts +15 -0
  310. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/earendil-announcement.d.ts +5 -0
  311. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-editor.d.ts +22 -0
  312. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-input.d.ts +23 -0
  313. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/extension-selector.d.ts +26 -0
  314. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/first-time-setup.d.ts +25 -0
  315. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/footer.d.ts +32 -0
  316. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/index.d.ts +34 -0
  317. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/keybinding-hints.d.ts +13 -0
  318. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/login-dialog.d.ts +52 -0
  319. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/model-selector.d.ts +54 -0
  320. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/oauth-selector.d.ts +33 -0
  321. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/scoped-models-selector.d.ts +42 -0
  322. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector-search.d.ts +23 -0
  323. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/session-selector.d.ts +95 -0
  324. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/settings-selector.d.ts +77 -0
  325. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/show-images-selector.d.ts +10 -0
  326. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/skill-invocation-message.d.ts +17 -0
  327. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/status-indicator.d.ts +28 -0
  328. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/theme-selector.d.ts +11 -0
  329. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/thinking-selector.d.ts +11 -0
  330. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tool-execution.d.ts +63 -0
  331. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/tree-selector.d.ts +94 -0
  332. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/trust-selector.d.ts +23 -0
  333. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message-selector.d.ts +30 -0
  334. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/user-message.d.ts +14 -0
  335. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/components/visual-truncate.d.ts +24 -0
  336. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/interactive-mode.d.ts +394 -0
  337. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/model-search.d.ts +12 -0
  338. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme-controller.d.ts +29 -0
  339. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/interactive/theme/theme.d.ts +120 -0
  340. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/print-mode.d.ts +28 -0
  341. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/jsonl.d.ts +17 -0
  342. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-client.d.ts +246 -0
  343. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-mode.d.ts +20 -0
  344. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/modes/rpc/rpc-types.d.ts +457 -0
  345. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/package-manager-cli.d.ts +8 -0
  346. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/rpc-entry.d.ts +3 -0
  347. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/ansi.d.ts +2 -0
  348. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/changelog.d.ts +22 -0
  349. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/child-process.d.ts +18 -0
  350. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-image.d.ts +11 -0
  351. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard-native.d.ts +11 -0
  352. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/clipboard.d.ts +4 -0
  353. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/deprecation.d.ts +4 -0
  354. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/exif-orientation.d.ts +5 -0
  355. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/frontmatter.d.ts +8 -0
  356. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/fs-watch.d.ts +5 -0
  357. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/git.d.ts +26 -0
  358. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/html.d.ts +7 -0
  359. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-convert.d.ts +10 -0
  360. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-process.d.ts +18 -0
  361. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-core.d.ts +30 -0
  362. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize-worker.d.ts +2 -0
  363. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/image-resize.d.ts +16 -0
  364. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/json.d.ts +3 -0
  365. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/mime.d.ts +3 -0
  366. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/open-browser.d.ts +9 -0
  367. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/paths.d.ts +31 -0
  368. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/photon.d.ts +21 -0
  369. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/pi-user-agent.d.ts +2 -0
  370. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/shell.d.ts +31 -0
  371. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/sleep.d.ts +5 -0
  372. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/syntax-highlight.d.ts +12 -0
  373. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/tools-manager.d.ts +3 -0
  374. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/version-check.d.ts +15 -0
  375. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/utils/windows-self-update.d.ts +3 -0
  376. package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/index.mjs +1 -1
  377. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +0 -54
  378. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1053 -414
  379. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +194 -4
  380. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +59 -4
  381. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +0 -77
  382. package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +107 -6
  383. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +211 -897
  384. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +265 -375
  385. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +77 -7
  386. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +1 -1
  387. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +10 -10
  388. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +547 -30
  389. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +2112 -310
  390. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +274 -34
  391. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +1 -1
  392. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +20 -3
  393. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +9 -0
  394. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +30 -5
  395. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +2 -0
  396. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +4 -2
  397. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +1 -0
  398. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +197 -0
  399. package/dist/extensions/pi-subagents/test/unit/agent-contract.test.ts +14 -0
  400. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +1 -1
  401. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +204 -27
  402. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +444 -145
  403. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +256 -4
  404. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +9 -7
  405. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +152 -22
  406. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +29 -0
  407. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +205 -23
  408. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +579 -0
  409. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +24 -0
  410. package/dist/extensions/pi-subagents/test/unit/authority-policy.test.ts +1 -0
  411. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +3 -3
  412. package/dist/extensions/pi-subagents/test/unit/capacity-resilient-json.test.ts +155 -0
  413. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +2 -2
  414. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +7 -0
  415. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +123 -0
  416. package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +81 -3
  417. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +109 -7
  418. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  419. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +5 -2
  420. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -2
  421. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +79 -0
  422. package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +22 -0
  423. package/dist/extensions/pi-subagents/test/unit/external-job-runner.test.ts +961 -0
  424. package/dist/extensions/pi-subagents/test/unit/file-system-retry.test.ts +64 -0
  425. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +1110 -0
  426. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +1664 -0
  427. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +81 -1
  428. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +138 -11
  429. package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +41 -2
  430. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +210 -79
  431. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +95 -0
  432. package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +596 -0
  433. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  434. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +3 -5
  435. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +167 -0
  436. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +319 -18
  437. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +78 -0
  438. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +19 -5
  439. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +44 -0
  440. package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +129 -0
  441. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +231 -2
  442. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +28 -1
  443. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +106 -2
  444. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +204 -7
  445. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +59 -4
  446. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +2 -6
  447. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +34 -2
  448. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -6
  449. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +12 -3
  450. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +56 -61
  451. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +416 -1
  452. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +9 -2
  453. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +42 -0
  454. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +21 -30
  455. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +86 -11
  456. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +49 -1
  457. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +233 -5
  458. package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +28 -5
  459. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +323 -20
  460. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +66 -10
  461. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -5
  462. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +174 -0
  463. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +679 -0
  464. package/dist/extensions/pi-subagents/test/unit/scheduled-store-root.test.ts +67 -0
  465. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +55 -44
  466. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +399 -84
  467. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +13 -0
  468. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
  469. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +15 -10
  470. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +11 -7
  471. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -0
  472. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +40 -1
  473. package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +10 -2
  474. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +38 -3
  475. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +110 -12
  476. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +204 -19
  477. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +25 -0
  478. package/dist/extensions/pi-subagents/test/unit/thinking-ceiling.test.ts +73 -0
  479. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +90 -50
  480. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +1 -2
  481. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +234 -7
  482. package/dist/extensions/pi-subagents/test/unit/watch-strategy.test.ts +24 -0
  483. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +36 -0
  484. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +37 -9
  485. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +302 -0
  486. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +167 -7
  487. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +113 -0
  488. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +6 -6
  489. package/dist/extensions/ponytail/index.js +31 -25
  490. package/dist/extensions/ponytail/ponytail-config.cjs +34 -7
  491. package/dist/extensions/ponytail/ponytail-instructions.cjs +7 -9
  492. package/dist/extensions/ponytail/test/extension.test.js +7 -8
  493. package/dist/extensions/ponytail/test/helpers.test.js +1 -1
  494. package/dist/skills/brandkit/SKILL.md +799 -0
  495. package/dist/skills/design-taste-frontend/SKILL.md +1207 -0
  496. package/dist/skills/design-taste-frontend-v1/SKILL.md +227 -0
  497. package/dist/skills/full-output-enforcement/SKILL.md +50 -0
  498. package/dist/skills/gpt-taste/SKILL.md +75 -0
  499. package/dist/skills/high-end-visual-design/SKILL.md +99 -0
  500. package/dist/skills/image-to-code/SKILL.md +1229 -0
  501. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1466 -0
  502. package/dist/skills/imagegen-frontend-web/SKILL.md +988 -0
  503. package/dist/skills/industrial-brutalist-ui/SKILL.md +93 -0
  504. package/dist/skills/minimalist-ui/SKILL.md +86 -0
  505. package/dist/skills/pi-subagents/SKILL.md +18 -17
  506. package/dist/skills/pi-subagents/references/constraints-and-recipes.md +31 -9
  507. package/dist/skills/pi-subagents/references/execution-controls.md +95 -143
  508. package/dist/skills/pi-subagents/references/management-authoring-rpc.md +9 -12
  509. package/dist/skills/pi-subagents/references/multi-lane-orchestration.md +39 -0
  510. package/dist/skills/pi-subagents/references/prompting-and-roles.md +45 -94
  511. package/dist/skills/ponytail/SKILL.md +12 -8
  512. package/dist/skills/ponytail-audit/SKILL.md +0 -1
  513. package/dist/skills/ponytail-debt/SKILL.md +0 -1
  514. package/dist/skills/ponytail-gain/SKILL.md +0 -1
  515. package/dist/skills/ponytail-help/SKILL.md +4 -3
  516. package/dist/skills/ponytail-review/SKILL.md +0 -1
  517. package/dist/skills/redesign-existing-projects/SKILL.md +179 -0
  518. package/dist/skills/stitch-design-taste/SKILL.md +185 -0
  519. package/dist/skills/unlazy/CHANGELOG.md +100 -0
  520. package/dist/skills/unlazy/CONTRIBUTING.md +66 -0
  521. package/dist/skills/unlazy/LICENSE +21 -0
  522. package/dist/skills/unlazy/README.md +242 -0
  523. package/dist/skills/unlazy/SECURITY.md +70 -0
  524. package/dist/skills/unlazy/SKILL.md +100 -0
  525. package/dist/skills/unlazy/package.json +13 -0
  526. package/dist/skills/unlazy/references/dispatch.md +82 -0
  527. package/dist/skills/unlazy/references/gates.md +132 -0
  528. package/dist/skills/unlazy/references/method.md +49 -0
  529. package/dist/skills/unlazy/references/orchestration.md +99 -0
  530. package/dist/skills/unlazy/references/parallel.md +123 -0
  531. package/dist/skills/unlazy/references/token-economy.md +39 -0
  532. package/dist/skills/unlazy/research/validation-protocol.md +81 -0
  533. package/dist/skills/unlazy/scripts/dispatch-check.mjs +108 -0
  534. package/dist/skills/unlazy/scripts/gate-check.mjs +894 -0
  535. package/dist/skills/unlazy/scripts/gate-lint.mjs +178 -0
  536. package/dist/skills/unlazy/scripts/install-hooks.mjs +206 -0
  537. package/dist/skills/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  538. package/dist/skills/unlazy/scripts/lib/dispatch.mjs +283 -0
  539. package/dist/skills/unlazy/scripts/lib/gates.mjs +629 -0
  540. package/dist/skills/unlazy/scripts/lib/process-tree.mjs +161 -0
  541. package/dist/skills/unlazy/scripts/lib/regex-worker.mjs +9 -0
  542. package/dist/skills/unlazy/scripts/stop-hook.mjs +174 -0
  543. package/dist/skills/unlazy/templates/PLAN.md +65 -0
  544. package/dist/skills/unlazy/templates/gates-leaf.md +51 -0
  545. package/dist/skills/unlazy/templates/gates-node.md +50 -0
  546. package/dist/skills/unlazy/tests/contract-tests.mjs +127 -0
  547. package/dist/skills/unlazy/tests/dispatch-tests.mjs +481 -0
  548. package/dist/skills/unlazy/tests/hardening-tests.mjs +903 -0
  549. package/dist/skills/unlazy/tests/lint-tests.mjs +242 -0
  550. package/dist/skills/unlazy/tests/run-tests.mjs +554 -0
  551. package/dist/skills/unlazy/tests/self-check.mjs +165 -0
  552. package/dist/skills/unlazy/tests/stress-tests.mjs +356 -0
  553. package/dist/skills/web-design-guidelines/SKILL.md +40 -0
  554. package/docs/settings.md +0 -5
  555. package/package.json +1 -1
  556. package/dist/core/tools/schema-prune.d.ts +0 -16
  557. package/dist/core/tools/schema-prune.js +0 -39
  558. package/dist/core/tools/schema-prune.test.d.ts +0 -1
  559. package/dist/core/tools/schema-prune.test.js +0 -111
  560. package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +0 -172
  561. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +0 -55
  562. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +0 -61
  563. package/dist/extensions/pi-subagents/src/agents/task-aware-routing.ts +0 -132
  564. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +0 -181
  565. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +0 -1354
  566. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +0 -1581
  567. package/dist/extensions/pi-subagents/src/shared/env.ts +0 -16
  568. package/dist/extensions/pi-subagents/src/shared/session-lineage.ts +0 -71
  569. package/dist/extensions/pi-subagents/src/slash/inline-subagents.ts +0 -136
  570. package/dist/extensions/pi-subagents/src/workflows/workflow-auto-relaunch.ts +0 -41
  571. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +0 -466
  572. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +0 -1859
  573. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +0 -733
  574. package/dist/extensions/pi-subagents/test/unit/builtin-agent-documentation.test.ts +0 -63
  575. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +0 -326
  576. package/dist/extensions/pi-subagents/test/unit/handoff-adoption.test.ts +0 -103
  577. package/dist/extensions/pi-subagents/test/unit/inline-subagents.test.ts +0 -127
  578. package/dist/extensions/pi-subagents/test/unit/session-lineage.test.ts +0 -73
  579. package/dist/extensions/pi-subagents/test/unit/task-aware-routing.test.ts +0 -213
  580. package/dist/extensions/pi-subagents/test/unit/workflow-auto-relaunch.test.ts +0 -65
@@ -15,12 +15,12 @@ import * as os from "node:os";
15
15
  import * as path from "node:path";
16
16
  import { createEventBus, createMockPi, createTempDir, events, makeAgent, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts";
17
17
  import type { MockPi } from "../support/helpers.ts";
18
- import { deliverCheckpointDecisionRequest, deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
18
+ import { deliverInterruptRequest, deliverStopRequest, deliverTimeoutRequest } from "../../src/runs/background/control-channel.ts";
19
19
  import { waitForSubagents } from "../../src/runs/background/subagent-wait.ts";
20
20
  import { writeAtomicJson } from "../../src/shared/atomic-json.ts";
21
21
  import { CHILD_WATCHDOG_STATUS_EVENT } from "../../src/watchdog/child-status.ts";
22
22
  import { MAX_CHILD_PENDING_LINE_BYTES, MAX_CHILD_STDERR_BYTES } from "../../src/runs/shared/child-protocol.ts";
23
- import { SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION } from "../../src/shared/types.ts";
23
+ import { SUBAGENT_ASYNC_STARTED_EVENT, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, TEMP_ARTIFACTS_DIR } from "../../src/shared/types.ts";
24
24
  import { registerSubagentCapabilityCeiling } from "../../src/api/capability-ceiling.ts";
25
25
  import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
26
26
  import { discoverAgents } from "../../src/agents/agents.ts";
@@ -55,7 +55,7 @@ interface UsageBudgetState {
55
55
  interface AsyncExecutionResult {
56
56
  content: Array<{ text?: string }>;
57
57
  isError?: boolean;
58
- details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; usageBudget?: UsageBudgetState; checkpoint?: { name?: string; status?: string } };
58
+ details: { asyncId?: string; asyncDir?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; usageBudget?: UsageBudgetState };
59
59
  }
60
60
 
61
61
  interface AsyncResultPayload {
@@ -80,8 +80,7 @@ interface AsyncResultPayload {
80
80
  totalTokens?: { input: number; output: number; total: number };
81
81
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
82
82
  usageBudget?: UsageBudgetState;
83
- checkpoint?: { name?: string; status?: string };
84
- results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
83
+ results: Array<{ agent?: string; launchContractDigest?: string; launchResolvedExtensions?: LaunchResolvedExtensions; runtimeAcknowledgedExtensions?: RuntimeAcknowledgedExtensions; output?: string; outputState?: "present" | "absent" | "unknown"; success?: boolean; error?: string; protocolError?: { code?: string; stream?: string; limitBytes?: number; observedBytes?: number }; timedOut?: boolean; timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string }; stopped?: boolean; turnBudget?: { maxTurns: number; graceTurns: number; outcome: string; turnCount: number; wrapUpRequestedAtTurn?: number; terminationDeferredAtTurn?: number; exceededAtTurn?: number }; turnBudgetExceeded?: boolean; wrapUpRequested?: boolean; model?: string; attemptedModels?: string[]; modelAttempts?: Array<{ success?: boolean; error?: string }>; totalCost?: { inputTokens: number; outputTokens: number; costUsd: number }; structuredOutput?: unknown; agentContract?: { version: 1 }; execution?: { status?: string; success?: boolean; exitCode?: number }; effects?: { fileMutation?: { status?: string; expected?: boolean; attempted?: boolean; message?: string } }; intercomTarget?: string; acceptance?: { status?: string; effectiveAcceptance?: { level?: string }; childReport?: unknown; runtimeChecks?: Array<{ id?: string; status?: string; message?: string }> }; artifactPaths?: { outputPath?: string; inputPath?: string; metadataPath?: string; transcriptPath?: string }; outputSaveError?: string; metadataSaveError?: string; capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] }; capabilityAudit?: { effectiveTools?: string[]; removedTools?: string[]; extensionsDenied?: boolean } }>;
85
84
  outputs?: Record<string, { text?: string; structured?: unknown }>;
86
85
  workflowGraph?: { nodes?: Array<{ kind?: string; label?: string; phase?: string; status?: string; acceptanceStatus?: string; error?: string; outputName?: string; structured?: boolean; children?: Array<{ label?: string; outputName?: string; itemKey?: string; status?: string; acceptanceStatus?: string; error?: string }> }> };
87
86
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
@@ -112,7 +111,6 @@ interface AsyncStatusPayload {
112
111
  totalTokens?: { total: number };
113
112
  totalCost?: { inputTokens: number; outputTokens: number; costUsd: number };
114
113
  usageBudget?: UsageBudgetState;
115
- checkpoint?: { name?: string; status?: string; message?: string };
116
114
  parallelGroups?: Array<{ start: number; count: number; stepIndex: number }>;
117
115
  parallelHandoff?: { version?: number; path?: string; groupCount?: number; childCount?: number; changedPatches?: number; cleanupState?: string };
118
116
  capabilityCeiling?: { version?: number; allowedTools?: string[]; denyExtensions?: boolean; sources?: string[] };
@@ -128,6 +126,7 @@ interface AsyncStatusPayload {
128
126
  status?: string;
129
127
  exitCode?: number;
130
128
  timedOut?: boolean;
129
+ timeoutRecovery?: { changedFiles?: string[]; message?: string; warning?: string };
131
130
  error?: string;
132
131
  model?: string;
133
132
  thinking?: string;
@@ -151,6 +150,7 @@ interface AsyncStatusPayload {
151
150
  interface MockPiCallRecord {
152
151
  args?: string[];
153
152
  systemPrompts?: Array<{ mode?: string; path?: string; text?: string; error?: string }>;
153
+ requiredChildTools?: string[];
154
154
  }
155
155
 
156
156
  function writeWatchdogSettings(projectDir: string, tailMs = 120_000): void {
@@ -331,6 +331,20 @@ async function waitForAsyncResultFile(id: string, timeoutMs = 15_000): Promise<s
331
331
  return resultPath;
332
332
  }
333
333
 
334
+ async function waitForAsyncEvent(id: string, type: string, timeoutMs = 10_000): Promise<Record<string, unknown>> {
335
+ const eventsPath = path.join(ASYNC_DIR, id, "events.jsonl");
336
+ const deadline = Date.now() + timeoutMs;
337
+ while (Date.now() <= deadline) {
338
+ const event = readIfExists(eventsPath)
339
+ ?.split("\n")
340
+ .map((line) => JSON.parse(line) as Record<string, unknown>)
341
+ .find((candidate) => candidate.type === type);
342
+ if (event) return event;
343
+ await new Promise((resolve) => setTimeout(resolve, 50));
344
+ }
345
+ assert.fail(`Timed out waiting for async event '${type}': ${eventsPath}`);
346
+ }
347
+
334
348
  async function waitForAsyncState(id: string, predicate: (status: AsyncStatusPayload) => boolean, timeoutMs = 10_000): Promise<AsyncStatusPayload> {
335
349
  const statusPath = path.join(ASYNC_DIR, id, "status.json");
336
350
  const deadline = Date.now() + timeoutMs;
@@ -405,6 +419,17 @@ function readMockPiArgs(mockPi: MockPi, index: number): string[] {
405
419
  return payload.args;
406
420
  }
407
421
 
422
+ function readMockPiRequiredTools(mockPi: MockPi, index: number): string[] {
423
+ const callFile = fs.readdirSync(mockPi.dir)
424
+ .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
425
+ .sort()
426
+ .at(index);
427
+ assert.ok(callFile, `expected recorded call ${index}`);
428
+ const payload = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord;
429
+ assert.ok(Array.isArray(payload.requiredChildTools), "expected recorded required child tools");
430
+ return payload.requiredChildTools;
431
+ }
432
+
408
433
  function readMockPiArgsMatching(mockPi: MockPi, text: string): string[] {
409
434
  const callFiles = fs.readdirSync(mockPi.dir)
410
435
  .filter((name) => name.startsWith("call-") && name.endsWith(".json"))
@@ -476,6 +501,52 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
476
501
  assert.equal(typeof result, "boolean");
477
502
  });
478
503
 
504
+ it("does not persist terminal async workflow status when the result index write fails", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
505
+ const id = `async-workflow-result-index-failure-${Date.now().toString(36)}`;
506
+ const resultIndexPath = path.join(RESULTS_DIR, "result-index");
507
+ let asyncDir: string | undefined;
508
+ let resultPath: string | undefined;
509
+ let pendingPath: string | undefined;
510
+ const originalError = console.error;
511
+ try {
512
+ console.error = () => {};
513
+ fs.rmSync(resultIndexPath, { recursive: true, force: true });
514
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
515
+ fs.writeFileSync(resultIndexPath, "not a directory", "utf-8");
516
+ const executor = makeAsyncExecutor([]);
517
+ const context = makeMinimalCtx(tempDir);
518
+ context.sessionManager.getSessionId = () => "session-workflow-index";
519
+
520
+ const launch = await executor.execute(id, { workflowScript: "return 'done'", async: true }, new AbortController().signal, undefined, context);
521
+ assert.equal(launch.isError, undefined);
522
+ const asyncId = launch.details?.asyncId;
523
+ assert.ok(asyncId, "expected async workflow id");
524
+ asyncDir = path.join(ASYNC_DIR, asyncId);
525
+ resultPath = path.join(RESULTS_DIR, `${asyncId}.json`);
526
+ pendingPath = path.join(RESULTS_DIR, "result-pending", encodeURIComponent("session-workflow-index"), `${encodeURIComponent(asyncId)}.json`);
527
+
528
+ const eventsPath = path.join(asyncDir, "events.jsonl");
529
+ const deadline = Date.now() + 5_000;
530
+ let eventsText = "";
531
+ while (Date.now() <= deadline) {
532
+ eventsText = readIfExists(eventsPath) ?? "";
533
+ if (eventsText.includes("subagent.workflow.result_write_failed")) break;
534
+ await new Promise((resolve) => setTimeout(resolve, 50));
535
+ }
536
+ assert.match(eventsText, /subagent\.workflow\.result_write_failed/);
537
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
538
+ assert.equal(status.state, "running");
539
+ assert.equal(fs.existsSync(resultPath), false);
540
+ assert.equal(fs.existsSync(pendingPath), true);
541
+ } finally {
542
+ console.error = originalError;
543
+ if (asyncDir) fs.rmSync(asyncDir, { recursive: true, force: true });
544
+ if (resultPath) fs.rmSync(resultPath, { recursive: true, force: true });
545
+ if (pendingPath) fs.rmSync(pendingPath, { force: true });
546
+ fs.rmSync(resultIndexPath, { recursive: true, force: true });
547
+ }
548
+ });
549
+
479
550
  it("background parses split UTF-8 JSON and a final unterminated protocol line", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
480
551
  const line = Buffer.from(JSON.stringify(events.assistantMessage("你好 from fragmented async JSON")));
481
552
  const unicodeStart = line.indexOf(Buffer.from("你"));
@@ -487,7 +558,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
487
558
  assert.equal(payload.results[0]?.output, "你好 from fragmented async JSON");
488
559
  });
489
560
 
490
- it("persists terminal status before creating the result artifact", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
561
+ it("persists terminal status with the result artifact", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
491
562
  mockPi.onCall({ output: "completed output" });
492
563
  const id = `async-terminal-status-${Date.now().toString(36)}`;
493
564
  launchProtocolTest(id);
@@ -497,6 +568,30 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
497
568
  assert.equal(status.endedAt !== undefined, true);
498
569
  });
499
570
 
571
+ it("persists intercom detach receipts on failed async steps", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
572
+ mockPi.onCall({ output: "Detached for intercom coordination before task completion.", exitCode: -2 });
573
+ const id = `async-intercom-detach-${Date.now().toString(36)}`;
574
+ launchProtocolTest(id);
575
+ await waitForAsyncResultFile(id);
576
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
577
+ assert.equal(status.state, "failed");
578
+ assert.equal(status.error, "Step failed: worker");
579
+ assert.equal(status.steps?.[0]?.error, "Detached for intercom coordination before task completion.");
580
+ });
581
+
582
+ it("persists actionable guidance for ambient extension registration conflicts", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
583
+ mockPi.onCall({
584
+ exitCode: 1,
585
+ stderr: 'Error: Failed to load extension "/tmp/pi-mcp-adapter-clone/index.ts": Flag "--mcp-config" conflicts with /tmp/pi-mcp-adapter/index.ts',
586
+ });
587
+ const id = `async-extension-conflict-${Date.now().toString(36)}`;
588
+ launchProtocolTest(id);
589
+ const payload = await readAsyncPayload(id);
590
+ assert.equal(payload.success, false);
591
+ assert.match(payload.results[0]?.error ?? "", /loaded conflicting ambient Pi extensions/);
592
+ assert.match(payload.results[0]?.error ?? "", /"worker":\{"extensions":\[\]\}/);
593
+ });
594
+
500
595
  it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
501
596
  mockPi.onCall({ jsonl: [mockAssistantMessage("", "tool_use")], stderr: "mock child failure", exitCode: 1 });
502
597
  const id = `async-output-absent-${Date.now().toString(36)}`;
@@ -521,37 +616,50 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
521
616
  const agentName = `contract-worker-${Date.now().toString(36)}`;
522
617
  const task = "Compare the resolved launch inputs.";
523
618
  const turnBudget = { maxTurns: 2, graceTurns: 1 } as const;
619
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
620
+ const agentDir = path.join(tempDir, "agent-home");
621
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
622
+ const permissionExtDir = path.join(agentDir, "extensions", "pi-permission-system");
623
+ fs.mkdirSync(path.join(permissionExtDir, "src"), { recursive: true });
624
+ fs.writeFileSync(path.join(permissionExtDir, "src", "index.ts"), "export default () => {};", "utf-8");
625
+ fs.writeFileSync(path.join(permissionExtDir, "package.json"), JSON.stringify({ name: "test", pi: { extensions: ["./src/index.ts"] } }), "utf-8");
524
626
  const agentPath = path.join(tempDir, ".selesai", "agents", `${agentName}.md`);
525
627
  fs.mkdirSync(path.dirname(agentPath), { recursive: true });
526
- fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\n---\n`, "utf-8");
527
- const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
528
- assert.ok(discovered, "expected temporary agent definition to be discovered");
529
- const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight" });
530
- assert.equal(preflight.ok, true);
531
-
532
- mockPi.onCall({ output: "foreground contract comparison" });
533
- const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
534
- assert.equal(foreground.exitCode, 0);
535
- assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
536
-
537
- mockPi.onCall({ output: "async contract comparison" });
538
- const asyncId = `async-contract-equivalence-${Date.now().toString(36)}`;
539
- const launch = executeAsyncSingle(asyncId, {
540
- agent: agentName,
541
- task,
542
- agentConfig: discovered,
543
- ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
544
- artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
545
- shareEnabled: false,
546
- sessionRoot: path.join(tempDir, "sessions"),
547
- maxSubagentDepth: 2,
548
- acceptance: false,
549
- turnBudget,
550
- });
551
- const payload = await readAsyncPayload(asyncId);
552
- assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest);
553
- assert.equal(payload.launchContractDigest, preflight.contract.launchContractDigest);
554
- assert.equal(payload.results[0]?.launchContractDigest, preflight.contract.launchContractDigest);
628
+ fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\npermissions:\n write: ask\n---\n`, "utf-8");
629
+ try {
630
+ const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
631
+ assert.ok(discovered, "expected temporary agent definition to be discovered");
632
+ const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, turnBudget, runId: "contract-preflight" });
633
+ assert.equal(preflight.ok, true);
634
+ assert.ok(preflight.contract.tools.extensionArgs.some((entry) => entry.endsWith(path.join("pi-permission-system", "src", "index.ts"))));
635
+
636
+ mockPi.onCall({ output: "foreground contract comparison" });
637
+ const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false, turnBudget });
638
+ assert.equal(foreground.exitCode, 0);
639
+ assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
640
+
641
+ mockPi.onCall({ output: "async contract comparison" });
642
+ const asyncId = `async-contract-equivalence-${Date.now().toString(36)}`;
643
+ const launch = executeAsyncSingle(asyncId, {
644
+ agent: agentName,
645
+ task,
646
+ agentConfig: discovered,
647
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
648
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
649
+ shareEnabled: false,
650
+ sessionRoot: path.join(tempDir, "sessions"),
651
+ maxSubagentDepth: 2,
652
+ acceptance: false,
653
+ turnBudget,
654
+ });
655
+ const payload = await readAsyncPayload(asyncId);
656
+ assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest);
657
+ assert.equal(payload.launchContractDigest, preflight.contract.launchContractDigest);
658
+ assert.equal(payload.results[0]?.launchContractDigest, preflight.contract.launchContractDigest);
659
+ } finally {
660
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
661
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
662
+ }
555
663
  });
556
664
 
557
665
  it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -561,20 +669,28 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
561
669
  });
562
670
  const id = `async-launch-digest-${Date.now().toString(36)}`;
563
671
  const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
672
+ const recoveryAgentConfig = makeAgent("worker", { completionGuard: false, extensions: [privateExtension], tools: ["read"], systemPrompt: "Base prompt" });
564
673
  const launch = executeAsyncSingle(id, {
565
674
  agent: "worker",
566
675
  task: "Exercise launch digest reporting",
567
- agentConfig: makeAgent("worker", { completionGuard: false, extensions: [privateExtension] }),
676
+ agentConfig: { ...recoveryAgentConfig, tools: ["read", "intercom", "contact_supervisor"], systemPrompt: "Base prompt\n\nIntercom orchestration channel:" },
677
+ recoveryAgentConfig,
568
678
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
569
679
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
570
680
  shareEnabled: false,
571
681
  sessionRoot: path.join(tempDir, "sessions"),
572
682
  maxSubagentDepth: 2,
573
683
  acceptance: false,
684
+ context: "fork",
685
+ intercomBridge: { mode: "off" },
574
686
  });
575
687
  assert.match(launch.details.launchContractDigest ?? "", /^[a-f0-9]{64}$/);
576
- const recovery = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "recovery-descriptor.json"), "utf-8")) as { runFanoutBudget?: { rootRunId?: string; limit?: number } };
688
+ const recovery = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "recovery-descriptor.json"), "utf-8")) as { runFanoutBudget?: { rootRunId?: string; limit?: number }; context?: string; intercomBridge?: { mode?: string }; tools?: string[]; systemPrompt?: string };
577
689
  assert.deepEqual(recovery.runFanoutBudget && { rootRunId: recovery.runFanoutBudget.rootRunId, limit: recovery.runFanoutBudget.limit }, { rootRunId: id, limit: 64 });
690
+ assert.equal(recovery.context, "fork");
691
+ assert.deepEqual(recovery.intercomBridge, { mode: "off" });
692
+ assert.deepEqual(recovery.tools, ["read"]);
693
+ assert.equal(recovery.systemPrompt, "Base prompt");
578
694
  const payload = await readAsyncPayload(id);
579
695
  const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete" && candidate.runtimeAcknowledgedExtensions !== undefined);
580
696
  assert.equal(payload.launchContractDigest, launch.details.launchContractDigest);
@@ -593,90 +709,208 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
593
709
  assert.deepEqual(status.runtimeAcknowledgedExtensions, runtimeAck);
594
710
  assert.deepEqual(status.steps?.[0]?.runtimeAcknowledgedExtensions, runtimeAck);
595
711
  assert.ok(!JSON.stringify(launch.details.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
712
+ // Stale supervisor-bridge pair: the --tools allowlist passes both names
713
+ // through, but SELESAI_SUBAGENT_REQUIRED_TOOLS excludes them so the 0.50 child
714
+ // runtime cannot fail the run over the removed native intercom (#1207).
715
+ const recoveryCallArgs = readMockPiArgs(mockPi, 0);
716
+ assert.equal(recoveryCallArgs[recoveryCallArgs.indexOf("--tools") + 1], "read,intercom,contact_supervisor");
717
+ assert.deepEqual(readMockPiRequiredTools(mockPi, 0), ["read"]);
596
718
  });
597
719
 
598
- it("async chain checkpoints pause before the next child until approved", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
599
- mockPi.onCall({ output: "analysis done" });
600
- mockPi.onCall({ output: "implementation done" });
601
- const id = `async-checkpoint-approve-${Date.now().toString(36)}`;
602
- const launch = executeAsyncChain(id, {
603
- chain: [
604
- { agent: "analyst", task: "Analyze" },
605
- { checkpoint: "review", message: "Approve implementation?" },
606
- { agent: "reporter", task: "Summarize {previous}" },
607
- ],
608
- agents: [makeAgent("analyst"), makeAgent("reporter")],
720
+ it("rejects async thinking above maxThinking before child startup", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
721
+ const launch = executeAsyncSingle(`async-thinking-ceiling-${Date.now().toString(36)}`, {
722
+ agent: "worker",
723
+ task: "Use the strongest available reasoning.",
724
+ agentConfig: makeAgent("worker", { model: "mock/test-model", maxThinking: "xhigh", completionGuard: false }),
725
+ thinkingOverride: "max",
726
+ availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model" }],
727
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
728
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
729
+ shareEnabled: false,
730
+ });
731
+ assert.equal(launch.isError, true);
732
+ assert.match(launch.content[0]?.text ?? "", /max.*xhigh.*worker/);
733
+ assert.equal(mockPi.callCount(), 0);
734
+ });
735
+
736
+ it("rejects implementation workers without mutation-capable tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
737
+ const id = `async-readonly-worker-contract-${Date.now().toString(36)}`;
738
+ mockPi.onCall({ output: "should not spawn" });
739
+ const launch = executeAsyncSingle(id, {
740
+ agent: "worker",
741
+ task: "Implement the requested source fix",
742
+ agentConfig: makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] }),
743
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
744
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
745
+ shareEnabled: false,
746
+ sessionRoot: path.join(tempDir, "sessions"),
747
+ maxSubagentDepth: 2,
748
+ acceptance: false,
749
+ });
750
+
751
+ assert.equal(launch.isError, true);
752
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
753
+ assert.equal(mockPi.callCount(), 0);
754
+ });
755
+
756
+ it("lets unrestricted implementation workers spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
757
+ const id = `async-unrestricted-worker-contract-${Date.now().toString(36)}`;
758
+ mockPi.onCall({ output: "implemented" });
759
+ const launch = executeAsyncSingle(id, {
760
+ agent: "worker",
761
+ task: "Implement the requested source fix",
762
+ agentConfig: makeAgent("worker"),
609
763
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
610
764
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
611
765
  shareEnabled: false,
766
+ sessionRoot: path.join(tempDir, "sessions"),
612
767
  maxSubagentDepth: 2,
768
+ acceptance: false,
613
769
  });
614
770
 
615
- const paused = await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
616
771
  assert.equal(launch.isError, undefined);
617
- assert.equal(paused.steps?.[1]?.status, "paused");
772
+ await waitForAsyncResultFile(id, 10_000);
618
773
  assert.equal(mockPi.callCount(), 1);
774
+ });
775
+
776
+ it("lets explicit fast false opt out async external single runs from inherited fast mode", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
777
+ const agentConfig = makeAgent("external", {
778
+ fast: true,
779
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('external async fast false')"] },
780
+ } as never);
619
781
 
620
- deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "approved" });
782
+ const rejected = executeAsyncSingle(`async-external-fast-inherited-${Date.now().toString(36)}`, {
783
+ agent: "external",
784
+ task: "Run external",
785
+ agentConfig,
786
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
787
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
788
+ shareEnabled: false,
789
+ });
790
+ assert.equal(rejected.isError, true);
791
+ assert.match(rejected.content[0]?.text ?? "", /does not support: fast mode/);
792
+
793
+ const id = `async-external-fast-false-${Date.now().toString(36)}`;
794
+ const launch = executeAsyncSingle(id, {
795
+ agent: "external",
796
+ task: "Run external",
797
+ agentConfig,
798
+ fast: false,
799
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
800
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
801
+ shareEnabled: false,
802
+ });
803
+
804
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
621
805
  const payload = await readAsyncPayload(id);
622
806
  assert.equal(payload.success, true);
623
- assert.equal(payload.state, "complete");
624
- assert.equal(payload.checkpoint?.status, "approved");
625
- assert.equal(payload.results.length, 2);
626
- assert.equal(mockPi.callCount(), 2);
807
+ assert.match(payload.results[0]?.output ?? "", /external async fast false/);
808
+ assert.equal(mockPi.callCount(), 0);
627
809
  });
628
810
 
629
- it("foreground checkpoint decisions use current-session control actions", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
630
- mockPi.onCall({ output: "analysis done" });
631
- const executor = makeAsyncExecutor([makeAgent("analyst"), makeAgent("worker")]);
632
- const ctx = makeMinimalCtx(tempDir);
633
- ctx.sessionManager.getSessionId = () => "session-foreground-checkpoint";
634
- const id = `foreground-checkpoint-${Date.now().toString(36)}`;
811
+ it("lets explicit fast false opt out async external chains from inherited fast mode", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
812
+ const agent = makeAgent("external", {
813
+ fast: true,
814
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('external chain fast false')"] },
815
+ } as never);
635
816
 
636
- const paused = await executor.execute(id, {
637
- chain: [
638
- { agent: "analyst", task: "Analyze" },
639
- { checkpoint: "review", message: "Approve implementation?" },
640
- { agent: "worker", task: "Implement" },
641
- ],
642
- }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
817
+ const rejected = executeAsyncChain(`async-chain-external-fast-inherited-${Date.now().toString(36)}`, {
818
+ chain: [{ agent: "external", task: "Run external" }],
819
+ resultMode: "chain",
820
+ agents: [agent],
821
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
822
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
823
+ shareEnabled: false,
824
+ acceptance: false,
825
+ });
826
+ assert.equal(rejected.isError, true);
827
+ assert.match(rejected.content[0]?.text ?? "", /does not support: fast mode/);
643
828
 
644
- assert.equal(paused.isError, undefined);
645
- assert.equal(paused.details.checkpoint?.status, "pending");
646
- assert.equal(mockPi.callCount(), 1);
647
- const runId = (paused.details as AsyncExecutionResult["details"] & { runId?: string }).runId;
648
- assert.ok(runId);
829
+ const id = `async-chain-external-fast-false-${Date.now().toString(36)}`;
830
+ const launch = executeAsyncChain(id, {
831
+ chain: [{ agent: "external", task: "Run external" }],
832
+ resultMode: "chain",
833
+ agents: [agent],
834
+ fast: false,
835
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
836
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
837
+ shareEnabled: false,
838
+ acceptance: false,
839
+ });
649
840
 
650
- const approved = await executor.execute(`${id}-approve`, { action: "approve-checkpoint", id: runId }, new AbortController().signal, undefined, ctx) as AsyncExecutionResult;
651
- assert.equal(approved.isError, undefined);
652
- assert.equal(approved.details.checkpoint?.name, "review");
653
- assert.equal(approved.details.checkpoint?.status, "approved");
654
- assert.equal(mockPi.callCount(), 1);
841
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
842
+ const payload = await readAsyncPayload(id);
843
+ assert.equal(payload.success, true);
844
+ assert.match(payload.results[0]?.output ?? "", /external chain fast false/);
845
+ assert.equal(mockPi.callCount(), 0);
655
846
  });
656
847
 
657
- it("async chain checkpoint rejection is an explicit terminal state", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
658
- mockPi.onCall({ output: "analysis done" });
659
- mockPi.onCall({ output: "should not run" });
660
- const id = `async-checkpoint-reject-${Date.now().toString(36)}`;
661
- executeAsyncChain(id, {
848
+ it("rejects implementation workers when a capability ceiling removes mutation tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
849
+ const id = `async-ceiling-readonly-worker-contract-${Date.now().toString(36)}`;
850
+ mockPi.onCall({ output: "should not spawn" });
851
+ const launch = executeAsyncSingle(id, {
852
+ agent: "worker",
853
+ task: "Implement the requested source fix",
854
+ agentConfig: makeAgent("worker", { tools: ["read", "write"] }),
855
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
856
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
857
+ shareEnabled: false,
858
+ sessionRoot: path.join(tempDir, "sessions"),
859
+ maxSubagentDepth: 2,
860
+ acceptance: false,
861
+ capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["test"] },
862
+ });
863
+
864
+ assert.equal(launch.isError, true);
865
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
866
+ assert.equal(mockPi.callCount(), 0);
867
+ });
868
+
869
+ it("rejects workflow implementation workers without mutation-capable tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
870
+ const id = `async-workflow-readonly-worker-contract-${Date.now().toString(36)}`;
871
+ mockPi.onCall({ output: "should not spawn" });
872
+ const launch = executeAsyncChain(id, {
873
+ chain: [{ agent: "worker", task: "Implement the requested source fix" }],
874
+ resultMode: "chain",
875
+ agents: [makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] })],
876
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
877
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
878
+ shareEnabled: false,
879
+ maxSubagentDepth: 2,
880
+ acceptance: false,
881
+ });
882
+
883
+ assert.equal(launch.isError, true);
884
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
885
+ assert.equal(mockPi.callCount(), 0);
886
+ });
887
+
888
+ it("rejects workflow read-only workers after previous-output templates resolve to implementation tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
889
+ const id = `async-workflow-resolved-readonly-worker-contract-${Date.now().toString(36)}`;
890
+ mockPi.onCall({ output: "Implement the requested source fix" });
891
+ mockPi.onCall({ output: "should not spawn" });
892
+ const launch = executeAsyncChain(id, {
662
893
  chain: [
663
- { agent: "analyst", task: "Analyze" },
664
- { checkpoint: "review" },
665
- { agent: "worker", task: "Implement" },
894
+ { agent: "producer", task: "Return the next instruction" },
895
+ { agent: "worker", task: "{previous}" },
896
+ ],
897
+ resultMode: "chain",
898
+ agents: [
899
+ makeAgent("producer", { completionGuard: false }),
900
+ makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] }),
666
901
  ],
667
- agents: [makeAgent("analyst"), makeAgent("worker")],
668
902
  ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
669
903
  artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
670
904
  shareEnabled: false,
671
905
  maxSubagentDepth: 2,
906
+ acceptance: false,
672
907
  });
673
908
 
674
- await waitForAsyncState(id, (status) => status.state === "paused" && status.checkpoint?.name === "review");
675
- deliverCheckpointDecisionRequest({ asyncDir: path.join(ASYNC_DIR, id), decision: "rejected" });
676
- const payload = await readAsyncPayload(id);
909
+ assert.equal(launch.isError, undefined);
910
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
911
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
677
912
  assert.equal(payload.success, false);
678
- assert.equal(payload.state, "rejected");
679
- assert.equal(payload.checkpoint?.status, "rejected");
913
+ assert.match(payload.results[1]?.error ?? "", /no mutation-capable tools/);
680
914
  assert.equal(mockPi.callCount(), 1);
681
915
  });
682
916
 
@@ -937,7 +1171,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
937
1171
  });
938
1172
 
939
1173
  it("background treats agent_settled as a clean terminal watermark", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
940
- mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 5000 });
1174
+ mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 15_000 });
941
1175
  const id = `async-lifecycle-settled-${Date.now().toString(36)}`;
942
1176
  const startedAt = Date.now();
943
1177
  launchProtocolTest(id);
@@ -945,7 +1179,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
945
1179
  assert.equal(payload.success, true);
946
1180
  assert.equal(payload.results[0]?.error, undefined);
947
1181
  assert.equal(payload.results[0]?.output, "settled async without a terminal assistant stop");
948
- assert.ok(Date.now() - startedAt < 4000, "agent_settled should trigger bounded child cleanup");
1182
+ assert.ok(Date.now() - startedAt < 10_000, "agent_settled should trigger bounded child cleanup");
949
1183
  });
950
1184
 
951
1185
  it("keeps named output references literal in async single tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
@@ -1118,15 +1352,15 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1118
1352
  assert.equal(mockPi.callCount(), 3);
1119
1353
  });
1120
1354
 
1121
- it("marks async parallel runs that exceed timeoutMs as timed out", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1122
- mockPi.onCall({ delay: 5_000, output: "one done" });
1123
- mockPi.onCall({ delay: 5_000, output: "two done" });
1124
- const id = `async-timeout-parallel-${Date.now().toString(36)}`;
1355
+ it("journals terminal child status events for running async child stops", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "cross-process stop delivery unreliable on Windows CI" : undefined }, async () => {
1356
+ mockPi.onCall({ delay: 5_000, output: "one late" });
1357
+ mockPi.onCall({ delay: 250, output: "two done" });
1358
+ const id = `async-child-stop-parallel-${Date.now().toString(36)}`;
1125
1359
  executeAsyncChain(id, {
1126
1360
  chain: [{
1127
1361
  parallel: [
1128
1362
  { agent: "one", task: "Wait" },
1129
- { agent: "two", task: "Wait" },
1363
+ { agent: "two", task: "Finish" },
1130
1364
  ],
1131
1365
  concurrency: 2,
1132
1366
  }],
@@ -1143,28 +1377,80 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1143
1377
  },
1144
1378
  shareEnabled: false,
1145
1379
  maxSubagentDepth: 2,
1146
- timeoutMs: 1_500,
1147
1380
  });
1148
1381
 
1149
1382
  await waitForMockPiCall(mockPi, 1, 10_000);
1150
- const resultPath = await waitForAsyncResultFile(id, 8_000);
1151
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1152
- const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
1153
- assert.equal(payload.state, "failed");
1154
- assert.equal(payload.success, false);
1155
- assert.equal(payload.exitCode, 1);
1156
- assert.equal(payload.timeoutMs, 1_500);
1157
- assert.equal(payload.timedOut, true);
1158
- assert.match(payload.summary ?? "", /Subagent timed out after 1500ms\./);
1159
- assert.equal(status.state, "failed");
1160
- assert.equal(status.timeoutMs, 1_500);
1161
- assert.equal(status.timedOut, true);
1162
- assert.match(status.error ?? "", /Subagent timed out after 1500ms\./);
1163
- assert.deepEqual(status.steps?.map((step) => step.status), ["failed", "failed"]);
1164
- assert.deepEqual(status.steps?.map((step) => step.timedOut), [true, true]);
1165
- assert.deepEqual(status.steps?.map((step) => step.error), ["Subagent timed out after 1500ms.", "Subagent timed out after 1500ms."]);
1166
- assert.deepEqual(payload.results.map((result) => result.timedOut), [true, true]);
1167
- assert.equal(mockPi.callCount(), 2);
1383
+ const asyncDir = path.join(ASYNC_DIR, id);
1384
+ const statusBeforeStop = await waitForAsyncState(id, (candidate) => candidate.steps?.[0]?.status === "running" && typeof candidate.pid === "number");
1385
+ deliverStopRequest({ asyncDir, pid: statusBeforeStop.pid, source: "test", targetIndex: 0, childId: "step:0" });
1386
+
1387
+ await waitForAsyncResultFile(id, 30_000);
1388
+ const status = await waitForAsyncState(id, (candidate) => candidate.state !== "running");
1389
+ assert.equal(status.steps?.[0]?.status, "stopped");
1390
+ const childStatusEvents = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8")
1391
+ .trim()
1392
+ .split("\n")
1393
+ .map((line) => JSON.parse(line) as { type?: string; childId?: string; status?: string });
1394
+ assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "step:0" && event.status === "stopping"));
1395
+ assert.ok(childStatusEvents.some((event) => event.type === "subagent.child-status" && event.childId === "step:0" && event.status === "stopped"));
1396
+ });
1397
+
1398
+ it("marks async parallel runs that exceed timeoutMs as timed out", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1399
+ mockPi.onCall({ delay: 5_000, output: "one done" });
1400
+ mockPi.onCall({ delay: 5_000, output: "two done" });
1401
+ const repo = createRepo("pi-subagents-parallel-timeout-recovery-");
1402
+ try {
1403
+ const id = `async-timeout-parallel-${Date.now().toString(36)}`;
1404
+ executeAsyncChain(id, {
1405
+ chain: [{
1406
+ parallel: [
1407
+ { agent: "one", task: "Wait" },
1408
+ { agent: "two", task: "Wait" },
1409
+ ],
1410
+ concurrency: 2,
1411
+ }],
1412
+ resultMode: "parallel",
1413
+ agents: [makeAgent("one"), makeAgent("two")],
1414
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
1415
+ artifactConfig: {
1416
+ enabled: false,
1417
+ includeInput: false,
1418
+ includeOutput: false,
1419
+ includeJsonl: false,
1420
+ includeMetadata: false,
1421
+ cleanupDays: 7,
1422
+ },
1423
+ shareEnabled: false,
1424
+ maxSubagentDepth: 2,
1425
+ timeoutMs: 1_500,
1426
+ });
1427
+
1428
+ await waitForMockPiCall(mockPi, 1, 10_000);
1429
+ fs.writeFileSync(path.join(repo, "input.md"), "parallel partial child change\n", "utf-8");
1430
+ const resultPath = await waitForAsyncResultFile(id, 8_000);
1431
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1432
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
1433
+ assert.equal(payload.state, "failed");
1434
+ assert.equal(payload.success, false);
1435
+ assert.equal(payload.exitCode, 1);
1436
+ assert.equal(payload.timeoutMs, 1_500);
1437
+ assert.equal(payload.timedOut, true);
1438
+ assert.match(payload.summary ?? "", /Subagent timed out after 1500ms\./);
1439
+ assert.equal(status.state, "failed");
1440
+ assert.equal(status.timeoutMs, 1_500);
1441
+ assert.equal(status.timedOut, true);
1442
+ assert.match(status.error ?? "", /Subagent timed out after 1500ms\./);
1443
+ assert.deepEqual(status.steps?.map((step) => step.status), ["failed", "failed"]);
1444
+ assert.deepEqual(status.steps?.map((step) => step.timedOut), [true, true]);
1445
+ assert.deepEqual(status.steps?.map((step) => step.error), ["Subagent timed out after 1500ms.", "Subagent timed out after 1500ms."]);
1446
+ assert.deepEqual(status.steps?.map((step) => step.timeoutRecovery?.changedFiles), [["input.md"], ["input.md"]]);
1447
+ assert.deepEqual(payload.results.map((result) => result.timedOut), [true, true]);
1448
+ assert.deepEqual(payload.results.map((result) => result.timeoutRecovery?.changedFiles), [["input.md"], ["input.md"]]);
1449
+ assert.ok(payload.results.every((result) => /Recovery summary:/.test(result.output ?? "")));
1450
+ assert.equal(mockPi.callCount(), 2);
1451
+ } finally {
1452
+ fs.rmSync(repo, { recursive: true, force: true });
1453
+ }
1168
1454
  });
1169
1455
 
1170
1456
  it("enforces an agent-level timeout on an async serial child without a composite deadline", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
@@ -1193,6 +1479,241 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1193
1479
  assert.equal(payload.results[0]?.error, "Subagent timed out after 150ms.");
1194
1480
  });
1195
1481
 
1482
+ it("preserves async timeout recovery summaries in final results", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1483
+ mockPi.onCall({ delay: 5_000, output: "too late" });
1484
+ const repo = createRepo("pi-subagents-timeout-recovery-");
1485
+ try {
1486
+ const id = `async-timeout-recovery-${Date.now().toString(36)}`;
1487
+ executeAsyncChain(id, {
1488
+ chain: [{ agent: "slow", task: "Wait" }],
1489
+ agents: [makeAgent("slow")],
1490
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
1491
+ artifactConfig: {
1492
+ enabled: true,
1493
+ includeInput: false,
1494
+ includeOutput: true,
1495
+ includeJsonl: true,
1496
+ includeMetadata: true,
1497
+ cleanupDays: 7,
1498
+ },
1499
+ shareEnabled: false,
1500
+ maxSubagentDepth: 2,
1501
+ timeoutMs: 1200,
1502
+ });
1503
+
1504
+ await waitForMockPiCall(mockPi, 0, 10_000);
1505
+ fs.writeFileSync(path.join(repo, "input.md"), "partial child change\n", "utf-8");
1506
+ const payload = await readAsyncPayload(id);
1507
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
1508
+ const result = payload.results[0];
1509
+ assert.equal(result?.timedOut, true);
1510
+ assert.deepEqual(result?.timeoutRecovery?.changedFiles, ["input.md"]);
1511
+ assert.match(result?.timeoutRecovery?.message ?? "", /changed tracked files: input\.md/);
1512
+ assert.match(result?.output ?? "", /Recovery summary:/);
1513
+ assert.match(result?.output ?? "", /Warning: Inspect partial changes before retrying/);
1514
+ assert.deepEqual(status.steps?.[0]?.timeoutRecovery?.changedFiles, ["input.md"]);
1515
+ } finally {
1516
+ fs.rmSync(repo, { recursive: true, force: true });
1517
+ }
1518
+ });
1519
+
1520
+ it("kills a wedged tool at the per-tool timeout with a tool-specific error before the run-level timeout", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1521
+ // Wedge: tool_execution_start fires, then the mock holds (long delay) so
1522
+ // tool_execution_end never arrives — output would keep flowing in a real
1523
+ // wedge, which is exactly why a run-level stall detector can't see it.
1524
+ mockPi.onCall({
1525
+ steps: [
1526
+ { jsonl: [events.toolStart("bash")] },
1527
+ { delay: 30_000 }, // hold far past the per-tool budget; no tool_execution_end
1528
+ ],
1529
+ });
1530
+ const id = `async-tool-timeout-${Date.now().toString(36)}`;
1531
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "1000";
1532
+ try {
1533
+ executeAsyncChain(id, {
1534
+ chain: [{ agent: "one", task: "Wait" }],
1535
+ agents: [makeAgent("one")],
1536
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1537
+ artifactConfig: {
1538
+ enabled: false,
1539
+ includeInput: false,
1540
+ includeOutput: false,
1541
+ includeJsonl: false,
1542
+ includeMetadata: false,
1543
+ cleanupDays: 7,
1544
+ },
1545
+ shareEnabled: false,
1546
+ maxSubagentDepth: 2,
1547
+ timeoutMs: 8_000, // run-level budget is longer; the per-tool timer must fire first
1548
+ });
1549
+
1550
+ const payload = await readAsyncPayload(id);
1551
+ assert.equal(payload.state, "failed");
1552
+ assert.equal(payload.results[0]?.timedOut, true);
1553
+ assert.match(payload.results[0]?.error ?? "", /Tool 'bash' exceeded its timeout of 1000ms\./);
1554
+ } finally {
1555
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1556
+ }
1557
+ });
1558
+
1559
+ it("keeps an earlier wedged tool armed when another tool starts and ends", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1560
+ mockPi.onCall({
1561
+ steps: [
1562
+ { jsonl: [{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: {} }] },
1563
+ { delay: 50, jsonl: [
1564
+ { type: "tool_execution_start", toolCallId: "read-1", toolName: "read", args: { path: "README.md" } },
1565
+ { type: "tool_execution_end", toolCallId: "read-1", toolName: "read" },
1566
+ ] },
1567
+ { delay: 30_000 },
1568
+ ],
1569
+ });
1570
+ const id = `async-tool-timeout-overlap-${Date.now().toString(36)}`;
1571
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "1000";
1572
+ try {
1573
+ executeAsyncChain(id, {
1574
+ chain: [{ agent: "one", task: "Wait" }],
1575
+ agents: [makeAgent("one")],
1576
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1577
+ artifactConfig: {
1578
+ enabled: false,
1579
+ includeInput: false,
1580
+ includeOutput: false,
1581
+ includeJsonl: false,
1582
+ includeMetadata: false,
1583
+ cleanupDays: 7,
1584
+ },
1585
+ shareEnabled: false,
1586
+ maxSubagentDepth: 2,
1587
+ timeoutMs: 8_000,
1588
+ });
1589
+
1590
+ const payload = await readAsyncPayload(id);
1591
+ assert.equal(payload.state, "failed");
1592
+ assert.equal(payload.results[0]?.timedOut, true);
1593
+ assert.match(payload.results[0]?.error ?? "", /Tool 'bash' exceeded its timeout of 1000ms\./);
1594
+ } finally {
1595
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1596
+ }
1597
+ });
1598
+
1599
+ it("lets the shorter run-level deadline win over a per-tool timeout", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1600
+ mockPi.onCall({
1601
+ steps: [
1602
+ { jsonl: [events.toolStart("bash")] },
1603
+ { delay: 30_000 },
1604
+ ],
1605
+ });
1606
+ const id = `async-tool-timeout-run-budget-${Date.now().toString(36)}`;
1607
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "1000";
1608
+ try {
1609
+ executeAsyncChain(id, {
1610
+ chain: [{ agent: "one", task: "Wait" }],
1611
+ agents: [makeAgent("one")],
1612
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1613
+ artifactConfig: {
1614
+ enabled: false,
1615
+ includeInput: false,
1616
+ includeOutput: false,
1617
+ includeJsonl: false,
1618
+ includeMetadata: false,
1619
+ cleanupDays: 7,
1620
+ },
1621
+ shareEnabled: false,
1622
+ maxSubagentDepth: 2,
1623
+ timeoutMs: 300,
1624
+ });
1625
+
1626
+ const payload = await readAsyncPayload(id);
1627
+ assert.equal(payload.results[0]?.timedOut, true);
1628
+ assert.equal(payload.results[0]?.error, "Subagent timed out after 300ms.");
1629
+ } finally {
1630
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1631
+ }
1632
+ });
1633
+
1634
+ it("does not kill a tool that completes before the per-tool timeout", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1635
+ mockPi.onCall({
1636
+ jsonl: [
1637
+ events.toolStart("bash"),
1638
+ events.toolEnd("bash"),
1639
+ events.assistantMessage("done"),
1640
+ { type: "agent_end", willRetry: false },
1641
+ { type: "agent_settled" },
1642
+ ],
1643
+ });
1644
+ const id = `async-tool-complete-${Date.now().toString(36)}`;
1645
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "1000";
1646
+ try {
1647
+ executeAsyncChain(id, {
1648
+ chain: [{ agent: "one", task: "Done" }],
1649
+ agents: [makeAgent("one")],
1650
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1651
+ artifactConfig: {
1652
+ enabled: false,
1653
+ includeInput: false,
1654
+ includeOutput: false,
1655
+ includeJsonl: false,
1656
+ includeMetadata: false,
1657
+ cleanupDays: 7,
1658
+ },
1659
+ shareEnabled: false,
1660
+ maxSubagentDepth: 2,
1661
+ timeoutMs: 8_000,
1662
+ });
1663
+
1664
+ const payload = await readAsyncPayload(id);
1665
+ assert.equal(payload.state, "complete");
1666
+ assert.notEqual(payload.results[0]?.timedOut, true);
1667
+ } finally {
1668
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1669
+ }
1670
+ });
1671
+
1672
+ it("does not apply the per-tool timeout to supervisor tools (contact_supervisor/intercom)", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1673
+ // contact_supervisor holds ~1.5s — longer than the 1s per-tool budget — but
1674
+ // is allowlisted, so the per-tool timer must not fire.
1675
+ mockPi.onCall({
1676
+ steps: [
1677
+ {
1678
+ delay: 1500,
1679
+ jsonl: [
1680
+ events.toolStart("contact_supervisor"),
1681
+ events.toolEnd("contact_supervisor"),
1682
+ events.assistantMessage("done"),
1683
+ { type: "agent_end", willRetry: false },
1684
+ { type: "agent_settled" },
1685
+ ],
1686
+ },
1687
+ ],
1688
+ });
1689
+ const id = `async-tool-allowlist-${Date.now().toString(36)}`;
1690
+ process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS = "1000";
1691
+ try {
1692
+ executeAsyncChain(id, {
1693
+ chain: [{ agent: "one", task: "Ask" }],
1694
+ agents: [makeAgent("one")],
1695
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1696
+ artifactConfig: {
1697
+ enabled: false,
1698
+ includeInput: false,
1699
+ includeOutput: false,
1700
+ includeJsonl: false,
1701
+ includeMetadata: false,
1702
+ cleanupDays: 7,
1703
+ },
1704
+ shareEnabled: false,
1705
+ maxSubagentDepth: 2,
1706
+ timeoutMs: 8_000,
1707
+ });
1708
+
1709
+ const payload = await readAsyncPayload(id);
1710
+ assert.equal(payload.state, "complete");
1711
+ assert.notEqual(payload.results[0]?.timedOut, true);
1712
+ } finally {
1713
+ delete process.env.SELESAI_SUBAGENT_TOOL_TIMEOUT_MS;
1714
+ }
1715
+ });
1716
+
1196
1717
  it("enforces child timeouts on async parallel tasks without a composite deadline", { skip: !isAsyncAvailable() ? "jiti not available" : process.platform === "win32" ? "timeout signal delivery intermittent on Windows CI" : undefined }, async () => {
1197
1718
  mockPi.onCall({ delay: 5_000, output: "one too late" });
1198
1719
  mockPi.onCall({ delay: 5_000, output: "two too late" });
@@ -1648,6 +2169,37 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1648
2169
  assert.match(payload.results[0]?.error ?? "", /subagentOnlyExtensions/);
1649
2170
  });
1650
2171
 
2172
+ it("records blocked mutation effects when background implementation tools are missing", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2173
+ mockPi.onCall({ output: "I cannot edit because fixture_search is missing", missingTools: ["fixture_search"] });
2174
+ const id = `async-missing-implementation-tool-${Date.now().toString(36)}`;
2175
+
2176
+ executeAsyncSingle(id, {
2177
+ agent: "worker",
2178
+ task: "Implement the requested source fix",
2179
+ agentConfig: makeAgent("worker", { tools: ["read", "fixture_search"] }),
2180
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
2181
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
2182
+ shareEnabled: false,
2183
+ sessionRoot: path.join(tempDir, "sessions"),
2184
+ maxSubagentDepth: 2,
2185
+ acceptance: false,
2186
+ });
2187
+
2188
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
2189
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
2190
+ const statusPayload = await waitForAsyncState(id, (candidate) => candidate.state === "failed");
2191
+
2192
+ assert.equal(payload.success, false);
2193
+ assert.equal(payload.state, "failed");
2194
+ assert.match(payload.results[0]?.error ?? "", /requested unavailable child tools: fixture_search/);
2195
+ assert.doesNotMatch(payload.results[0]?.error ?? "", /completed without making edits/);
2196
+ assert.equal(payload.results[0]?.effects?.fileMutation?.status, "blocked");
2197
+ assert.equal(payload.results[0]?.effects?.fileMutation?.expected, true);
2198
+ assert.equal(payload.results[0]?.effects?.fileMutation?.attempted, false);
2199
+ assert.match(payload.results[0]?.effects?.fileMutation?.message ?? "", /requested unavailable child tools: fixture_search/);
2200
+ assert.equal(statusPayload.steps?.[0]?.effects?.fileMutation?.status, "blocked");
2201
+ });
2202
+
1651
2203
  it("applies agent acceptance roles to inferred async acceptance", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1652
2204
  mockPi.onCall({ output: "exploration complete" });
1653
2205
  const executor = makeAsyncExecutor([makeAgent("worker", { acceptanceRole: "read-only" })]);
@@ -1666,23 +2218,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1666
2218
  assert.equal(payload.results[0]?.acceptance?.effectiveAcceptance.level, "attested");
1667
2219
  });
1668
2220
 
1669
- it("applies agent acceptance roles to inferred async parallel acceptance", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1670
- mockPi.onCall({ output: "parallel exploration complete" });
1671
- const executor = makeAsyncExecutor([makeAgent("worker", { acceptanceRole: "read-only" })]);
1672
2221
 
1673
- const result = await executor.execute(
1674
- "async-parallel-agent-acceptance-role",
1675
- { tasks: [{ agent: "worker", task: "Explore the authentication flow" }], async: true, clarify: false },
1676
- new AbortController().signal,
1677
- undefined,
1678
- makeMinimalCtx(tempDir),
1679
- );
1680
-
1681
- const asyncId = result.details?.asyncId;
1682
- assert.ok(asyncId, "expected asyncId");
1683
- const payload = await readAsyncPayload(asyncId);
1684
- assert.equal(payload.results[0]?.acceptance?.effectiveAcceptance.level, "attested");
1685
- });
1686
2222
 
1687
2223
  it("infers async chain acceptance after expanding top-level task templates", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1688
2224
  mockPi.onCall({ output: "patched" });
@@ -1715,170 +2251,13 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1715
2251
  assert.equal(reviewPayload.results[0]?.acceptance?.effectiveAcceptance?.level, "attested");
1716
2252
  });
1717
2253
 
1718
- it("top-level async parallel conversion preserves output, reads, and progress", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1719
- fs.writeFileSync(path.join(tempDir, "input.md"), "context");
1720
- mockPi.onCall({ output: "Async top-level report" });
1721
- const executor = createSubagentExecutor!({
1722
- pi: { events: createEventBus(), getSessionName: () => undefined },
1723
- state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
1724
- config: {},
1725
- asyncByDefault: false,
1726
- tempArtifactsDir: tempDir,
1727
- getSubagentSessionRoot: () => tempDir,
1728
- expandTilde: (p: string) => p,
1729
- discoverAgents: () => ({ agents: [makeAgent("worker", { defaultProgress: true })] }),
1730
- });
1731
2254
 
1732
- const result = await executor.execute(
1733
- "async-parallel-fields",
1734
- {
1735
- tasks: [{ agent: "worker", task: "Do async work", output: "async-top-output.md", reads: ["input.md"] }],
1736
- async: true,
1737
- clarify: false,
1738
- },
1739
- new AbortController().signal,
1740
- undefined,
1741
- makeMinimalCtx(tempDir),
1742
- );
1743
-
1744
- const asyncId = result.details?.asyncId;
1745
- assert.ok(asyncId, "expected asyncId");
1746
- const resultPath = path.join(RESULTS_DIR, `${asyncId}.json`);
1747
- const statusPath = path.join(ASYNC_DIR, asyncId, "status.json");
1748
- const deadline = Date.now() + 10_000;
1749
- while (!fs.existsSync(resultPath)) {
1750
- if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
1751
- await new Promise((resolve) => setTimeout(resolve, 100));
1752
- }
1753
-
1754
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
1755
- const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
1756
- assert.equal(payload.mode, "parallel");
1757
- assert.equal(payload.sessionId, "session-123");
1758
- assert.equal(payload.results[0]?.acceptance?.status, "review-required");
1759
- assert.equal(payload.results[0]?.acceptance?.evidenceStatus, "checked");
1760
- assert.equal(status.sessionId, "session-123");
1761
- assert.equal(status.steps?.[0]?.acceptance?.status, "review-required");
1762
- assert.equal(status.steps?.[0]?.acceptance?.evidenceStatus, "checked");
1763
- const outputPath = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", asyncId, "async-top-output.md");
1764
- const outputDeadline = Date.now() + 5_000;
1765
- while (!fs.existsSync(outputPath)) {
1766
- if (Date.now() > outputDeadline) {
1767
- assert.fail(`Timed out waiting for saved output file: ${outputPath}`);
1768
- }
1769
- await new Promise((resolve) => setTimeout(resolve, 50));
1770
- }
1771
- assert.equal(fs.readFileSync(outputPath, "utf-8"), "Async top-level report");
1772
- const callFile = fs.readdirSync(mockPi.dir).find((name) => name.startsWith("call-"));
1773
- assert.ok(callFile, "expected a recorded mock pi call");
1774
- const args = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[];
1775
- const taskArg = args.at(-1) ?? "";
1776
- const progressPath = path.join(tempDir, ".pi-subagents", "artifacts", "progress", asyncId, "progress.md");
1777
- assert.ok(taskArg.includes(`[Read from: ${path.join(tempDir, "input.md")}]`));
1778
- assert.ok(taskArg.includes(`Update progress at: ${progressPath}`));
1779
- assert.ok(taskArg.includes(`Write your findings to exactly this path: ${outputPath}`));
1780
- assert.equal(fs.existsSync(progressPath), true);
1781
- assert.equal(fs.existsSync(path.join(tempDir, "progress.md")), false);
1782
- });
1783
-
1784
- for (const outputOverride of [undefined, true] as const) {
1785
- it(`async top-level parallel isolates inherited output (${outputOverride === true ? "output:true" : "omitted output"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1786
- mockPi.onCall({ matchArgIncludes: "Write the first report", output: "first async report" });
1787
- mockPi.onCall({ matchArgIncludes: "Write the second report", output: "second async report" });
1788
- const agent = makeAgent("worker", { output: "context.md", tools: ["read", "grep", "find", "ls"] });
1789
- const executor = makeAsyncExecutor([agent]);
1790
- const tasks = [
1791
- { agent: "worker", task: "Write the first report", ...(outputOverride !== undefined ? { output: outputOverride } : {}) },
1792
- { agent: "worker", task: "Write the second report", ...(outputOverride !== undefined ? { output: outputOverride } : {}) },
1793
- ];
1794
- const launch = await executor.execute(
1795
- `async-inherited-output-${outputOverride === true ? "true" : "omitted"}`,
1796
- { tasks, async: true, clarify: false },
1797
- new AbortController().signal,
1798
- undefined,
1799
- makeMinimalCtx(tempDir),
1800
- );
1801
2255
 
1802
- assert.equal(launch.isError, undefined);
1803
- const payload = await readAsyncPayload(launch.details?.asyncId as string);
1804
- assert.equal(payload.success, true);
1805
- assert.equal(payload.results[0]?.output?.split("\n\nOutput saved to:")[0], "first async report");
1806
- assert.equal(payload.results[1]?.output?.split("\n\nOutput saved to:")[0], "second async report");
1807
- const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string);
1808
- const authoritativePaths = [
1809
- path.join(outputDir, "parallel-0", "0-worker", "context.md"),
1810
- path.join(outputDir, "parallel-0", "1-worker", "context.md"),
1811
- ];
1812
- assert.equal(fs.readFileSync(authoritativePaths[0]!, "utf-8"), "first async report");
1813
- assert.equal(fs.readFileSync(authoritativePaths[1]!, "utf-8"), "second async report");
1814
- const artifactPaths = payload.results.map((result) => result.artifactPaths?.outputPath);
1815
- assert.ok(artifactPaths[0] && artifactPaths[1]);
1816
- assert.notEqual(artifactPaths[0], artifactPaths[1]);
1817
- assert.equal(fs.readFileSync(artifactPaths[0], "utf-8"), "first async report");
1818
- assert.equal(fs.readFileSync(artifactPaths[1], "utf-8"), "second async report");
1819
- const calls = fs.readdirSync(mockPi.dir).filter((name) => name.startsWith("call-")).sort();
1820
- const taskArgs = calls.map((name) => (JSON.parse(fs.readFileSync(path.join(mockPi.dir, name), "utf-8")) as MockPiCallRecord).args?.at(-1) ?? "");
1821
- const firstTask = taskArgs.find((task) => task.includes("Write the first report")) ?? "";
1822
- const secondTask = taskArgs.find((task) => task.includes("Write the second report")) ?? "";
1823
- assert.ok(firstTask.includes(path.join("parallel-0", "0-worker", "context.md")));
1824
- assert.ok(secondTask.includes(path.join("parallel-0", "1-worker", "context.md")));
1825
- for (const taskArg of [firstTask, secondTask]) {
1826
- assert.match(taskArg, /Return the complete artifact in your final response\./);
1827
- assert.match(taskArg, /Do not call contact_supervisor merely because no write-capable tool is available\./);
1828
- assert.doesNotMatch(taskArg, /Write your findings to exactly this path/);
1829
- }
1830
- });
1831
- }
1832
2256
 
1833
- it("async top-level parallel rejects duplicate explicit output paths before spawning", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1834
- const executor = makeAsyncExecutor([makeAgent("worker")]);
1835
- const result = await executor.execute(
1836
- "async-duplicate-output",
1837
- {
1838
- tasks: [
1839
- { agent: "worker", task: "Write A", output: "same.md" },
1840
- { agent: "worker", task: "Write B", output: "same.md" },
1841
- ],
1842
- async: true,
1843
- clarify: false,
1844
- },
1845
- new AbortController().signal,
1846
- undefined,
1847
- makeMinimalCtx(tempDir),
1848
- );
1849
2257
 
1850
- assert.equal(result.isError, true);
1851
- assert.match(result.content[0]?.text ?? "", /Parallel tasks 1 \(worker\) and 2 \(worker\).*same\.md/);
1852
- assert.equal(mockPi.callCount(), 0);
1853
- });
1854
2258
 
1855
- it("async top-level parallel preserves distinct explicit output destinations", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1856
- mockPi.onCall({ matchArgIncludes: "first.md", output: "first explicit report" });
1857
- mockPi.onCall({ matchArgIncludes: "second.md", output: "second explicit report" });
1858
- const executor = makeAsyncExecutor([makeAgent("worker", { output: "context.md" })]);
1859
- const launch = await executor.execute(
1860
- "async-distinct-explicit-output",
1861
- {
1862
- tasks: [
1863
- { agent: "worker", task: "Write A", output: "first.md" },
1864
- { agent: "worker", task: "Write B", output: "second.md" },
1865
- ],
1866
- async: true,
1867
- clarify: false,
1868
- },
1869
- new AbortController().signal,
1870
- undefined,
1871
- makeMinimalCtx(tempDir),
1872
- );
1873
2259
 
1874
- const payload = await readAsyncPayload(launch.details?.asyncId as string);
1875
- assert.equal(payload.success, true);
1876
- const outputDir = path.join(tempDir, ".pi-subagents", "artifacts", "outputs", launch.details?.asyncId as string);
1877
- assert.equal(fs.readFileSync(path.join(outputDir, "first.md"), "utf-8"), "first explicit report");
1878
- assert.equal(fs.readFileSync(path.join(outputDir, "second.md"), "utf-8"), "second explicit report");
1879
- assert.ok(payload.results[0]?.artifactPaths?.outputPath?.endsWith("_worker_0_output.md"));
1880
- assert.ok(payload.results[1]?.artifactPaths?.outputPath?.endsWith("_worker_1_output.md"));
1881
- });
2260
+
1882
2261
 
1883
2262
  it("async chain static parallel namespaces inherited default outputs", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1884
2263
  mockPi.onCall({ matchArgIncludes: "Write first", output: "chain first report" });
@@ -1964,46 +2343,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
1964
2343
  assert.equal(status.steps?.[0]?.acceptance?.status, "review-required");
1965
2344
  });
1966
2345
 
1967
- it("top-level async chain suppresses progress for {task} review-only tasks", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
1968
- mockPi.onCall({ output: "Async review" });
1969
- const executor = createSubagentExecutor!({
1970
- pi: { events: createEventBus(), getSessionName: () => undefined },
1971
- state: { baseCwd: tempDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
1972
- config: {},
1973
- asyncByDefault: false,
1974
- tempArtifactsDir: tempDir,
1975
- getSubagentSessionRoot: () => tempDir,
1976
- expandTilde: (p: string) => p,
1977
- discoverAgents: () => ({ agents: [makeAgent("reviewer", { defaultProgress: true })] }),
1978
- });
1979
-
1980
- const result = await executor.execute(
1981
- "async-chain-read-only-progress",
1982
- {
1983
- chain: [{ agent: "reviewer" }],
1984
- task: "Review-only. Do not edit files. Return findings.",
1985
- async: true,
1986
- clarify: false,
1987
- },
1988
- new AbortController().signal,
1989
- undefined,
1990
- makeMinimalCtx(tempDir),
1991
- );
1992
2346
 
1993
- const asyncId = result.details?.asyncId;
1994
- assert.ok(asyncId, "expected asyncId");
1995
- const resultPath = path.join(RESULTS_DIR, `${asyncId}.json`);
1996
- const deadline = Date.now() + 10_000;
1997
- while (!fs.existsSync(resultPath)) {
1998
- if (Date.now() > deadline) assert.fail(`Timed out waiting for async result file: ${resultPath}`);
1999
- await new Promise((resolve) => setTimeout(resolve, 100));
2000
- }
2001
- const callFile = fs.readdirSync(mockPi.dir).find((name) => name.startsWith("call-"));
2002
- assert.ok(callFile, "expected a recorded mock pi call");
2003
- const args = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")).args as string[];
2004
- assert.doesNotMatch(args.at(-1) ?? "", /progress\.md/);
2005
- assert.equal(fs.existsSync(path.join(tempDir, "progress.md")), false);
2006
- });
2007
2347
 
2008
2348
  it("async chains reject malformed named output references before spawning", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2009
2349
  const id = `async-malformed-output-ref-${Date.now().toString(36)}`;
@@ -2577,64 +2917,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2577
2917
  assert.match(payload.workflowGraph?.nodes?.[1]?.error ?? "", /Collected output validation failed/);
2578
2918
  });
2579
2919
 
2580
- it("top-level async worktree parallel keeps existing reads and writes output under project artifacts", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : process.platform === "win32" ? "worktree path separators unreliable on Windows CI" : undefined }, async () => {
2581
- const repoDir = createRepo("pi-subagent-async-worktree-");
2582
- try {
2583
- mockPi.onCall({ output: "Worktree report" });
2584
- const executor = createSubagentExecutor!({
2585
- pi: { events: createEventBus(), getSessionName: () => undefined },
2586
- state: { baseCwd: repoDir, currentSessionId: null, asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null },
2587
- config: {},
2588
- asyncByDefault: false,
2589
- tempArtifactsDir: repoDir,
2590
- getSubagentSessionRoot: () => repoDir,
2591
- expandTilde: (p: string) => p,
2592
- discoverAgents: () => ({ agents: [makeAgent("worker")] }),
2593
- });
2594
2920
 
2595
- const result = await executor.execute(
2596
- "async-parallel-worktree-fields",
2597
- {
2598
- tasks: [{ agent: "worker", task: "Do worktree work", output: "report.md", reads: ["input.md"] }],
2599
- async: true,
2600
- clarify: false,
2601
- worktree: true,
2602
- },
2603
- new AbortController().signal,
2604
- undefined,
2605
- makeMinimalCtx(repoDir),
2606
- );
2607
-
2608
- const asyncId = result.details?.asyncId;
2609
- assert.ok(asyncId, "expected asyncId");
2610
-
2611
- const worktreeCwd = path.join(os.tmpdir(), `pi-worktree-${asyncId}-s0-0`);
2612
- const args = await waitForMockPiArgs(mockPi, 0);
2613
- const taskArg = args.at(-1) ?? "";
2614
- assert.match(taskArg, new RegExp(`\\[Read from: ${escapeRegExp(path.join(worktreeCwd, "input.md"))}\\]`));
2615
- assert.ok(taskArg.includes(`Write your findings to exactly this path: ${path.join(repoDir, ".pi-subagents", "artifacts", "outputs", asyncId, "report.md")}`));
2616
- const resultPath = await waitForAsyncResultFile(asyncId, 90_000);
2617
- const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
2618
- const status = await waitForAsyncState(asyncId, (candidate) => candidate.state === "complete");
2619
- assert.equal(payload.parallelHandoff?.version, 1);
2620
- assert.equal(payload.parallelHandoff?.path, path.join(result.details!.asyncDir!, "handoff.json"));
2621
- assert.deepEqual(status.parallelHandoff, payload.parallelHandoff);
2622
- assert.equal(payload.parallelHandoff?.childCount, 1);
2623
- assert.equal(payload.parallelHandoff?.cleanupState, "complete");
2624
- assert.equal(fs.existsSync(worktreeCwd), false, "temporary worktree should be removed before handoff publication");
2625
- const handoff = JSON.parse(fs.readFileSync(payload.parallelHandoff!.path!, "utf-8")) as {
2626
- version: number;
2627
- groups: Array<{ children: Array<{ agent: string; status: string; patch: { path: string } }>; cleanup: { state: string } }>;
2628
- };
2629
- assert.equal(handoff.version, 1);
2630
- assert.equal(handoff.groups[0]!.children[0]!.agent, "worker");
2631
- assert.equal(handoff.groups[0]!.children[0]!.status, "completed");
2632
- assert.equal(handoff.groups[0]!.cleanup.state, "complete");
2633
- assert.equal(fs.existsSync(handoff.groups[0]!.children[0]!.patch.path), true, "patch artifact should outlive cleanup");
2634
- } finally {
2635
- removeTempDir(repoDir);
2636
- }
2637
- });
2638
2921
 
2639
2922
  it("readStatus caches ordered sweeps above 50 files and invalidates same-mtime replacements", () => {
2640
2923
  const root = createTempDir();
@@ -2771,7 +3054,14 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2771
3054
  assert.equal(payload.results[0].modelAttempts.length, 2);
2772
3055
  assert.deepEqual(payload.results[0].totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
2773
3056
  assert.deepEqual(payload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
2774
- const statusPayload = await waitForAsyncState(id, (candidate) => candidate.state === "complete" && candidate.totalCost !== undefined);
3057
+ const statusPayload = await waitForAsyncState(id, (candidate) => candidate.state === "complete"
3058
+ && candidate.lifecycleArtifactVersion !== undefined
3059
+ && candidate.totalTokens?.total !== undefined
3060
+ && candidate.totalCost !== undefined
3061
+ && candidate.steps[0]?.model !== undefined
3062
+ && candidate.steps[0]?.thinking !== undefined
3063
+ && candidate.steps[0]?.tokens?.total !== undefined
3064
+ && candidate.steps[0]?.totalCost !== undefined);
2775
3065
  assert.equal(statusPayload.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION);
2776
3066
  assert.equal(statusPayload.steps[0]?.model, "anthropic/claude-sonnet-4:low");
2777
3067
  assert.equal(statusPayload.steps[0]?.thinking, "low");
@@ -2781,9 +3071,9 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2781
3071
  assert.deepEqual(statusPayload.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
2782
3072
  const events = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line));
2783
3073
  assert.equal(events.find((event) => event.type === "subagent.run.started")?.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION);
2784
- const completed = events.find((event) => event.type === "subagent.run.completed");
2785
- assert.equal(completed?.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION);
2786
- assert.deepEqual(completed?.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
3074
+ const completed = await waitForAsyncEvent(id, "subagent.run.completed");
3075
+ assert.equal(completed.lifecycleArtifactVersion, SUBAGENT_LIFECYCLE_ARTIFACT_VERSION);
3076
+ assert.deepEqual(completed.totalCost, { inputTokens: 110, outputTokens: 55, costUsd: 0.011 });
2787
3077
  assert.match(fs.readFileSync(path.join(asyncDir, "output-0.log"), "utf-8"), /Recovered asynchronously/);
2788
3078
  assert.equal(mockPi.callCount(), 2);
2789
3079
  });
@@ -2825,6 +3115,44 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
2825
3115
  assert.equal(mockPi.callCount(), 2);
2826
3116
  });
2827
3117
 
3118
+ it("background runs fail when a configured provider-qualified model starts on a different child model", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3119
+ mockPi.onCall({ jsonl: [events.assistantMessage("wrong async provider", "openai-codex/gpt-5.6-sol")] });
3120
+ const id = `async-model-verification-${Date.now().toString(36)}`;
3121
+ executeAsyncSingle(id, {
3122
+ agent: "worker",
3123
+ task: "Do work",
3124
+ agentConfig: makeAgent("worker", { model: "opencode-go/ox-alpha-free:max" }),
3125
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3126
+ availableModels: [
3127
+ { provider: "opencode-go", id: "ox-alpha-free", fullId: "opencode-go/ox-alpha-free" },
3128
+ { provider: "openai-codex", id: "gpt-5.6-sol", fullId: "openai-codex/gpt-5.6-sol" },
3129
+ ],
3130
+ artifactConfig: {
3131
+ enabled: false,
3132
+ includeInput: false,
3133
+ includeOutput: false,
3134
+ includeJsonl: false,
3135
+ includeMetadata: false,
3136
+ cleanupDays: 7,
3137
+ },
3138
+ shareEnabled: false,
3139
+ maxSubagentDepth: 2,
3140
+ });
3141
+
3142
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf-8")) as AsyncResultPayload;
3143
+ assert.equal(payload.success, false);
3144
+ assert.equal(payload.results[0]?.model, "opencode-go/ox-alpha-free:max");
3145
+ assert.deepEqual(payload.results[0]?.attemptedModels, ["opencode-go/ox-alpha-free:max"]);
3146
+ assert.equal(payload.results[0]?.modelAttempts?.[0]?.success, false);
3147
+ assert.match(payload.results[0]?.error ?? "", /model_verification_failed/);
3148
+ assert.match(payload.results[0]?.error ?? "", /Expected 'opencode-go\/ox-alpha-free:max'/);
3149
+ assert.match(payload.results[0]?.error ?? "", /observed 'openai-codex\/gpt-5\.6-sol'/);
3150
+ assert.match(payload.results[0]?.modelAttempts?.[0]?.error ?? "", /model_verification_failed/);
3151
+ const args = readMockPiArgs(mockPi, 0);
3152
+ assert.equal(args[args.indexOf("--model") + 1], "opencode-go/ox-alpha-free:max");
3153
+ assert.equal(mockPi.callCount(), 1);
3154
+ });
3155
+
2828
3156
  it("background runs retry the fallback model when the provider stream ends without finish_reason", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
2829
3157
  mockPi.onCall({
2830
3158
  jsonl: [{
@@ -3451,6 +3779,107 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3451
3779
  assert.doesNotMatch(eventsText, /Interrupt:/);
3452
3780
  });
3453
3781
 
3782
+ it("does not use shared-cwd sibling tracked edits as parallel completion-guard proof", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3783
+ mockPi.onCall({
3784
+ matchArgIncludes: "Edit tracked file",
3785
+ delay: 50,
3786
+ writeFiles: [{ path: "input.md", content: "changed by first sibling\n" }],
3787
+ jsonl: [
3788
+ ...events.completedWrite("input.md", "changed by first sibling\n"),
3789
+ events.assistantMessage("Implemented first sibling change."),
3790
+ ],
3791
+ });
3792
+ mockPi.onCall({
3793
+ matchArgIncludes: "Implement second sibling change",
3794
+ delay: 500,
3795
+ output: "Implemented second sibling change.",
3796
+ });
3797
+ const repo = createRepo("pi-subagents-shared-cwd-mutation-guard-");
3798
+ const id = `async-parallel-shared-cwd-mutation-${Date.now().toString(36)}`;
3799
+ let runnerStarted = false;
3800
+ try {
3801
+ const launch = executeAsyncChain(id, {
3802
+ chain: [{
3803
+ parallel: [
3804
+ { agent: "first", task: "Edit tracked file" },
3805
+ { agent: "second", task: "Implement second sibling change" },
3806
+ ],
3807
+ concurrency: 2,
3808
+ }],
3809
+ resultMode: "parallel",
3810
+ agents: [makeAgent("first"), makeAgent("second")],
3811
+ ctx: { pi: { events: { emit() {} } }, cwd: repo, currentSessionId: "session-1" },
3812
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
3813
+ shareEnabled: false,
3814
+ maxSubagentDepth: 2,
3815
+ });
3816
+ runnerStarted = !launch.isError;
3817
+
3818
+ const payload = await readAsyncPayload(id);
3819
+ assert.equal(payload.results[0]?.success, true);
3820
+ assert.equal(payload.results[0]?.effects?.fileMutation?.status, "observed");
3821
+ assert.equal(payload.results[1]?.success, false);
3822
+ assert.equal(payload.results[1]?.effects?.fileMutation?.status, "missing");
3823
+ assert.equal(payload.results[1]?.effects?.fileMutation?.attempted, false);
3824
+ assert.match(payload.results[1]?.error ?? "", /completed without making edits/);
3825
+ } finally {
3826
+ if (runnerStarted) await waitForAsyncEvent(id, "subagent.run.process_terminal");
3827
+ fs.rmSync(repo, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
3828
+ }
3829
+ });
3830
+
3831
+ it("background implementation challenges keep explicit no-change reports successful", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3832
+ mockPi.onCall({ output: [
3833
+ "Kept the current implementation. No new code or test changes were made in this challenge pass.",
3834
+ "Reason: the current candidate is the smallest correct shape.",
3835
+ ].join("\n\n") });
3836
+
3837
+ const id = `async-no-change-challenge-${Date.now().toString(36)}`;
3838
+ const sessionRoot = path.join(tempDir, "sessions");
3839
+ const task = [
3840
+ "You are reviving a previous subagent conversation.",
3841
+ "",
3842
+ "Original run: source-run",
3843
+ "Original agent: worker",
3844
+ "Original session file: /tmp/source-session.jsonl",
3845
+ "",
3846
+ "Use the stored session context as background. Answer the orchestrator's follow-up below. Do not assume the original child process is still alive.",
3847
+ "",
3848
+ "Follow-up:",
3849
+ "Implementation challenge pass 1 for the accepted candidate. Reconsider it and implement any better current-scope change.",
3850
+ ].join("\n");
3851
+
3852
+ executeAsyncSingle(id, {
3853
+ agent: "worker",
3854
+ task,
3855
+ agentConfig: makeAgent("worker"),
3856
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
3857
+ artifactConfig: {
3858
+ enabled: false,
3859
+ includeInput: false,
3860
+ includeOutput: false,
3861
+ includeJsonl: false,
3862
+ includeMetadata: false,
3863
+ cleanupDays: 7,
3864
+ },
3865
+ shareEnabled: false,
3866
+ sessionRoot,
3867
+ maxSubagentDepth: 2,
3868
+ });
3869
+
3870
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
3871
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8"));
3872
+ assert.equal(payload.success, true);
3873
+ assert.equal(payload.exitCode, 0);
3874
+ assert.equal(payload.results[0].success, true);
3875
+ assert.match(String(payload.results[0].output), /Kept the current implementation/);
3876
+ assert.doesNotMatch(String(payload.results[0].error ?? ""), /completed without making edits/);
3877
+
3878
+ const eventsText = fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf-8");
3879
+ assert.doesNotMatch(eventsText, /Subagent failed: worker/);
3880
+ assert.doesNotMatch(eventsText, /"reason":"completion_guard"/);
3881
+ });
3882
+
3454
3883
  it("agent contract v1 keeps async acceptance and file-mutation effects separate from execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3455
3884
  mockPi.onCall({ output: "I’ll do that now and report back after implementing.\n```acceptance-report\n{\"criteriaSatisfied\":[{\"id\":\"criterion-1\",\"status\":\"not-satisfied\",\"evidence\":\"no proof\"}]}\n```" });
3456
3885
  const id = `async-v1-separate-${Date.now().toString(36)}`;
@@ -3651,10 +4080,105 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3651
4080
  assert.match(result.content[0]?.type === "text" ? result.content[0].text : "", /Active async run capacity exhausted: 1\/1 used/);
3652
4081
  assert.equal(fs.existsSync(rejectedAsyncDir), false);
3653
4082
  assert.equal(fs.existsSync(rejectedResultPath), false);
3654
- assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents")), false);
4083
+ assert.equal(fs.existsSync(path.join(tempDir, ".selesai", "subagents")), false);
3655
4084
  fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey("session-cap")), { recursive: true, force: true });
3656
4085
  });
3657
4086
 
4087
+ it("scheduled executor launches retain the live active session ownership", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4088
+ mockPi.onCall({ output: "Scheduled work completed" });
4089
+ const liveCwd = path.join(tempDir, "live-project");
4090
+ fs.mkdirSync(liveCwd);
4091
+ const state = {
4092
+ baseCwd: liveCwd,
4093
+ currentSessionId: "session-b",
4094
+ lastParentModel: { provider: "deepseek", id: "live-model" },
4095
+ subagentSpawns: { sessionId: "session-b", count: 1, configuredLimit: 1, granted: 1, grantHistory: [] },
4096
+ asyncJobs: new Map(),
4097
+ fleetJobs: new Map(),
4098
+ foregroundControls: new Map(),
4099
+ lastForegroundControlId: null,
4100
+ };
4101
+ const executor = createSubagentExecutor!({
4102
+ pi: { events: createEventBus(), getSessionName: () => undefined },
4103
+ state,
4104
+ config: { maxSubagentSpawnsPerSession: 1 },
4105
+ asyncByDefault: false,
4106
+ tempArtifactsDir: tempDir,
4107
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
4108
+ expandTilde: (p: string) => p,
4109
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
4110
+ });
4111
+ const retainedCtx = makeMinimalCtx(tempDir);
4112
+ retainedCtx.sessionManager.getSessionId = () => "session-a";
4113
+ retainedCtx.model = { provider: "deepseek", id: "scheduled-model" };
4114
+ const launch = await executor.executeScheduled(
4115
+ `scheduled-owner-${Date.now().toString(36)}`,
4116
+ { agent: "worker", task: "Run retained project timer", async: true, acceptance: false },
4117
+ new AbortController().signal,
4118
+ retainedCtx,
4119
+ ) as AsyncExecutionResult;
4120
+
4121
+ assert.equal(launch.isError, undefined);
4122
+ assert.ok(launch.details.asyncId);
4123
+ assert.equal(state.currentSessionId, "session-b");
4124
+ assert.equal(state.baseCwd, liveCwd);
4125
+ assert.deepEqual(state.lastParentModel, { provider: "deepseek", id: "live-model" });
4126
+ assert.deepEqual(state.subagentSpawns, { sessionId: "session-b", count: 1, configuredLimit: 1, granted: 1, grantHistory: [] });
4127
+ const blocked = await executor.executeScheduled(
4128
+ `scheduled-owner-blocked-${Date.now().toString(36)}`,
4129
+ { agent: "worker", task: "Exceed the retained owner budget", async: true, acceptance: false },
4130
+ new AbortController().signal,
4131
+ retainedCtx,
4132
+ );
4133
+ assert.equal(blocked.isError, true);
4134
+ assert.match(blocked.content[0]?.type === "text" ? blocked.content[0].text : "", /1\/1 used/);
4135
+ assert.deepEqual(state.subagentSpawns, { sessionId: "session-b", count: 1, configuredLimit: 1, granted: 1, grantHistory: [] });
4136
+ await readAsyncPayload(launch.details.asyncId);
4137
+ });
4138
+
4139
+ it("scheduled workflows keep owner status and registries isolated from the live session", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4140
+ const liveCwd = path.join(tempDir, "live-workflow-project");
4141
+ fs.mkdirSync(liveCwd);
4142
+ const state = {
4143
+ baseCwd: liveCwd,
4144
+ currentSessionId: "session-b",
4145
+ subagentSpawns: { sessionId: "session-b", count: 4, configuredLimit: 5, granted: 0, grantHistory: [] },
4146
+ asyncJobs: new Map(),
4147
+ fleetJobs: new Map(),
4148
+ foregroundControls: new Map(),
4149
+ lastForegroundControlId: null,
4150
+ };
4151
+ const executor = createSubagentExecutor!({
4152
+ pi: { events: createEventBus(), getSessionName: () => undefined },
4153
+ state,
4154
+ config: { maxSubagentSpawnsPerSession: 5 },
4155
+ asyncByDefault: false,
4156
+ tempArtifactsDir: tempDir,
4157
+ getSubagentSessionRoot: () => path.join(tempDir, "sessions"),
4158
+ expandTilde: (p: string) => p,
4159
+ discoverAgents: () => ({ agents: [makeAgent("worker")] }),
4160
+ });
4161
+ const retainedCtx = makeMinimalCtx(tempDir);
4162
+ retainedCtx.sessionManager.getSessionId = () => "session-a";
4163
+ const workflowId = `scheduled-workflow-${Date.now().toString(36)}`;
4164
+ const launch = await executor.executeScheduled(
4165
+ workflowId,
4166
+ { workflowScript: `return await runs.status("${workflowId}")`, async: true },
4167
+ new AbortController().signal,
4168
+ retainedCtx,
4169
+ ) as AsyncExecutionResult;
4170
+
4171
+ assert.equal(launch.isError, undefined);
4172
+ assert.equal(state.asyncJobs.size, 0);
4173
+ assert.equal(state.fleetJobs.size, 0);
4174
+ assert.deepEqual(state.subagentSpawns, { sessionId: "session-b", count: 4, configuredLimit: 5, granted: 0, grantHistory: [] });
4175
+ const payload = await readAsyncPayload(launch.details.asyncId);
4176
+ assert.equal(state.asyncJobs.size, 0);
4177
+ assert.equal(state.fleetJobs.size, 0);
4178
+ assert.match(payload.workflow.value.output, /Spawn budget: 0\/5 used/);
4179
+ assert.match(payload.workflow.value.output, new RegExp(workflowId));
4180
+ });
4181
+
3658
4182
  it("async executor keeps the last parent session model after continuation drops ctx.model", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
3659
4183
  mockPi.onCall({ output: "Done asynchronously" });
3660
4184
  const state = {
@@ -3698,27 +4222,7 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3698
4222
  assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3699
4223
  });
3700
4224
 
3701
- it("foreground chains keep the last parent session model after continuation drops ctx.model", { skip: !createSubagentExecutor ? "executor not available" : undefined }, async () => {
3702
- mockPi.onCall({ output: "Done foreground" });
3703
- const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
3704
- const initialCtx = makeMinimalCtx(tempDir);
3705
- initialCtx.sessionManager.getSessionId = () => "session-continued";
3706
- initialCtx.model = { provider: "deepseek", id: "deepseek-v4-flash" };
3707
- await executor.execute("prime-parent-model", { action: "list" }, new AbortController().signal, undefined, initialCtx);
3708
4225
 
3709
- const continuedCtx = makeMinimalCtx(tempDir);
3710
- continuedCtx.sessionManager.getSessionId = () => "session-continued";
3711
- const result = await executor.execute(
3712
- "continued-foreground-chain",
3713
- { chain: [{ agent: "worker", task: "Do work" }], acceptance: false },
3714
- new AbortController().signal,
3715
- undefined,
3716
- continuedCtx,
3717
- );
3718
- assert.equal(result.isError, undefined);
3719
- const args = readMockPiArgs(mockPi, 0);
3720
- assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3721
- });
3722
4226
 
3723
4227
  it("background single runs inherit the parent session model when no model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3724
4228
  mockPi.onCall({ output: "Done asynchronously" });
@@ -3757,6 +4261,74 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
3757
4261
  assert.equal(args[args.indexOf("--model") + 1], "deepseek/deepseek-v4-flash");
3758
4262
  });
3759
4263
 
4264
+ it("background forked runs inherit a parent model outside the registry", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4265
+ mockPi.onCall({ output: "Forked async work" });
4266
+ const parentSessionFile = path.join(tempDir, "parent.jsonl");
4267
+ const forkedSessionFile = path.join(tempDir, "forked.jsonl");
4268
+ const sessionHeader = JSON.stringify({ type: "session", cwd: fs.realpathSync(tempDir) });
4269
+ fs.writeFileSync(parentSessionFile, `${sessionHeader}\n`, "utf-8");
4270
+ fs.writeFileSync(forkedSessionFile, `${sessionHeader}\n`, "utf-8");
4271
+ const ctx = {
4272
+ ...makeMinimalCtx(tempDir),
4273
+ model: { provider: "gateway", id: "parent-model" },
4274
+ modelRegistry: { getAvailable: () => [{ provider: "openai", id: "gpt-5-mini" }] },
4275
+ sessionManager: {
4276
+ getSessionId: () => "session-123",
4277
+ getSessionFile: () => parentSessionFile,
4278
+ getLeafId: () => "leaf-current",
4279
+ openSession: () => ({ createBranchedSession: () => forkedSessionFile }),
4280
+ },
4281
+ };
4282
+ const launch = await makeAsyncExecutor([makeAgent("worker")]).execute(
4283
+ "forked-parent-model",
4284
+ { agent: "worker", task: "Do work", async: true, context: "fork" },
4285
+ new AbortController().signal,
4286
+ undefined,
4287
+ ctx,
4288
+ ) as AsyncExecutionResult;
4289
+ assert.ok(!launch.isError, launch.content[0]?.text);
4290
+ assert.ok(launch.details.asyncId);
4291
+ const payload = await readAsyncPayload(launch.details.asyncId);
4292
+ assert.equal(payload.results[0]?.model, "gateway/parent-model");
4293
+ });
4294
+
4295
+ it("revives an inherited parent model outside the current registry", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
4296
+ mockPi.onCall({ output: "Initial async work" });
4297
+ const sourceId = `async-revive-parent-model-${Date.now().toString(36)}`;
4298
+ const sessionFile = path.join(tempDir, "sessions", "source.jsonl");
4299
+ fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
4300
+ fs.writeFileSync(sessionFile, "", "utf-8");
4301
+ executeAsyncSingle(sourceId, {
4302
+ agent: "worker",
4303
+ task: "Initial work",
4304
+ agentConfig: makeAgent("worker"),
4305
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-123" },
4306
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
4307
+ shareEnabled: false,
4308
+ sessionRoot: path.join(tempDir, "sessions"),
4309
+ sessionFile,
4310
+ modelOverride: "gateway/parent-model",
4311
+ modelOverrideFromParent: true,
4312
+ maxSubagentDepth: 2,
4313
+ });
4314
+ await readAsyncPayload(sourceId);
4315
+ const descriptor = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, sourceId, "recovery-descriptor.json"), "utf-8"));
4316
+ assert.equal(descriptor.modelOverrideFromParent, true);
4317
+
4318
+ mockPi.onCall({ output: "Revived async work" });
4319
+ const result = await makeAsyncExecutor([makeAgent("worker")]).execute(
4320
+ "revive-parent-model",
4321
+ { action: "resume", id: sourceId, message: "Continue" },
4322
+ new AbortController().signal,
4323
+ undefined,
4324
+ makeMinimalCtx(tempDir),
4325
+ ) as AsyncExecutionResult;
4326
+ assert.ok(!result.isError, result.content[0]?.text);
4327
+ assert.ok(result.details.asyncId);
4328
+ const payload = await readAsyncPayload(result.details.asyncId);
4329
+ assert.equal(payload.results[0]?.model, "gateway/parent-model");
4330
+ });
4331
+
3760
4332
  it("background chains inherit the parent session model when no step or agent model is set", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
3761
4333
  mockPi.onCall({ output: "Done asynchronously" });
3762
4334
 
@@ -4628,6 +5200,73 @@ describe("async execution utilities", { skip: !available ? "pi packages not avai
4628
5200
  assert.equal(payload.success, true);
4629
5201
  });
4630
5202
 
5203
+ it("background open-tool attention survives an overlapping quick tool", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
5204
+ mockPi.onCall({
5205
+ steps: [
5206
+ { jsonl: [{ type: "tool_execution_start", toolCallId: "bash-1", toolName: "bash", args: { command: "sleep 2" } }] },
5207
+ { delay: 50, jsonl: [
5208
+ { type: "tool_execution_start", toolCallId: "read-1", toolName: "read", args: { path: "README.md" } },
5209
+ { type: "tool_execution_end", toolCallId: "read-1", toolName: "read" },
5210
+ ] },
5211
+ { delay: 2_000, jsonl: [
5212
+ { type: "tool_execution_end", toolCallId: "bash-1", toolName: "bash" },
5213
+ events.toolResult("bash", "done"),
5214
+ events.assistantMessage("Done"),
5215
+ ] },
5216
+ ],
5217
+ });
5218
+
5219
+ const id = `async-overlap-tool-attention-${Date.now().toString(36)}`;
5220
+ const asyncDir = path.join(ASYNC_DIR, id);
5221
+ const eventsPath = path.join(asyncDir, "events.jsonl");
5222
+ const statusPath = path.join(asyncDir, "status.json");
5223
+ const resultPath = path.join(RESULTS_DIR, `${id}.json`);
5224
+ executeAsyncSingle(id, {
5225
+ agent: "worker",
5226
+ task: "Run the command",
5227
+ agentConfig: makeAgent("worker"),
5228
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
5229
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
5230
+ shareEnabled: false,
5231
+ sessionRoot: path.join(tempDir, "sessions"),
5232
+ maxSubagentDepth: 2,
5233
+ controlConfig: {
5234
+ enabled: true,
5235
+ needsAttentionAfterMs: 999_999,
5236
+ activeNoticeAfterMs: 100,
5237
+ failedToolAttemptsBeforeAttention: 3,
5238
+ notifyOn: ["needs_attention"],
5239
+ notifyChannels: ["event", "async", "intercom"],
5240
+ },
5241
+ });
5242
+
5243
+ const deadline = Date.now() + 10_000;
5244
+ let eventText = "";
5245
+ let statusDuringEvent: AsyncStatusPayload | undefined;
5246
+ while (Date.now() < deadline) {
5247
+ if (fs.existsSync(eventsPath)) eventText = fs.readFileSync(eventsPath, "utf-8");
5248
+ if (eventText.includes('"reason":"tool_open_threshold"') && fs.existsSync(statusPath)) {
5249
+ const status = JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatusPayload;
5250
+ if (status.activityState === "needs_attention" && status.steps?.[0]?.activityState === "needs_attention") {
5251
+ statusDuringEvent = status;
5252
+ break;
5253
+ }
5254
+ }
5255
+ if (eventText.includes('"reason":"tool_open_threshold"') && fs.existsSync(resultPath)) {
5256
+ assert.fail("run completed before status.json exposed overlapping tool attention");
5257
+ }
5258
+ await new Promise((resolve) => setTimeout(resolve, 100));
5259
+ }
5260
+
5261
+ assert.match(eventText, /"type":"needs_attention"/);
5262
+ assert.match(eventText, /"reason":"tool_open_threshold"/);
5263
+ assert.match(eventText, /"currentTool":"bash"/);
5264
+ assert.ok(statusDuringEvent, "expected status.json to expose overlapping tool attention while the run is active");
5265
+ assert.equal(statusDuringEvent.currentTool, "bash");
5266
+ assert.equal(statusDuringEvent.steps?.[0]?.currentTool, "bash");
5267
+ await waitForAsyncResultFile(id);
5268
+ });
5269
+
4631
5270
  it("subagent_wait wakes when an async child is waiting on contact_supervisor", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
4632
5271
  const id = `async-supervisor-attention-${Date.now().toString(36)}`;
4633
5272
  const replyReleasePath = path.join(tempDir, `${id}.reply`);