@selesai/code 0.13.15 → 0.13.18

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 (473) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +2 -14
  3. package/dist/bun/cli.d.ts +3 -1
  4. package/dist/bun/cli.js +3 -9
  5. package/dist/bun/{register-bedrock.js → runtime-setup.js} +5 -0
  6. package/dist/bun/sandbox-env-setup.d.ts +1 -0
  7. package/dist/bun/sandbox-env-setup.js +3 -0
  8. package/dist/cli/auth-command.d.ts +23 -0
  9. package/dist/cli/auth-command.js +102 -0
  10. package/dist/cli/config-selector.js +2 -1
  11. package/dist/cli/setup.d.ts +1 -0
  12. package/dist/cli/setup.js +11 -0
  13. package/dist/cli.js +2 -14
  14. package/dist/config.d.ts +3 -2
  15. package/dist/config.js +9 -4
  16. package/dist/core/agent-session-runtime.js +15 -7
  17. package/dist/core/agent-session.d.ts +2 -6
  18. package/dist/core/agent-session.js +15 -4
  19. package/dist/core/compaction/branch-summarization.d.ts +1 -1
  20. package/dist/core/compaction/branch-summarization.js +2 -1
  21. package/dist/core/extensions/loader.js +3 -0
  22. package/dist/core/extensions/types.d.ts +16 -2
  23. package/dist/core/http-dispatcher.js +2 -0
  24. package/dist/core/keybindings.d.ts +5 -0
  25. package/dist/core/keybindings.js +4 -0
  26. package/dist/core/messages.d.ts +1 -1
  27. package/dist/core/model-config.d.ts +15 -0
  28. package/dist/core/model-config.js +3 -0
  29. package/dist/core/model-runtime.d.ts +1 -0
  30. package/dist/core/model-runtime.js +5 -2
  31. package/dist/core/provider-composer.js +8 -2
  32. package/dist/core/session-export.d.ts +3 -0
  33. package/dist/core/session-export.js +31 -0
  34. package/dist/core/session-manager.d.ts +3 -2
  35. package/dist/core/session-manager.js +40 -13
  36. package/dist/core/skills.d.ts +1 -1
  37. package/dist/core/skills.js +4 -2
  38. package/dist/core/system-prompt.js +10 -10
  39. package/dist/core/tools/bash.js +3 -117
  40. package/dist/core/tools/edit.d.ts +1 -12
  41. package/dist/core/tools/edit.js +5 -163
  42. package/dist/core/tools/find.js +4 -55
  43. package/dist/core/tools/grep.js +4 -60
  44. package/dist/core/tools/ls.js +4 -49
  45. package/dist/core/tools/read.d.ts +4 -0
  46. package/dist/core/tools/read.js +10 -122
  47. package/dist/core/tools/renderers/bash.d.ts +11 -0
  48. package/dist/core/tools/renderers/bash.js +129 -0
  49. package/dist/core/tools/renderers/edit.d.ts +22 -0
  50. package/dist/core/tools/renderers/edit.js +166 -0
  51. package/dist/core/tools/renderers/find.d.ts +9 -0
  52. package/dist/core/tools/renderers/find.js +63 -0
  53. package/dist/core/tools/renderers/grep.d.ts +9 -0
  54. package/dist/core/tools/renderers/grep.js +68 -0
  55. package/dist/core/tools/renderers/index.d.ts +27 -0
  56. package/dist/core/tools/renderers/index.js +46 -0
  57. package/dist/core/tools/renderers/ls.d.ts +9 -0
  58. package/dist/core/tools/renderers/ls.js +57 -0
  59. package/dist/core/tools/renderers/read.d.ts +10 -0
  60. package/dist/core/tools/renderers/read.js +128 -0
  61. package/dist/core/tools/renderers/write.d.ts +9 -0
  62. package/dist/core/tools/renderers/write.js +152 -0
  63. package/dist/core/tools/write.js +5 -146
  64. package/dist/extensions/capability-gateway/index.ts +12 -1
  65. package/dist/extensions/capability-gateway/integration.test.ts +23 -0
  66. package/dist/extensions/pi-hermes-memory/CHANGELOG.md +18 -0
  67. package/dist/extensions/pi-hermes-memory/README.md +16 -12
  68. package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +1 -1
  69. package/dist/extensions/pi-hermes-memory/package-lock.json +587 -62
  70. package/dist/extensions/pi-hermes-memory/package.json +4 -4
  71. package/dist/extensions/pi-hermes-memory/src/config.ts +32 -0
  72. package/dist/extensions/pi-hermes-memory/src/constants.ts +12 -2
  73. package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +109 -51
  74. package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +9 -3
  75. package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +257 -98
  76. package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +9 -2
  77. package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +5 -0
  78. package/dist/extensions/pi-hermes-memory/src/index.ts +22 -2
  79. package/dist/extensions/pi-hermes-memory/src/paths.ts +1 -1
  80. package/dist/extensions/pi-hermes-memory/src/store/db.ts +6 -0
  81. package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +58 -1
  82. package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +60 -8
  83. package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +169 -7
  84. package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +7 -21
  85. package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +134 -15
  86. package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +4 -2
  87. package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +24 -9
  88. package/dist/extensions/pi-hermes-memory/src/types.ts +11 -0
  89. package/dist/extensions/pi-hermes-memory/tests/config.test.ts +42 -0
  90. package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +159 -3
  91. package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +452 -0
  92. package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +39 -0
  93. package/dist/extensions/pi-hermes-memory/tests/integration/corruption-recovery-integration.test.ts +91 -0
  94. package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +29 -0
  95. package/dist/extensions/pi-hermes-memory/tests/store/fts-query.test.ts +116 -0
  96. package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +163 -0
  97. package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +233 -0
  98. package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +12 -0
  99. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +175 -0
  100. package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +2 -2
  101. package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +22 -0
  102. package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +37 -1
  103. package/dist/extensions/pi-intercom/CHANGELOG.md +13 -0
  104. package/dist/extensions/pi-intercom/README.md +16 -2
  105. package/dist/extensions/pi-intercom/broker/spawn.test.ts +19 -2
  106. package/dist/extensions/pi-intercom/broker/spawn.ts +4 -4
  107. package/dist/extensions/pi-intercom/index.ts +1 -1
  108. package/dist/extensions/pi-intercom/intercom.integration.test.ts +121 -2
  109. package/dist/extensions/pi-intercom/package.json +1 -1
  110. package/dist/extensions/pi-intercom/project-agent.test.ts +1 -1
  111. package/dist/extensions/pi-intercom/project-agent.ts +1 -6
  112. package/dist/extensions/pi-intercom/skills/pi-intercom/SKILL.md +7 -6
  113. package/dist/extensions/pi-rewind-hook/index.test.ts +7 -0
  114. package/dist/extensions/pi-subagents/.github/workflows/test.yml +36 -4
  115. package/dist/extensions/pi-subagents/CHANGELOG.md +120 -0
  116. package/dist/extensions/pi-subagents/README.md +3 -3
  117. package/dist/extensions/pi-subagents/agents/researcher.md +23 -13
  118. package/dist/extensions/pi-subagents/agents/reviewer.md +1 -1
  119. package/dist/extensions/pi-subagents/agents/scout.md +1 -1
  120. package/dist/extensions/pi-subagents/docs/agents.md +32 -16
  121. package/dist/extensions/pi-subagents/docs/configuration.md +41 -15
  122. package/dist/extensions/pi-subagents/docs/extension-api.md +109 -6
  123. package/dist/extensions/pi-subagents/docs/missions.md +2 -0
  124. package/dist/extensions/pi-subagents/docs/models.md +58 -1
  125. package/dist/extensions/pi-subagents/docs/observability.md +50 -12
  126. package/dist/extensions/pi-subagents/docs/tool-reference.md +17 -10
  127. package/dist/extensions/pi-subagents/docs/watchdog.md +1 -1
  128. package/dist/extensions/pi-subagents/docs/workflows.md +26 -9
  129. package/dist/extensions/pi-subagents/package-lock.json +747 -177
  130. package/dist/extensions/pi-subagents/package.json +6 -4
  131. package/dist/extensions/pi-subagents/runner-server-preload.mjs +13 -0
  132. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +2 -1
  133. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  134. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +21 -4
  135. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  136. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
  137. package/dist/extensions/pi-subagents/src/agents/advertised-agent-prompt.ts +63 -0
  138. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +61 -16
  139. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
  140. package/dist/extensions/pi-subagents/src/agents/agents.ts +8 -0
  141. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +0 -1
  142. package/dist/extensions/pi-subagents/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  143. package/dist/extensions/pi-subagents/src/api/preflight.ts +7 -4
  144. package/dist/extensions/pi-subagents/src/api/shared-types.ts +2 -2
  145. package/dist/extensions/pi-subagents/src/api/workflow-resources.ts +6 -0
  146. package/dist/extensions/pi-subagents/src/extension/config.ts +4 -2
  147. package/dist/extensions/pi-subagents/src/extension/doctor.ts +2 -10
  148. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +9 -11
  149. package/dist/extensions/pi-subagents/src/extension/index.ts +99 -10
  150. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +13 -0
  151. package/dist/extensions/pi-subagents/src/extension/rpc.ts +7 -23
  152. package/dist/extensions/pi-subagents/src/extension/schemas.ts +11 -8
  153. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +25 -7
  154. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +73 -0
  155. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +228 -136
  156. package/dist/extensions/pi-subagents/src/intercom/supervisor-ui.ts +244 -0
  157. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +37 -16
  158. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +18 -18
  159. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +51 -25
  160. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +46 -3
  161. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -3
  162. package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +9 -0
  163. package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +10 -12
  164. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -15
  165. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +40 -29
  166. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +8 -0
  167. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +79 -247
  168. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +88 -12
  169. package/dist/extensions/pi-subagents/src/runs/background/owned-process-tree.ts +6 -6
  170. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +24 -24
  171. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +96 -0
  172. package/dist/extensions/pi-subagents/src/runs/background/run-child-session.ts +642 -0
  173. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +75 -6
  174. package/dist/extensions/pi-subagents/src/runs/background/runner-aliases.ts +163 -0
  175. package/dist/extensions/pi-subagents/src/runs/background/runner-child-launch.ts +86 -0
  176. package/dist/extensions/pi-subagents/src/runs/background/runner-child-sessions.ts +31 -0
  177. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +18 -4
  178. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +3 -1
  179. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +597 -1092
  180. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +3 -0
  181. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +4 -0
  182. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +20 -17
  183. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +296 -394
  184. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +4 -0
  185. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +3 -1
  186. package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +9 -5
  187. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +630 -283
  188. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  189. package/dist/extensions/pi-subagents/src/runs/foreground/workflow-foreground-steering.ts +78 -98
  190. package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +3 -3
  191. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +16 -3
  192. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +1 -1
  193. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +47 -47
  194. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -2
  195. package/dist/extensions/pi-subagents/src/runs/shared/child-hooks.ts +174 -0
  196. package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +14 -3
  197. package/dist/extensions/pi-subagents/src/runs/shared/child-launch.ts +319 -0
  198. package/dist/extensions/pi-subagents/src/runs/shared/child-lifecycle.ts +25 -0
  199. package/dist/extensions/pi-subagents/src/runs/shared/child-runtime-config.ts +126 -0
  200. package/dist/extensions/pi-subagents/src/runs/shared/child-session.ts +373 -0
  201. package/dist/extensions/pi-subagents/src/runs/shared/child-tool-plan.ts +530 -0
  202. package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +2 -2
  203. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +2 -1
  204. package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +16 -0
  205. package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +11 -11
  206. package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +30 -20
  207. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  208. package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +84 -15
  209. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +88 -12
  210. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +37 -53
  211. package/dist/extensions/pi-subagents/src/runs/shared/nested-path.ts +1 -1
  212. package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +18 -7
  213. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +57 -12
  214. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +3 -4
  215. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +0 -11
  216. package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +4 -1
  217. package/dist/extensions/pi-subagents/src/runs/shared/readonly-drain-observation.ts +42 -0
  218. package/dist/extensions/pi-subagents/src/runs/shared/readonly-model-continuation.ts +69 -0
  219. package/dist/extensions/pi-subagents/src/runs/shared/readonly-session-evidence.ts +307 -0
  220. package/dist/extensions/pi-subagents/src/runs/shared/run-fanout-budget.ts +8 -21
  221. package/dist/extensions/pi-subagents/src/runs/shared/runtime-acknowledged-extensions.ts +3 -30
  222. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +17 -4
  223. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +2 -0
  224. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +99 -386
  225. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +18 -62
  226. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +0 -14
  227. package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +31 -9
  228. package/dist/extensions/pi-subagents/src/runs/shared/worktree-setup-command.ts +190 -0
  229. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +506 -226
  230. package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +1 -1
  231. package/dist/extensions/pi-subagents/src/shared/jsonl-writer.ts +11 -0
  232. package/dist/extensions/pi-subagents/src/shared/model-response-aliases.ts +13 -0
  233. package/dist/extensions/pi-subagents/src/shared/thinking-ceiling.ts +0 -6
  234. package/dist/extensions/pi-subagents/src/shared/types.ts +145 -89
  235. package/dist/extensions/pi-subagents/src/shared/utils.ts +13 -6
  236. package/dist/extensions/pi-subagents/src/shared/watch-strategy.ts +2 -0
  237. package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +18 -13
  238. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +0 -6
  239. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +1 -1
  240. package/dist/extensions/pi-subagents/src/tui/fleet.ts +11 -6
  241. package/dist/extensions/pi-subagents/src/tui/render.ts +277 -46
  242. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +0 -1
  243. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +12 -12
  244. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -3
  245. package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +116 -15
  246. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +15 -18
  247. package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +57 -8
  248. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +19 -19
  249. package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +3 -3
  250. package/dist/extensions/pi-subagents/src/workflows/workflow-resources.ts +97 -22
  251. package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +3 -0
  252. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +336 -21
  253. package/dist/extensions/pi-subagents/test/integration/async-execution.part-1.test.ts +1181 -0
  254. package/dist/extensions/pi-subagents/test/integration/async-execution.part-2.test.ts +2240 -0
  255. package/dist/extensions/pi-subagents/test/integration/async-execution.part-3.test.ts +2268 -0
  256. package/dist/extensions/pi-subagents/test/integration/async-execution.part-4.test.ts +1512 -0
  257. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +231 -16
  258. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +4 -18
  259. package/dist/extensions/pi-subagents/test/integration/foreign-workflow-steering.test.ts +95 -0
  260. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +9 -15
  261. package/dist/extensions/pi-subagents/test/integration/in-process-child.test.ts +374 -0
  262. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +20 -10
  263. package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +18 -6
  264. package/dist/extensions/pi-subagents/test/integration/recovered-assistant-error.test.ts +151 -0
  265. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +42 -1
  266. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +150 -25
  267. package/dist/extensions/pi-subagents/test/integration/result-publication.test.ts +354 -0
  268. package/dist/extensions/pi-subagents/test/integration/shared-cwd-terminal-evidence.test.ts +117 -0
  269. package/dist/extensions/pi-subagents/test/integration/single-execution.part-1.test.ts +3678 -0
  270. package/dist/extensions/pi-subagents/test/integration/{single-execution.test.ts → single-execution.part-2.test.ts} +500 -4207
  271. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -2
  272. package/dist/extensions/pi-subagents/test/integration/workflow-result-publication.test.ts +127 -0
  273. package/dist/extensions/pi-subagents/test/integration/workflow-steer-inbox.test.ts +365 -0
  274. package/dist/extensions/pi-subagents/test/smoke/pi085-child.ts +31 -0
  275. package/dist/extensions/pi-subagents/test/smoke/pi085-clean-install.mjs +74 -0
  276. package/dist/extensions/pi-subagents/test/smoke/pi085-extension.ts +16 -0
  277. package/dist/extensions/pi-subagents/test/support/async-execution-fixture.ts +699 -0
  278. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +418 -0
  279. package/dist/extensions/pi-subagents/test/support/foreign-workflow-steer-host.mjs +13 -0
  280. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +30 -76
  281. package/dist/extensions/pi-subagents/test/support/notify-diagnostics-fixture.ts +84 -0
  282. package/dist/extensions/pi-subagents/test/support/result-publication-capacity-preload.mjs +70 -0
  283. package/dist/extensions/pi-subagents/test/support/runner-child-session-factory.ts +17 -0
  284. package/dist/extensions/pi-subagents/test/support/single-execution-fixture.ts +390 -0
  285. package/dist/extensions/pi-subagents/test/unit/acceptance-compaction.test.ts +120 -0
  286. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +24 -3
  287. package/dist/extensions/pi-subagents/test/unit/advertised-agent-prompt.test.ts +87 -0
  288. package/dist/extensions/pi-subagents/test/unit/advertised-agent-refresh.test.ts +163 -0
  289. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +70 -31
  290. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +78 -3
  291. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +2 -8
  292. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +1 -1
  293. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +73 -66
  294. package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +51 -1
  295. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +49 -1
  296. package/dist/extensions/pi-subagents/test/unit/async-spawn-preload.test.ts +103 -0
  297. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +3 -3
  298. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-agent-allowlist.test.ts +9 -8
  299. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +10 -7
  300. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +48 -6
  301. package/dist/extensions/pi-subagents/test/unit/child-runtime-config.test.ts +90 -0
  302. package/dist/extensions/pi-subagents/test/unit/{pi-args-permission-system.test.ts → child-tool-plan-permission-system.test.ts} +48 -86
  303. package/dist/extensions/pi-subagents/test/unit/child-tool-plan.test.ts +33 -0
  304. package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +18 -18
  305. package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +9 -9
  306. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +8 -7
  307. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +225 -65
  308. package/dist/extensions/pi-subagents/test/unit/extension-bindings.test.ts +17 -22
  309. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +11 -1
  310. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +47 -3
  311. package/dist/extensions/pi-subagents/test/unit/foreign-workflow-steering.test.ts +74 -0
  312. package/dist/extensions/pi-subagents/test/unit/fork-cache-key.test.ts +23 -29
  313. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +96 -4
  314. package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +4 -4
  315. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +116 -55
  316. package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +58 -1
  317. package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +104 -3
  318. package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +10 -1
  319. package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +31 -1
  320. package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +102 -51
  321. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +196 -6
  322. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +181 -38
  323. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +62 -70
  324. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +58 -53
  325. package/dist/extensions/pi-subagents/test/unit/notify-diagnostics.test.ts +42 -0
  326. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +84 -0
  327. package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +13 -5
  328. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +21 -5
  329. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +5 -4
  330. package/dist/extensions/pi-subagents/test/unit/permissions.test.ts +2 -6
  331. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +28 -2
  332. package/dist/extensions/pi-subagents/test/unit/pi-web-session-liveness.test.ts +209 -0
  333. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +38 -1
  334. package/dist/extensions/pi-subagents/test/unit/prompt-audit-optional-peers.test.ts +43 -0
  335. package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +24 -2
  336. package/dist/extensions/pi-subagents/test/unit/readonly-drain-observation.test.ts +174 -0
  337. package/dist/extensions/pi-subagents/test/unit/readonly-model-continuation.test.ts +94 -0
  338. package/dist/extensions/pi-subagents/test/unit/readonly-session-evidence.test.ts +1329 -0
  339. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +29 -80
  340. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +11 -8
  341. package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +2 -2
  342. package/dist/extensions/pi-subagents/test/unit/retained-nested-route-tracker.test.ts +175 -0
  343. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +38 -12
  344. package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +2 -5
  345. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +89 -1
  346. package/dist/extensions/pi-subagents/test/unit/runtime-acknowledged-extensions.test.ts +2 -16
  347. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +27 -3
  348. package/dist/extensions/pi-subagents/test/unit/scheduled-supervisor-demand.test.ts +215 -0
  349. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +15 -11
  350. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +237 -1
  351. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -5
  352. package/dist/extensions/pi-subagents/test/unit/streamed-progress-bounds.test.ts +2 -2
  353. package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +14 -1
  354. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +130 -739
  355. package/dist/extensions/pi-subagents/test/unit/supervisor-ask-registration.test.ts +682 -0
  356. package/dist/extensions/pi-subagents/test/unit/supervisor-ui.test.ts +137 -0
  357. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +1 -11
  358. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +23 -3
  359. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +8 -1
  360. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +19 -1
  361. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +72 -4
  362. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +17 -3
  363. package/dist/extensions/pi-subagents/test/unit/workflow-child-summary.test.ts +34 -0
  364. package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +12 -2
  365. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +46 -6
  366. package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +3 -3
  367. package/dist/extensions/pi-subagents/test/unit/workflow-resources.test.ts +133 -1
  368. package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +53 -2
  369. package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +222 -14
  370. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +526 -85
  371. package/dist/extensions/pi-web-agent/src/backends/config.ts +1 -0
  372. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +5 -3
  373. package/dist/extensions/pi-web-agent/src/search/tokenin.ts +7 -3
  374. package/dist/extensions/pi-web-agent/src/types.ts +1 -1
  375. package/dist/extensions/pi-zentui/README.md +17 -0
  376. package/dist/extensions/pi-zentui/assets/screenshots/footer-status-hyperlink.png +0 -0
  377. package/dist/extensions/pi-zentui/docs/configuration.md +71 -2
  378. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +11 -0
  379. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +14 -0
  380. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +29 -13
  381. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +5 -4
  382. package/dist/extensions/pi-zentui/extensions/zentui/footer-text.ts +11 -0
  383. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +11 -10
  384. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +77 -3
  385. package/dist/extensions/pi-zentui/extensions/zentui/presets.ts +77 -0
  386. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +103 -2
  387. package/dist/extensions/pi-zentui/extensions/zentui/working-line-extension-segments.ts +115 -0
  388. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +87 -5
  389. package/dist/extensions/pi-zentui/package-lock.json +277 -517
  390. package/dist/extensions/pi-zentui/package.json +7 -4
  391. package/dist/extensions/pi-zentui/test/config-presets.test.ts +238 -0
  392. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +24 -1
  393. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +570 -2
  394. package/dist/extensions/pi-zentui/test/extension-status.test.ts +90 -0
  395. package/dist/extensions/pi-zentui/test/footer-hyperlink.test.ts +243 -0
  396. package/dist/extensions/pi-zentui/test/package-contents.mjs +1 -0
  397. package/dist/extensions/pi-zentui/test/presets.test.ts +121 -0
  398. package/dist/extensions/pi-zentui/test/settings-command.test.ts +205 -1
  399. package/dist/extensions/pi-zentui/test/working-line-extension-segments.test.ts +139 -0
  400. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +237 -3
  401. package/dist/extensions/pi-zentui/test/working-line.test.ts +94 -2
  402. package/dist/index.d.ts +1 -1
  403. package/dist/main.js +4 -1
  404. package/dist/modes/interactive/chat-viewport.d.ts +19 -0
  405. package/dist/modes/interactive/chat-viewport.js +27 -0
  406. package/dist/modes/interactive/components/assistant-message.d.ts +6 -2
  407. package/dist/modes/interactive/components/assistant-message.js +30 -13
  408. package/dist/modes/interactive/components/custom-editor.d.ts +10 -1
  409. package/dist/modes/interactive/components/custom-editor.js +39 -1
  410. package/dist/modes/interactive/components/first-time-setup.js +1 -1
  411. package/dist/modes/interactive/components/index.d.ts +1 -1
  412. package/dist/modes/interactive/components/markdown-transform.d.ts +2 -0
  413. package/dist/modes/interactive/components/markdown-transform.js +18 -0
  414. package/dist/modes/interactive/components/model-selector.js +19 -17
  415. package/dist/modes/interactive/components/scoped-models-selector.js +21 -23
  416. package/dist/modes/interactive/components/settings-selector.d.ts +7 -1
  417. package/dist/modes/interactive/components/settings-selector.js +128 -20
  418. package/dist/modes/interactive/components/settings-submenu.d.ts +71 -0
  419. package/dist/modes/interactive/components/settings-submenu.js +164 -0
  420. package/dist/modes/interactive/components/status-indicator.d.ts +3 -1
  421. package/dist/modes/interactive/components/status-indicator.js +10 -3
  422. package/dist/modes/interactive/components/thinking-selector.d.ts +15 -3
  423. package/dist/modes/interactive/components/thinking-selector.js +75 -18
  424. package/dist/modes/interactive/components/tool-execution.d.ts +21 -4
  425. package/dist/modes/interactive/components/tool-execution.js +35 -33
  426. package/dist/modes/interactive/components/trust-selector.js +2 -2
  427. package/dist/modes/interactive/interactive-mode.d.ts +13 -15
  428. package/dist/modes/interactive/interactive-mode.js +144 -80
  429. package/dist/modes/interactive/session-share.d.ts +15 -0
  430. package/dist/modes/interactive/session-share.js +183 -0
  431. package/dist/modes/interactive/theme/dark.json +2 -1
  432. package/dist/modes/interactive/theme/light.json +2 -1
  433. package/dist/modes/interactive/theme/theme-controller.d.ts +1 -0
  434. package/dist/modes/interactive/theme/theme-controller.js +5 -0
  435. package/dist/modes/interactive/theme/theme-json.d.ts +83 -0
  436. package/dist/modes/interactive/theme/theme-json.js +129 -0
  437. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  438. package/dist/modes/interactive/theme/theme.d.ts +14 -5
  439. package/dist/modes/interactive/theme/theme.js +18 -119
  440. package/dist/modes/interactive/tui-renderer.d.ts +20 -0
  441. package/dist/modes/interactive/tui-renderer.js +65 -0
  442. package/dist/utils/exif-orientation.js +2 -3
  443. package/dist/utils/management-http.d.ts +24 -0
  444. package/dist/utils/management-http.js +53 -0
  445. package/dist/utils/text.d.ts +7 -0
  446. package/dist/utils/text.js +8 -0
  447. package/dist/utils/tools-manager.d.ts +1 -0
  448. package/dist/utils/tools-manager.js +43 -21
  449. package/docs/docs.json +0 -4
  450. package/package.json +4 -4
  451. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +0 -415
  452. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +0 -1059
  453. package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +0 -116
  454. package/dist/extensions/pi-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  455. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +0 -224
  456. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +0 -6986
  457. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +0 -447
  458. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +0 -185
  459. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +0 -311
  460. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +0 -84
  461. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +0 -202
  462. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +0 -101
  463. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +0 -2347
  464. package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +0 -104
  465. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +0 -49
  466. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -1465
  467. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -987
  468. package/dist/skills/implanger/SKILL.md +0 -70
  469. package/dist/skills/output/SKILL.md +0 -49
  470. package/dist/skills/planger/SKILL.md +0 -167
  471. package/dist/skills/workflow/SKILL.md +0 -95
  472. package/docs/workflows.md +0 -18
  473. /package/dist/bun/{register-bedrock.d.ts → runtime-setup.d.ts} +0 -0
@@ -0,0 +1,1181 @@
1
+ /**
2
+ * Integration tests for async (background) agent execution.
3
+ *
4
+ * Tests the async support utilities: jiti availability check,
5
+ * status file reading/caching.
6
+ *
7
+ * Requires pi packages to be importable. Skips gracefully if unavailable.
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import fsDefault from "node:fs";
13
+ import * as fs from "node:fs";
14
+ import { syncBuiltinESMExports } from "node:module";
15
+ import * as path from "node:path";
16
+ import { events, makeAgent, makeMinimalCtx, resolveMockPiCallArgs } from "../support/helpers.ts";
17
+ import { registerSubagentCapabilityCeiling } from "../../src/api/capability-ceiling.ts";
18
+ import { registerWorkflowResource } from "../../src/api/workflow-resources.ts";
19
+ import type { WorkflowReceipt } from "../../src/workflows/workflow-receipt.ts";
20
+ import { resolveSubagentLaunchContract } from "../../src/api/preflight.ts";
21
+ import { discoverAgents } from "../../src/agents/agents.ts";
22
+ import { runSync } from "../../src/runs/foreground/execution.ts";
23
+ import { ACTIVE_ASYNC_CAPACITY_DIR, acquireActiveAsyncCapacity, activeAsyncCapacitySessionKey, getActiveAsyncCapacitySnapshot } from "../../src/runs/background/active-async-capacity.ts";
24
+ import { recordModelFailure } from "../../src/runs/shared/model-exclusions.ts";
25
+ import type { AsyncExecutionResult, AsyncResultPayload, AsyncStatusPayload, MockPiCallRecord } from "../support/async-execution-fixture.ts";
26
+ import {
27
+ installAsyncExecutionHooks, mockAssistantMessage, available, isAsyncAvailable,
28
+ executeAsyncSingle, executeAsyncChain, resolveTargetedAsyncRun, ASYNC_DIR,
29
+ RESULTS_DIR, createSubagentExecutor, readIfExists, waitForAsyncResultFile,
30
+ waitForAsyncState, waitForMockPiCall, readMockPiArgs, readMockPiRequiredTools,
31
+ tempDir, mockPi, makeAsyncExecutor, readAsyncPayload, launchProtocolTest,
32
+ } from "../support/async-execution-fixture.ts";
33
+
34
+ describe("async execution utilities", { skip: !available ? "pi packages not available" : undefined }, () => {
35
+ installAsyncExecutionHooks();
36
+
37
+ it("executes a registered mixed background workflow with captured grants after disposal", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
38
+ const ctx = makeMinimalCtx(tempDir);
39
+ const command = `${JSON.stringify(process.execPath)} registered-check.cjs`;
40
+ fs.writeFileSync(path.join(tempDir, "registered-check.cjs"), `require("node:fs").writeFileSync("registered-marker", "ran"); console.log("background finite check passed");`);
41
+ const definition = { name: "test.background", version: 1, resolve: () => ({
42
+ script: `const child = await runs.run("review", { agent: "reviewer", task: "Review the change" }); if (!child.ok) throw new Error("Required review failed"); return await runs.host("check", ${JSON.stringify({ kind: "command", command, timeoutMs: 5000, output: "registered-check.log" })});`,
43
+ hostCommands: [{ key: "check", command }],
44
+ }) };
45
+ const registration = registerWorkflowResource({ sessionId: ctx.sessionManager.getSessionId(), definition });
46
+ const executor = makeAsyncExecutor([makeAgent("reviewer", { completionGuard: false })]);
47
+ mockPi.onCall({ output: "Background review completed" });
48
+ try {
49
+ const pending = executor.executePublic("registered-background", { workflow: definition.name, async: true }, new AbortController().signal, undefined, ctx);
50
+ // executePublic has synchronously captured the expansion; no timing-based wait.
51
+ registration.dispose();
52
+ const missing = await executor.executePublic("disposed-background", { workflow: definition.name, async: true }, new AbortController().signal, undefined, ctx);
53
+ assert.equal(missing.isError, true);
54
+ assert.match(missing.content[0]?.text ?? "", /Unknown workflow/);
55
+ const replacement = registerWorkflowResource({ sessionId: ctx.sessionManager.getSessionId(), definition: { ...definition, resolve: () => ({ script: "return 'replacement'" }) } });
56
+ try {
57
+ const launch = await pending;
58
+ assert.equal(launch.isError, undefined, launch.content[0]?.text);
59
+ const id = launch.details?.asyncId;
60
+ assert.ok(id);
61
+ assert.equal(launch.details?.workflowReceiptPath, undefined);
62
+ const pendingStatus = await executor.executePublic("pending-receipt-status", { action: "status", id }, new AbortController().signal, undefined, ctx);
63
+ assert.equal(pendingStatus.details?.workflowReceiptPath, undefined);
64
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf8")) as AsyncResultPayload & { workflowReceipt: { path: string; receipt: WorkflowReceipt } };
65
+ assert.equal(payload.success, true, payload.error);
66
+ assert.equal((await waitForAsyncState(id, (status) => status.state === "complete")).state, "complete");
67
+ assert.equal(payload.results[0]?.output, "Background review completed");
68
+ assert.equal(mockPi.callCount(), 1);
69
+ assert.equal(fs.readFileSync(path.join(tempDir, "registered-marker"), "utf8"), "ran");
70
+ assert.match(fs.readFileSync(path.join(tempDir, "registered-check.log"), "utf8"), /background finite check passed/);
71
+ assert.equal(payload.workflowReceipt.receipt.resource?.name, definition.name);
72
+ assert.equal(payload.workflowReceipt.receipt.state, "complete");
73
+ assert.equal(payload.workflowReceipt.receipt.entries.review.agent, "reviewer");
74
+ assert.ok(payload.workflowReceipt.receipt.entries.review.latestRunId);
75
+ assert.deepEqual(payload.workflowReceipt.receipt.hostSteps?.map(({ id, state, exitCode }) => ({ id, state, exitCode })), [{ id: "check", state: "done", exitCode: 0 }]);
76
+ assert.deepEqual(JSON.parse(fs.readFileSync(payload.workflowReceipt.path, "utf8")), payload.workflowReceipt.receipt);
77
+ for (const action of ["status", "debug.run"] as const) {
78
+ const inspected = await executor.executePublic(`receipt-${action}`, { action, id }, new AbortController().signal, undefined, ctx);
79
+ assert.equal(inspected.details?.workflowReceiptPath, payload.workflowReceipt.path);
80
+ assert.ok(inspected.content[0]?.text?.includes(`Workflow receipt: ${payload.workflowReceipt.path}`));
81
+ }
82
+ } finally { replacement.dispose(); }
83
+ } finally { registration.dispose(); }
84
+ });
85
+
86
+ it("enforces the registered background command deadline and records terminal failure", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
87
+ const ctx = makeMinimalCtx(tempDir);
88
+ const command = `${JSON.stringify(process.execPath)} finite-slow-check.cjs`;
89
+ fs.writeFileSync(path.join(tempDir, "finite-slow-check.cjs"), `console.log("finite slow check started"); setTimeout(() => console.log("unexpected completion"), 10000);`);
90
+ const registration = registerWorkflowResource({ sessionId: ctx.sessionManager.getSessionId(), definition: {
91
+ name: "test.deadline", version: 1, resolve: () => ({ script: `return await runs.host("check", ${JSON.stringify({ kind: "command", command, timeoutMs: 250, output: "deadline.log" })});`, hostCommands: [{ key: "check", command }] }),
92
+ } });
93
+ try {
94
+ const launch = await makeAsyncExecutor([]).executePublic("registered-deadline", { workflow: "test.deadline", async: true }, new AbortController().signal, undefined, ctx);
95
+ assert.equal(launch.isError, undefined, launch.content[0]?.text);
96
+ const id = launch.details?.asyncId;
97
+ assert.ok(id);
98
+ const payload = JSON.parse(fs.readFileSync(await waitForAsyncResultFile(id), "utf8")) as AsyncResultPayload & { workflowReceipt: { receipt: WorkflowReceipt } };
99
+ assert.equal(payload.success, false);
100
+ assert.equal((await waitForAsyncState(id, (status) => status.state === "failed")).state, "failed");
101
+ assert.equal(payload.workflowReceipt.receipt.resource?.name, "test.deadline");
102
+ assert.equal(payload.workflowReceipt.receipt.state, "failed");
103
+ assert.deepEqual(payload.workflowReceipt.receipt.hostSteps?.map(({ state, reasonCode }) => ({ state, reasonCode })), [{ state: "error", reasonCode: "timed_out" }]);
104
+ assert.doesNotMatch(fs.readFileSync(path.join(tempDir, "deadline.log"), "utf8"), /unexpected completion/);
105
+ assert.equal(mockPi.callCount(), 0);
106
+ } finally { registration.dispose(); }
107
+ });
108
+
109
+ it("reports jiti availability as boolean", () => {
110
+ const result = isAsyncAvailable();
111
+ assert.equal(typeof result, "boolean");
112
+ });
113
+
114
+ it("does not persist terminal async workflow status when the result index write fails", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
115
+ const id = `async-workflow-result-index-failure-${Date.now().toString(36)}`;
116
+ const resultIndexPath = path.join(RESULTS_DIR, "result-index");
117
+ let asyncDir: string | undefined;
118
+ let resultPath: string | undefined;
119
+ let pendingPath: string | undefined;
120
+ const originalError = console.error;
121
+ try {
122
+ console.error = () => {};
123
+ fs.rmSync(resultIndexPath, { recursive: true, force: true });
124
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
125
+ fs.writeFileSync(resultIndexPath, "not a directory", "utf-8");
126
+ const executor = makeAsyncExecutor([]);
127
+ const context = makeMinimalCtx(tempDir);
128
+ context.sessionManager.getSessionId = () => "session-workflow-index";
129
+
130
+ const launch = await executor.execute(id, { workflowScript: "return 'done'", async: true }, new AbortController().signal, undefined, context);
131
+ assert.equal(launch.isError, undefined);
132
+ const asyncId = launch.details?.asyncId;
133
+ assert.ok(asyncId, "expected async workflow id");
134
+ asyncDir = path.join(ASYNC_DIR, asyncId);
135
+ resultPath = path.join(RESULTS_DIR, `${asyncId}.json`);
136
+ pendingPath = path.join(RESULTS_DIR, "result-pending", encodeURIComponent("session-workflow-index"), `${encodeURIComponent(asyncId)}.json`);
137
+
138
+ const eventsPath = path.join(asyncDir, "events.jsonl");
139
+ const deadline = Date.now() + 5_000;
140
+ let eventsText = "";
141
+ while (Date.now() <= deadline) {
142
+ eventsText = readIfExists(eventsPath) ?? "";
143
+ if (eventsText.includes("subagent.workflow.result_write_failed")) break;
144
+ await new Promise((resolve) => setTimeout(resolve, 50));
145
+ }
146
+ assert.match(eventsText, /subagent\.workflow\.result_write_failed/);
147
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
148
+ assert.equal(status.state, "running");
149
+ assert.equal(fs.existsSync(resultPath), false);
150
+ assert.equal(fs.existsSync(pendingPath), true);
151
+ } finally {
152
+ console.error = originalError;
153
+ if (asyncDir) fs.rmSync(asyncDir, { recursive: true, force: true });
154
+ if (resultPath) fs.rmSync(resultPath, { recursive: true, force: true });
155
+ if (pendingPath) fs.rmSync(pendingPath, { force: true });
156
+ fs.rmSync(resultIndexPath, { recursive: true, force: true });
157
+ }
158
+ });
159
+
160
+ it("summarizes result-wait evidence without exposing artifact contents", async () => {
161
+ const id = `async-wait-diagnostic-${Date.now().toString(36)}`;
162
+ const asyncDir = path.join(ASYNC_DIR, id);
163
+ fs.mkdirSync(asyncDir, { recursive: true });
164
+ try {
165
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ state: "running", steps: [{ status: "pending" }], task: "secret-prompt" }));
166
+ fs.writeFileSync(path.join(asyncDir, "runner.stdout.log"), "");
167
+ fs.mkdirSync(path.join(asyncDir, "runner.stderr.log"));
168
+ fs.writeFileSync(path.join(asyncDir, "runner-startup-proceed.json"), JSON.stringify({ token: "secret-token" }));
169
+ fs.writeFileSync(path.join(asyncDir, "events.jsonl"), "secret-event-output\n");
170
+ const checkTimeout = async (stepState: string, callCount: number) => {
171
+ await assert.rejects(waitForAsyncResultFile(id, -1), (error: unknown) => {
172
+ assert.ok(error instanceof Error);
173
+ assert.match(error.message, new RegExp(`"steps":\\["${stepState}"\\]`));
174
+ assert.ok(error.message.includes(`mock queue: readable, prompt call records=${callCount}`));
175
+ assert.match(error.message, /runner.stdout.log: empty/);
176
+ assert.match(error.message, /runner.stderr.log: unreadable \(EISDIR\)/);
177
+ assert.match(error.message, /process-terminal.json: absent/);
178
+ assert.match(error.message, /runner-startup-proceed.json: readable, \d+ bytes \(contents withheld\)/);
179
+ assert.match(error.message, /events.jsonl: readable/);
180
+ assert.doesNotMatch(error.message, /secret-/);
181
+ return true;
182
+ });
183
+ };
184
+ await checkTimeout("pending", 0);
185
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({ state: "running", steps: [{ status: "running" }] }));
186
+ fs.writeFileSync(path.join(mockPi.dir, "call-diagnostic.json"), JSON.stringify({ task: "secret-mock-prompt" }));
187
+ await checkTimeout("running", 1);
188
+ } finally {
189
+ fs.rmSync(asyncDir, { recursive: true, force: true });
190
+ }
191
+ });
192
+
193
+ it("persists terminal status with the result artifact", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
194
+ mockPi.onCall({ output: "completed output" });
195
+ const id = `async-terminal-status-${Date.now().toString(36)}`;
196
+ launchProtocolTest(id);
197
+ await waitForAsyncResultFile(id);
198
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
199
+ assert.equal(status.state, "complete");
200
+ assert.equal(status.endedAt !== undefined, true);
201
+ });
202
+
203
+ it("preserves effective thinking in the completed async result artifact", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
204
+ mockPi.onCall({ output: "completed with thinking" });
205
+ const id = `async-result-thinking-${Date.now().toString(36)}`;
206
+ const launch = executeAsyncSingle(id, {
207
+ agent: "worker",
208
+ task: "Report the configured reasoning level.",
209
+ agentConfig: makeAgent("worker", { model: "mock/test-model", thinking: "high", completionGuard: false }),
210
+ availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model" }],
211
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-result-thinking" },
212
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
213
+ shareEnabled: false,
214
+ maxSubagentDepth: 2,
215
+ acceptance: false,
216
+ });
217
+
218
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
219
+ const payload = await readAsyncPayload(id);
220
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
221
+ assert.equal(payload.success, true);
222
+ assert.equal(payload.results[0]?.thinking, "high");
223
+ assert.equal(status.steps?.[0]?.thinking, "high");
224
+ });
225
+
226
+ it("persists the bounded usage projection in async results and metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
227
+ mockPi.onCall({ output: "accounted output" });
228
+ const id = `async-usage-artifact-${Date.now().toString(36)}`;
229
+ const artifactsDir = path.join(tempDir, ".pi-subagents", "artifacts");
230
+ executeAsyncSingle(id, {
231
+ agent: "worker",
232
+ task: "Persist usage",
233
+ agentConfig: makeAgent("worker", { completionGuard: false }),
234
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-usage-artifact" },
235
+ artifactConfig: { enabled: true, includeInput: false, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 },
236
+ artifactsDir,
237
+ shareEnabled: false,
238
+ maxSubagentDepth: 2,
239
+ acceptance: false,
240
+ });
241
+
242
+ const payload = await readAsyncPayload(id);
243
+ const expectedUsage = { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.001, turns: 1 };
244
+ assert.deepEqual(payload.results[0]?.usage, expectedUsage);
245
+ const metadataPath = payload.results[0]?.artifactPaths?.metadataPath;
246
+ assert.ok(metadataPath);
247
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { usage?: unknown };
248
+ assert.deepEqual(metadata.usage, expectedUsage);
249
+ });
250
+
251
+ it("makes a launched async run immediately visible to exact status lookup", { skip: !isAsyncAvailable() || !resolveTargetedAsyncRun ? "jiti not available" : undefined }, async () => {
252
+ mockPi.onCall({ delay: 500, output: "visible async done" });
253
+ const id = `async-initial-status-${Date.now().toString(36)}`;
254
+ const launch = executeAsyncSingle(id, {
255
+ agent: "worker",
256
+ task: "Remain visible while starting",
257
+ agentConfig: makeAgent("worker", { completionGuard: false, model: "mock/test-model" }),
258
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-initial-status" },
259
+ availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model", contextWindow: 128_000 }],
260
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
261
+ shareEnabled: false,
262
+ maxSubagentDepth: 2,
263
+ });
264
+
265
+ assert.equal(launch.isError, undefined);
266
+ assert.equal(resolveTargetedAsyncRun(ASYNC_DIR, id, "session-initial-status").kind, "exact");
267
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
268
+ assert.equal(status.sessionId, "session-initial-status");
269
+ assert.equal(status.pid !== undefined, true);
270
+ assert.equal(status.steps?.[0]?.contextLimit, 128_000);
271
+ await waitForAsyncResultFile(id);
272
+ });
273
+
274
+ it("persists absent output provenance when async lifecycle text is synthetic", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
275
+ mockPi.onCall({ jsonl: [mockAssistantMessage("", "tool_use")], stderr: "mock child failure", exitCode: 1 });
276
+ const id = `async-output-absent-${Date.now().toString(36)}`;
277
+ launchProtocolTest(id);
278
+ const payload = await readAsyncPayload(id);
279
+ assert.equal(payload.success, false);
280
+ assert.equal(payload.results[0]?.outputState, "absent");
281
+ assert.match(payload.results[0]?.error ?? "", /mock child failure/);
282
+ });
283
+
284
+ it("persists present output provenance when async failure follows partial output", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
285
+ mockPi.onCall({ output: "usable partial answer", stderr: "mock post-output failure", exitCode: 1 });
286
+ const id = `async-output-present-${Date.now().toString(36)}`;
287
+ launchProtocolTest(id);
288
+ const payload = await readAsyncPayload(id);
289
+ assert.equal(payload.success, false);
290
+ assert.equal(payload.results[0]?.outputState, "present");
291
+ assert.equal(payload.results[0]?.output, "usable partial answer");
292
+ });
293
+
294
+ it("matches preflight launch digest in equivalent foreground and async execution", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
295
+ const agentName = `contract-worker-${Date.now().toString(36)}`;
296
+ const task = "Compare the resolved launch inputs.";
297
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
298
+ const agentDir = path.join(tempDir, "agent-home");
299
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
300
+ const permissionExtDir = path.join(agentDir, "extensions", "pi-permission-system");
301
+ fs.mkdirSync(path.join(permissionExtDir, "src"), { recursive: true });
302
+ fs.writeFileSync(path.join(permissionExtDir, "src", "index.ts"), "export default () => {};", "utf-8");
303
+ fs.writeFileSync(path.join(permissionExtDir, "package.json"), JSON.stringify({ name: "test", pi: { extensions: ["./src/index.ts"] } }), "utf-8");
304
+ const agentPath = path.join(tempDir, ".selesai", "agents", `${agentName}.md`);
305
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
306
+ fs.writeFileSync(agentPath, `---\nname: ${agentName}\ndescription: Contract comparison worker\npermissions:\n write: ask\n---\n`, "utf-8");
307
+ try {
308
+ const discovered = discoverAgents(tempDir).agents.find((agent) => agent.name === agentName);
309
+ assert.ok(discovered, "expected temporary agent definition to be discovered");
310
+ const preflight = await resolveSubagentLaunchContract({ agent: agentName, cwd: tempDir, task, runId: "contract-preflight" });
311
+ assert.equal(preflight.ok, true);
312
+ assert.ok(preflight.contract.tools.extensionArgs.some((entry) => entry.endsWith(path.join("pi-permission-system", "src", "index.ts"))));
313
+
314
+ mockPi.onCall({ output: "foreground contract comparison" });
315
+ const foreground = await runSync(tempDir, [discovered], agentName, task, { runId: "contract-foreground", acceptance: false });
316
+ assert.equal(foreground.exitCode, 0);
317
+ assert.equal(foreground.launchContractDigest, preflight.contract.launchContractDigest);
318
+
319
+ mockPi.onCall({ output: "async contract comparison" });
320
+ const asyncId = `async-contract-equivalence-${Date.now().toString(36)}`;
321
+ const launch = executeAsyncSingle(asyncId, {
322
+ agent: agentName,
323
+ task,
324
+ agentConfig: discovered,
325
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
326
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
327
+ shareEnabled: false,
328
+ sessionRoot: path.join(tempDir, "sessions"),
329
+ maxSubagentDepth: 2,
330
+ acceptance: false,
331
+ });
332
+ const payload = await readAsyncPayload(asyncId);
333
+ assert.equal(launch.details.launchContractDigest, preflight.contract.launchContractDigest);
334
+ assert.equal(payload.launchContractDigest, preflight.contract.launchContractDigest);
335
+ assert.equal(payload.results[0]?.launchContractDigest, preflight.contract.launchContractDigest);
336
+ } finally {
337
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
338
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
339
+ }
340
+ });
341
+
342
+ it("persists the actual launch digest in async status and result metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
343
+ mockPi.onCall({
344
+ output: "digest-bound async done",
345
+ runtimeAcknowledgedExtensions: { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 },
346
+ });
347
+ const id = `async-launch-digest-${Date.now().toString(36)}`;
348
+ const privateExtension = path.join(tempDir, "extensions", "private-extension.ts");
349
+ const recoveryAgentConfig = makeAgent("worker", { completionGuard: false, extensions: [privateExtension], tools: ["read"], systemPrompt: "Base prompt" });
350
+ const launch = executeAsyncSingle(id, {
351
+ agent: "worker",
352
+ task: "Exercise launch digest reporting",
353
+ agentConfig: { ...recoveryAgentConfig, tools: ["read", "intercom", "contact_supervisor"], systemPrompt: "Base prompt\n\nIntercom orchestration channel:" },
354
+ recoveryAgentConfig,
355
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
356
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
357
+ shareEnabled: false,
358
+ sessionRoot: path.join(tempDir, "sessions"),
359
+ maxSubagentDepth: 2,
360
+ acceptance: false,
361
+ context: "fork",
362
+ intercomBridge: { mode: "off" },
363
+ });
364
+ assert.match(launch.details.launchContractDigest ?? "", /^[a-f0-9]{64}$/);
365
+ 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 };
366
+ assert.deepEqual(recovery.runFanoutBudget && { rootRunId: recovery.runFanoutBudget.rootRunId, limit: recovery.runFanoutBudget.limit }, { rootRunId: id, limit: 64 });
367
+ assert.equal(recovery.context, "fork");
368
+ assert.deepEqual(recovery.intercomBridge, { mode: "off" });
369
+ assert.deepEqual(recovery.tools, ["read"]);
370
+ assert.equal(recovery.systemPrompt, "Base prompt");
371
+ const payload = await readAsyncPayload(id);
372
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete" && candidate.runtimeAcknowledgedExtensions !== undefined);
373
+ assert.equal(payload.launchContractDigest, launch.details.launchContractDigest);
374
+ assert.equal(payload.results[0]?.launchContractDigest, launch.details.launchContractDigest);
375
+ assert.equal(status.launchContractDigest, launch.details.launchContractDigest);
376
+ assert.equal(status.steps?.[0]?.launchContractDigest, launch.details.launchContractDigest);
377
+ assert.equal(launch.details.launchResolvedExtensions?.source, "launch-resolved");
378
+ assert.equal(launch.details.launchResolvedExtensions?.disableAmbientExtensions, true);
379
+ assert.deepEqual(payload.launchResolvedExtensions, launch.details.launchResolvedExtensions);
380
+ assert.deepEqual(payload.results[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
381
+ assert.deepEqual(status.launchResolvedExtensions, launch.details.launchResolvedExtensions);
382
+ assert.deepEqual(status.steps?.[0]?.launchResolvedExtensions, launch.details.launchResolvedExtensions);
383
+ const runtimeAck = { version: 1, source: "child-runtime", ids: ["ext.async"], omitted: 0 };
384
+ assert.deepEqual(payload.runtimeAcknowledgedExtensions, runtimeAck);
385
+ assert.deepEqual(payload.results[0]?.runtimeAcknowledgedExtensions, runtimeAck);
386
+ assert.deepEqual(status.runtimeAcknowledgedExtensions, runtimeAck);
387
+ assert.deepEqual(status.steps?.[0]?.runtimeAcknowledgedExtensions, runtimeAck);
388
+ assert.ok(!JSON.stringify(launch.details.launchResolvedExtensions).includes(tempDir), "projection should not expose raw extension paths");
389
+ // Stale supervisor-bridge pair: the --tools allowlist passes both names
390
+ // through, but SELESAI_SUBAGENT_REQUIRED_TOOLS excludes them so the 0.50 child
391
+ // runtime cannot fail the run over the removed native intercom (#1207).
392
+ const recoveryCallArgs = readMockPiArgs(mockPi, 0);
393
+ assert.equal(recoveryCallArgs[recoveryCallArgs.indexOf("--tools") + 1], "read,intercom,contact_supervisor");
394
+ assert.deepEqual(readMockPiRequiredTools(mockPi, 0), ["read"]);
395
+ });
396
+
397
+ it("rejects an explicit unknown model before spawn even when a fallback exists", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
398
+ mockPi.onCall({ output: "should not spawn" });
399
+ const launch = executeAsyncSingle(`async-explicit-unknown-model-${Date.now().toString(36)}`, {
400
+ agent: "worker",
401
+ task: "Do work",
402
+ agentConfig: makeAgent("worker", { model: "mock/fallback", fallbackModels: ["mock/fallback"], completionGuard: false }),
403
+ modelOverride: "mock/does-not-exist",
404
+ modelOrigin: "explicit",
405
+ availableModels: [{ provider: "mock", id: "fallback", fullId: "mock/fallback" }],
406
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
407
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
408
+ shareEnabled: false,
409
+ maxSubagentDepth: 2,
410
+ acceptance: false,
411
+ });
412
+ assert.equal(launch.isError, true);
413
+ assert.match(launch.content[0]?.text ?? "", /Unknown subagent model 'mock\/does-not-exist'/);
414
+ assert.equal(mockPi.callCount(), 0);
415
+ });
416
+
417
+ it("retries configured fallbacks after a valid explicit primary fails", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
418
+ mockPi.onCall({
419
+ jsonl: [{
420
+ type: "message_end",
421
+ message: {
422
+ role: "assistant",
423
+ content: [{ type: "text", text: "primary failed" }],
424
+ model: "openai/gpt-5-mini",
425
+ errorMessage: "rate limit exceeded",
426
+ usage: { input: 10, output: 5, cacheRead: 0, cacheWrite: 0, cost: { total: 0.01 } },
427
+ },
428
+ }],
429
+ exitCode: 1,
430
+ });
431
+ mockPi.onCall({ output: "explicit fallback recovered" });
432
+ const id = `async-explicit-model-fallback-${Date.now().toString(36)}`;
433
+ const launch = executeAsyncSingle(id, {
434
+ agent: "worker",
435
+ task: "Do work",
436
+ agentConfig: makeAgent("worker", { model: "mock/configured", fallbackModels: ["anthropic/claude-sonnet-4"], completionGuard: false }),
437
+ modelOverride: "openai/gpt-5-mini",
438
+ modelOrigin: "explicit",
439
+ availableModels: [
440
+ { provider: "mock", id: "configured", fullId: "mock/configured" },
441
+ { provider: "openai", id: "gpt-5-mini", fullId: "openai/gpt-5-mini" },
442
+ { provider: "anthropic", id: "claude-sonnet-4", fullId: "anthropic/claude-sonnet-4" },
443
+ ],
444
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
445
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
446
+ shareEnabled: false,
447
+ maxSubagentDepth: 2,
448
+ acceptance: false,
449
+ });
450
+
451
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
452
+ const payload = await readAsyncPayload(id);
453
+ assert.equal(payload.success, true);
454
+ assert.equal(payload.results[0]?.model, "anthropic/claude-sonnet-4");
455
+ assert.deepEqual(payload.results[0]?.attemptedModels, ["openai/gpt-5-mini", "anthropic/claude-sonnet-4"]);
456
+ assert.equal(mockPi.callCount(), 2);
457
+ });
458
+
459
+ it("rejects an explicit cached-excluded model before spawn even when a fallback exists", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
460
+ recordModelFailure({ modelId: "blocked", provider: "mock", reason: "sk-secret-token-xyz" });
461
+ mockPi.onCall({ output: "should not spawn" });
462
+ const launch = executeAsyncSingle(`async-explicit-cached-excluded-${Date.now().toString(36)}`, {
463
+ agent: "worker",
464
+ task: "Do work",
465
+ agentConfig: makeAgent("worker", { model: "mock/fallback", fallbackModels: ["mock/fallback"], completionGuard: false }),
466
+ modelOverride: "mock/blocked",
467
+ modelOrigin: "explicit",
468
+ availableModels: [
469
+ { provider: "mock", id: "blocked", fullId: "mock/blocked" },
470
+ { provider: "mock", id: "fallback", fullId: "mock/fallback" },
471
+ ],
472
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
473
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
474
+ shareEnabled: false,
475
+ maxSubagentDepth: 2,
476
+ acceptance: false,
477
+ });
478
+ assert.equal(launch.isError, true);
479
+ assert.match(launch.content[0]?.text ?? "", /is excluded and cannot be replaced by a fallback/);
480
+ assert.equal((launch.content[0]?.text ?? "").includes("sk-secret-token-xyz"), false);
481
+ assert.equal(mockPi.callCount(), 0);
482
+ });
483
+
484
+ it("rejects fallback-only configurations with no launch candidates before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
485
+ mockPi.onCall({ output: "should not spawn" });
486
+ const launch = executeAsyncSingle(`async-fallback-only-zero-candidates-${Date.now().toString(36)}`, {
487
+ agent: "worker",
488
+ task: "Do work",
489
+ agentConfig: makeAgent("worker", { fallbackModels: ["does-not-exist"], completionGuard: false }),
490
+ availableModels: [{ provider: "mock", id: "fallback", fullId: "mock/fallback" }],
491
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
492
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
493
+ shareEnabled: false,
494
+ maxSubagentDepth: 2,
495
+ acceptance: false,
496
+ });
497
+ assert.equal(launch.isError, true);
498
+ assert.match(launch.content[0]?.text ?? "", /Unknown subagent model 'does-not-exist'/);
499
+ assert.equal(mockPi.callCount(), 0);
500
+ });
501
+
502
+ it("rejects an explicit non-strict out-of-scope model before spawn even when a fallback exists", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
503
+ mockPi.onCall({ output: "should not spawn" });
504
+ const launch = executeAsyncSingle(`async-explicit-scope-${Date.now().toString(36)}`, {
505
+ agent: "worker",
506
+ task: "Do work",
507
+ agentConfig: makeAgent("worker", { model: "mock/fallback", fallbackModels: ["mock/fallback"], completionGuard: false }),
508
+ modelOverride: "mock/blocked",
509
+ modelOrigin: "explicit",
510
+ availableModels: [
511
+ { provider: "mock", id: "blocked", fullId: "mock/blocked" },
512
+ { provider: "mock", id: "fallback", fullId: "mock/fallback" },
513
+ ],
514
+ ctx: {
515
+ pi: { events: { emit() {} } },
516
+ cwd: tempDir,
517
+ currentSessionId: "session-1",
518
+ modelScope: { enforce: true, allow: ["mock/fallback"] },
519
+ },
520
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
521
+ shareEnabled: false,
522
+ maxSubagentDepth: 2,
523
+ acceptance: false,
524
+ });
525
+ assert.equal(launch.isError, true);
526
+ assert.match(launch.content[0]?.text ?? "", /outside the configured subagent model scope/);
527
+ assert.equal(mockPi.callCount(), 0);
528
+ });
529
+
530
+ it("uses a configured fallback when the agent primary is unavailable", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
531
+ const id = `async-configured-fallback-${Date.now().toString(36)}`;
532
+ mockPi.onCall({ output: "fallback model ran" });
533
+ const launch = executeAsyncSingle(id, {
534
+ agent: "worker",
535
+ task: "Do work",
536
+ agentConfig: makeAgent("worker", { model: "mock/missing-primary", fallbackModels: ["mock/fallback"], completionGuard: false }),
537
+ modelOrigin: "configured",
538
+ availableModels: [{ provider: "mock", id: "fallback", fullId: "mock/fallback" }],
539
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
540
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
541
+ shareEnabled: false,
542
+ maxSubagentDepth: 2,
543
+ acceptance: false,
544
+ });
545
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
546
+ const payload = await readAsyncPayload(id);
547
+ assert.equal(payload.success, true);
548
+ assert.equal(payload.results[0]?.model, "mock/fallback");
549
+ assert.equal(mockPi.callCount(), 1);
550
+ });
551
+
552
+ it("rejects async thinking above maxThinking before child startup", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
553
+ const launch = executeAsyncSingle(`async-thinking-ceiling-${Date.now().toString(36)}`, {
554
+ agent: "worker",
555
+ task: "Use the strongest available reasoning.",
556
+ agentConfig: makeAgent("worker", { model: "mock/test-model", maxThinking: "xhigh", completionGuard: false }),
557
+ thinkingOverride: "max",
558
+ availableModels: [{ provider: "mock", id: "test-model", fullId: "mock/test-model" }],
559
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
560
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
561
+ shareEnabled: false,
562
+ });
563
+ assert.equal(launch.isError, true);
564
+ assert.match(launch.content[0]?.text ?? "", /max.*xhigh.*worker/);
565
+ assert.equal(mockPi.callCount(), 0);
566
+ });
567
+
568
+ it("rejects implementation workers without mutation-capable tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
569
+ const id = `async-readonly-worker-contract-${Date.now().toString(36)}`;
570
+ mockPi.onCall({ output: "should not spawn" });
571
+ const launch = executeAsyncSingle(id, {
572
+ agent: "worker",
573
+ task: "Implement the requested source fix",
574
+ agentConfig: makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] }),
575
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
576
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
577
+ shareEnabled: false,
578
+ sessionRoot: path.join(tempDir, "sessions"),
579
+ maxSubagentDepth: 2,
580
+ acceptance: false,
581
+ });
582
+
583
+ assert.equal(launch.isError, true);
584
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
585
+ assert.equal(mockPi.callCount(), 0);
586
+ });
587
+
588
+ it("lets unrestricted implementation workers spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
589
+ const id = `async-unrestricted-worker-contract-${Date.now().toString(36)}`;
590
+ mockPi.onCall({ output: "implemented" });
591
+ const launch = executeAsyncSingle(id, {
592
+ agent: "worker",
593
+ task: "Implement the requested source fix",
594
+ agentConfig: makeAgent("worker"),
595
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
596
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
597
+ shareEnabled: false,
598
+ sessionRoot: path.join(tempDir, "sessions"),
599
+ maxSubagentDepth: 2,
600
+ acceptance: false,
601
+ });
602
+
603
+ assert.equal(launch.isError, undefined);
604
+ await waitForAsyncResultFile(id, 10_000);
605
+ assert.equal(mockPi.callCount(), 1);
606
+ });
607
+
608
+ it("lets explicit fast false opt out async external single runs from inherited fast mode", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
609
+ const agentConfig = makeAgent("external", {
610
+ fast: true,
611
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('external async fast false')"] },
612
+ } as never);
613
+
614
+ const rejected = executeAsyncSingle(`async-external-fast-inherited-${Date.now().toString(36)}`, {
615
+ agent: "external",
616
+ task: "Run external",
617
+ agentConfig,
618
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
619
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
620
+ shareEnabled: false,
621
+ });
622
+ assert.equal(rejected.isError, true);
623
+ assert.match(rejected.content[0]?.text ?? "", /does not support: fast mode/);
624
+
625
+ const id = `async-external-fast-false-${Date.now().toString(36)}`;
626
+ const launch = executeAsyncSingle(id, {
627
+ agent: "external",
628
+ task: "Run external",
629
+ agentConfig,
630
+ fast: false,
631
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
632
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
633
+ shareEnabled: false,
634
+ });
635
+
636
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
637
+ const payload = await readAsyncPayload(id);
638
+ assert.equal(payload.success, true);
639
+ assert.match(payload.results[0]?.output ?? "", /external async fast false/);
640
+ assert.equal(mockPi.callCount(), 0);
641
+ });
642
+
643
+ it("establishes a lifecycle sidecar before an external CLI can run", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
644
+ const id = `async-external-lifecycle-${Date.now().toString(36)}`;
645
+ const startedRunners: string[] = [];
646
+ const launch = executeAsyncSingle(id, {
647
+ agent: "external",
648
+ task: "Run external",
649
+ agentConfig: makeAgent("external", {
650
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "setTimeout(() => process.stdout.write('external lifecycle'), 500)"] },
651
+ } as never),
652
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
653
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
654
+ shareEnabled: false,
655
+ activeAsyncCapacity: {
656
+ owner: {},
657
+ markStarted: (runnerProcessInstanceId: string) => { startedRunners.push(runnerProcessInstanceId); },
658
+ markWorkflowStarted() {},
659
+ rollback: () => false,
660
+ rollbackBeforeRunnerProceed: () => false,
661
+ reconcile: () => ({ used: 1, limit: 1 }),
662
+ } as never,
663
+ });
664
+
665
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
666
+ const proof = JSON.parse(fs.readFileSync(path.join(launch.details.asyncDir ?? "", "process-terminal.json"), "utf-8")) as { state?: string; runId?: string; runnerProcessInstanceId?: string };
667
+ assert.equal(proof.state, "pending");
668
+ assert.equal(proof.runId, id);
669
+ assert.deepEqual(startedRunners, [proof.runnerProcessInstanceId]);
670
+ const payload = await readAsyncPayload(id);
671
+ assert.equal(payload.success, true);
672
+ assert.match(payload.results[0]?.output ?? "", /external lifecycle/);
673
+ });
674
+
675
+ it("continues startup when proceed authorization is published but temp cleanup fails", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async (t) => {
676
+ const id = `async-external-proceed-cleanup-${Date.now().toString(36)}`;
677
+ const originalRmSync = fsDefault.rmSync;
678
+ let proceedCleanupFailures = 0;
679
+ let rollbackCount = 0;
680
+ t.mock.method(fsDefault, "rmSync", ((target: fs.PathLike, options?: fs.RmOptions) => {
681
+ const targetPath = String(target);
682
+ if (targetPath.includes(".runner-startup-proceed.json.") && targetPath.endsWith(".tmp") && proceedCleanupFailures === 0) {
683
+ proceedCleanupFailures += 1;
684
+ throw new Error("simulated proceed cleanup failure");
685
+ }
686
+ return originalRmSync(target, options);
687
+ }) as typeof fsDefault.rmSync);
688
+ syncBuiltinESMExports();
689
+ t.after(() => syncBuiltinESMExports());
690
+
691
+ const launch = executeAsyncSingle(id, {
692
+ agent: "external",
693
+ task: "Run external",
694
+ agentConfig: makeAgent("external", {
695
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('external proceed cleanup')"] },
696
+ } as never),
697
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
698
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
699
+ shareEnabled: false,
700
+ activeAsyncCapacity: {
701
+ owner: {},
702
+ markStarted() {},
703
+ markWorkflowStarted() {},
704
+ rollback() { rollbackCount += 1; return false; },
705
+ rollbackBeforeRunnerProceed() { rollbackCount += 1; return false; },
706
+ reconcile: () => ({ used: 1, limit: 1 }),
707
+ } as never,
708
+ });
709
+
710
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
711
+ assert.equal(proceedCleanupFailures, 1);
712
+ assert.equal(rollbackCount, 0);
713
+ const payload = await readAsyncPayload(id);
714
+ assert.equal(payload.success, true);
715
+ assert.match(payload.results[0]?.output ?? "", /external proceed cleanup/);
716
+ });
717
+
718
+ it("fails pre-proceed capacity bind without rebinding the killed runner", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async (t) => {
719
+ const parentSessionId = `session-capacity-bind-${Date.now().toString(36)}`;
720
+ const id = `async-capacity-bind-failure-${Date.now().toString(36)}`;
721
+ const asyncDir = path.join(ASYNC_DIR, id);
722
+ let ownerWrites = 0;
723
+ fs.rmSync(path.join(ACTIVE_ASYNC_CAPACITY_DIR, activeAsyncCapacitySessionKey(parentSessionId)), { recursive: true, force: true });
724
+ const activeAsyncCapacity = acquireActiveAsyncCapacity({ sessionId: parentSessionId, limit: 1, runId: id, kind: "runner", asyncDir }, {
725
+ writeOwner(filePath, owner) {
726
+ ownerWrites += 1;
727
+ if (ownerWrites === 1) throw new Error("simulated durable bind failure");
728
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
729
+ fs.writeFileSync(filePath, JSON.stringify(owner, null, 2));
730
+ },
731
+ });
732
+ assert.ok(activeAsyncCapacity);
733
+ const originalKill = process.kill;
734
+ t.mock.method(process, "kill", ((pid: number, signal?: NodeJS.Signals | 0) => {
735
+ if (signal === 0) return true;
736
+ return originalKill(pid, signal);
737
+ }) as typeof process.kill);
738
+
739
+ const launch = executeAsyncSingle(id, {
740
+ agent: "external",
741
+ task: "Run external",
742
+ agentConfig: makeAgent("external", {
743
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "setTimeout(() => {}, 30000)"] },
744
+ } as never),
745
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: parentSessionId },
746
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
747
+ shareEnabled: false,
748
+ activeAsyncCapacity,
749
+ });
750
+
751
+ assert.equal(launch.isError, true);
752
+ assert.match(launch.content[0]?.text ?? "", /Failed to establish async runner capacity ownership/);
753
+ assert.equal(ownerWrites, 1);
754
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
755
+ assert.equal(status.state, "failed");
756
+ assert.match(status.error ?? "", /Failed to establish async runner capacity ownership/);
757
+ assert.equal(status.processTerminal?.state, "not-started");
758
+ assert.deepEqual(getActiveAsyncCapacitySnapshot(parentSessionId, 1), { used: 0, limit: 1 });
759
+ });
760
+
761
+ it("lets explicit fast false opt out async external chains from inherited fast mode", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
762
+ const agent = makeAgent("external", {
763
+ fast: true,
764
+ runner: { type: "external-cli", command: process.execPath, args: ["-e", "process.stdout.write('external chain fast false')"] },
765
+ } as never);
766
+
767
+ const rejected = executeAsyncChain(`async-chain-external-fast-inherited-${Date.now().toString(36)}`, {
768
+ chain: [{ agent: "external", task: "Run external" }],
769
+ resultMode: "chain",
770
+ agents: [agent],
771
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
772
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
773
+ shareEnabled: false,
774
+ acceptance: false,
775
+ });
776
+ assert.equal(rejected.isError, true);
777
+ assert.match(rejected.content[0]?.text ?? "", /does not support: fast mode/);
778
+
779
+ const id = `async-chain-external-fast-false-${Date.now().toString(36)}`;
780
+ const launch = executeAsyncChain(id, {
781
+ chain: [{ agent: "external", task: "Run external" }],
782
+ resultMode: "chain",
783
+ agents: [agent],
784
+ fast: false,
785
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
786
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
787
+ shareEnabled: false,
788
+ acceptance: false,
789
+ });
790
+
791
+ assert.equal(launch.isError, undefined, launch.content[0]?.text ?? "launch failed");
792
+ const payload = await readAsyncPayload(id);
793
+ assert.equal(payload.success, true);
794
+ assert.match(payload.results[0]?.output ?? "", /external chain fast false/);
795
+ assert.equal(mockPi.callCount(), 0);
796
+ });
797
+
798
+ it("rejects implementation workers when a capability ceiling removes mutation tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
799
+ const id = `async-ceiling-readonly-worker-contract-${Date.now().toString(36)}`;
800
+ mockPi.onCall({ output: "should not spawn" });
801
+ const launch = executeAsyncSingle(id, {
802
+ agent: "worker",
803
+ task: "Implement the requested source fix",
804
+ agentConfig: makeAgent("worker", { tools: ["read", "write"] }),
805
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
806
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
807
+ shareEnabled: false,
808
+ sessionRoot: path.join(tempDir, "sessions"),
809
+ maxSubagentDepth: 2,
810
+ acceptance: false,
811
+ capabilityCeiling: { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["test"] },
812
+ });
813
+
814
+ assert.equal(launch.isError, true);
815
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
816
+ assert.equal(mockPi.callCount(), 0);
817
+ });
818
+
819
+ it("rejects workflow implementation workers without mutation-capable tools before spawn", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, () => {
820
+ const id = `async-workflow-readonly-worker-contract-${Date.now().toString(36)}`;
821
+ mockPi.onCall({ output: "should not spawn" });
822
+ const launch = executeAsyncChain(id, {
823
+ chain: [{ agent: "worker", task: "Implement the requested source fix" }],
824
+ resultMode: "chain",
825
+ agents: [makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] })],
826
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
827
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
828
+ shareEnabled: false,
829
+ maxSubagentDepth: 2,
830
+ acceptance: false,
831
+ });
832
+
833
+ assert.equal(launch.isError, true);
834
+ assert.match(launch.content[0]?.text ?? "", /no mutation-capable tools/);
835
+ assert.equal(mockPi.callCount(), 0);
836
+ });
837
+
838
+ it("rejects workflow read-only workers after previous-output templates resolve to implementation tasks", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
839
+ const id = `async-workflow-resolved-readonly-worker-contract-${Date.now().toString(36)}`;
840
+ mockPi.onCall({ output: "Implement the requested source fix" });
841
+ mockPi.onCall({ output: "should not spawn" });
842
+ const launch = executeAsyncChain(id, {
843
+ chain: [
844
+ { agent: "producer", task: "Return the next instruction" },
845
+ { agent: "worker", task: "{previous}" },
846
+ ],
847
+ resultMode: "chain",
848
+ agents: [
849
+ makeAgent("producer", { completionGuard: false }),
850
+ makeAgent("worker", { tools: ["read", "grep", "find", "ls", "contact_supervisor"] }),
851
+ ],
852
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
853
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
854
+ shareEnabled: false,
855
+ maxSubagentDepth: 2,
856
+ acceptance: false,
857
+ });
858
+
859
+ assert.equal(launch.isError, undefined);
860
+ const resultPath = await waitForAsyncResultFile(id, 10_000);
861
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
862
+ assert.equal(payload.success, false);
863
+ assert.match(payload.results[1]?.error ?? "", /no mutation-capable tools/);
864
+ assert.equal(mockPi.callCount(), 1);
865
+ });
866
+
867
+ it("background parallel groups report usage budget state and block queued children", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
868
+ mockPi.onCall({ output: "first async result" });
869
+ const id = `async-usage-budget-${Date.now().toString(36)}`;
870
+ const launch = executeAsyncChain(id, {
871
+ chain: [{
872
+ parallel: [
873
+ { agent: "first", task: "First task" },
874
+ { agent: "second", task: "Second task" },
875
+ ],
876
+ concurrency: 1,
877
+ }],
878
+ resultMode: "parallel",
879
+ usageBudget: { tokens: { hard: 10 } },
880
+ agents: [makeAgent("first"), makeAgent("second")],
881
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
882
+ artifactConfig: { enabled: false, includeInput: false, includeOutput: false, includeJsonl: false, includeMetadata: false, cleanupDays: 7 },
883
+ shareEnabled: false,
884
+ maxSubagentDepth: 2,
885
+ });
886
+
887
+ assert.equal(launch.details.usageBudget?.exhausted, false);
888
+ const payload = await readAsyncPayload(id);
889
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf-8")) as AsyncStatusPayload;
890
+ assert.equal(payload.success, false);
891
+ assert.equal(payload.state, "failed");
892
+ assert.match(payload.error ?? payload.summary ?? "", /Usage budget exhausted/);
893
+ assert.equal(payload.results.length, 2);
894
+ assert.equal(payload.results[1]?.skipped, true);
895
+ assert.match(payload.results[1]?.error ?? "", /Usage budget exhausted/);
896
+ assert.equal(mockPi.callCount(), 1);
897
+ assert.equal(payload.usageBudget?.exhausted, true);
898
+ assert.equal(payload.usageBudget?.reason, "tokens");
899
+ assert.equal(status.usageBudget?.exhausted, true);
900
+ assert.equal(status.steps?.[0]?.status, "complete");
901
+ assert.equal(status.steps?.[1]?.status, "failed");
902
+ });
903
+
904
+ it("routes async artifacts to the configured session directory", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
905
+ mockPi.onCall({ output: "async session artifact" });
906
+ const sessionFile = path.join(tempDir, "sessions", "parent-session", "session.jsonl");
907
+ const ctx = makeMinimalCtx(tempDir);
908
+ ctx.sessionManager.getSessionFile = () => sessionFile;
909
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })], { artifactDir: "session" });
910
+
911
+ const launch = await executor.execute(
912
+ "async-session-artifact-dir",
913
+ { agent: "worker", task: "Write async session artifacts", async: true, runId: "async-session-artifacts", acceptance: false },
914
+ new AbortController().signal,
915
+ undefined,
916
+ ctx,
917
+ ) as AsyncExecutionResult;
918
+
919
+ const expectedDir = path.join(path.dirname(sessionFile), "subagent-artifacts");
920
+ assert.equal(launch.isError, undefined);
921
+ assert.ok(launch.details.asyncId);
922
+ const descriptor = JSON.parse(fs.readFileSync(path.join(launch.details.asyncDir!, "recovery-descriptor.json"), "utf-8"));
923
+ assert.equal(descriptor.artifactsDir, expectedDir);
924
+ assert.equal(descriptor.artifactConfig.dir, "session");
925
+
926
+ const payload = await readAsyncPayload(launch.details.asyncId);
927
+ const outputPath = payload.results[0]?.artifactPaths?.outputPath;
928
+ assert.ok(outputPath?.startsWith(`${expectedDir}${path.sep}`));
929
+ assert.equal(fs.readFileSync(outputPath, "utf-8"), "async session artifact");
930
+ assert.equal(fs.existsSync(path.join(tempDir, ".pi-subagents", "artifacts")), false);
931
+ });
932
+
933
+ it("persists async capability ceiling audit to status, results, events, and metadata", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
934
+ mockPi.onCall({ output: "restricted async done" });
935
+ const sessionId = `session-capability-${Date.now().toString(36)}`;
936
+ const handle = registerSubagentCapabilityCeiling({ sessionId, ceiling: { allowedTools: ["read"], denyExtensions: true }, source: "test" });
937
+ let waitForOwnedRun: (() => Promise<void>) | undefined;
938
+ try {
939
+ const executor = makeAsyncExecutor([makeAgent("worker", { tools: ["read", "write"], completionGuard: false })]);
940
+ const id = `async-capability-${Date.now().toString(36)}`;
941
+ const ctx = makeMinimalCtx(tempDir);
942
+ ctx.sessionManager.getSessionId = () => sessionId;
943
+ const launch = await executor.execute(
944
+ id,
945
+ { agent: "worker", task: "Run with a restricted capability ceiling", async: true, runId: id, acceptance: false, artifacts: true },
946
+ new AbortController().signal,
947
+ undefined,
948
+ ctx,
949
+ ) as AsyncExecutionResult;
950
+ const asyncId = launch.details.asyncId;
951
+ const asyncDir = launch.details.asyncDir;
952
+ if (asyncId && asyncDir) {
953
+ let terminalWait: Promise<void> | undefined;
954
+ waitForOwnedRun = () => terminalWait ??= (async () => {
955
+ const eventsPath = path.join(asyncDir, "events.jsonl");
956
+ const deadline = Date.now() + 10_000;
957
+ while (true) {
958
+ let journal = "";
959
+ try {
960
+ journal = fs.readFileSync(eventsPath, "utf-8");
961
+ } catch (error) {
962
+ if (!["ENOENT", "EINTR", "EAGAIN", "EBUSY"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error;
963
+ }
964
+ // Only complete records for this run establish the final status/journal boundary.
965
+ const terminal = journal.split("\n").slice(0, -1).some((line) => {
966
+ try {
967
+ const event = JSON.parse(line);
968
+ return event?.type === "subagent.run.process_terminal" && event.runId === asyncId;
969
+ } catch {
970
+ return false;
971
+ }
972
+ });
973
+ if (terminal) break;
974
+ assert.ok(Date.now() <= deadline, `Timed out waiting for async event 'subagent.run.process_terminal': ${eventsPath}`);
975
+ await new Promise((resolve) => setTimeout(resolve, 50));
976
+ }
977
+ })();
978
+ }
979
+ assert.equal(launch.isError, undefined);
980
+ assert.ok(asyncId);
981
+ assert.ok(asyncDir);
982
+ assert.equal(asyncDir, path.join(ASYNC_DIR, asyncId));
983
+ const resultPath = await waitForAsyncResultFile(asyncId, 10_000);
984
+ await waitForOwnedRun!();
985
+ const payload = JSON.parse(fs.readFileSync(resultPath, "utf-8")) as AsyncResultPayload;
986
+ const status = JSON.parse(fs.readFileSync(path.join(asyncDir, "status.json"), "utf-8")) as AsyncStatusPayload;
987
+ assert.deepEqual(payload.capabilityCeiling, { version: 1, allowedTools: ["read"], denyExtensions: true, sources: ["test"] });
988
+ assert.deepEqual(payload.results[0]?.capabilityCeiling, payload.capabilityCeiling);
989
+ assert.deepEqual(status.capabilityCeiling, payload.capabilityCeiling);
990
+ assert.deepEqual(status.steps?.[0]?.capabilityCeiling, payload.capabilityCeiling);
991
+ assert.deepEqual(payload.capabilityAudit?.effectiveTools, ["read"]);
992
+ assert.deepEqual(payload.capabilityAudit?.removedTools, ["write", "contact_supervisor"]);
993
+ assert.equal(payload.capabilityAudit?.extensionsDenied, true);
994
+ const events = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line));
995
+ assert.ok(events.some((event) => event.type === "subagent.capability-ceiling.applied" && event.stepIndex === 0 && event.capabilityAudit?.removedTools?.includes("write")));
996
+ const metadataPath = payload.results[0]?.artifactPaths?.metadataPath;
997
+ assert.ok(metadataPath);
998
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { launchContractDigest?: string; capabilityCeiling?: unknown; capabilityAudit?: { removedTools?: string[] } };
999
+ assert.equal(metadata.launchContractDigest, payload.results[0]?.launchContractDigest);
1000
+ assert.deepEqual(metadata.capabilityCeiling, payload.capabilityCeiling);
1001
+ assert.deepEqual(metadata.capabilityAudit?.removedTools, ["write", "contact_supervisor"]);
1002
+ } finally {
1003
+ try {
1004
+ // Result/read/assertion failures must not skip an established owned-run wait.
1005
+ await waitForOwnedRun?.();
1006
+ } finally {
1007
+ handle.dispose();
1008
+ }
1009
+ }
1010
+ });
1011
+
1012
+ it("redacts async task text from durable artifacts, transcripts, and metadata", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1013
+ mockPi.onCall({ output: "async redaction complete" });
1014
+ const id = `async-prompt-redaction-${Date.now().toString(36)}`;
1015
+ const sentinel = "ASYNC_RAW_PROMPT_SENTINEL_1021";
1016
+ executeAsyncSingle(id, {
1017
+ agent: "worker",
1018
+ task: `Handle ${sentinel} without persisting it`,
1019
+ agentConfig: makeAgent("worker", { completionGuard: false }),
1020
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1021
+ artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeJsonl: false, includeTranscript: true, includeMetadata: true, cleanupDays: 7 },
1022
+ artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"),
1023
+ shareEnabled: false,
1024
+ maxSubagentDepth: 2,
1025
+ acceptance: false,
1026
+ });
1027
+
1028
+ await waitForMockPiCall(mockPi, 0);
1029
+ const callFile = fs.readdirSync(mockPi.dir).find((name) => name.endsWith(".json"));
1030
+ assert.ok(callFile);
1031
+ const call = JSON.parse(fs.readFileSync(path.join(mockPi.dir, callFile), "utf-8")) as MockPiCallRecord;
1032
+ assert.match(resolveMockPiCallArgs(call).join("\n"), new RegExp(sentinel));
1033
+
1034
+ const payload = await readAsyncPayload(id);
1035
+ const artifactPaths = payload.results[0]?.artifactPaths;
1036
+ assert.ok(artifactPaths?.inputPath);
1037
+ assert.ok(artifactPaths.metadataPath);
1038
+ assert.ok(artifactPaths.transcriptPath);
1039
+ const inputText = fs.readFileSync(artifactPaths.inputPath, "utf-8");
1040
+ const transcriptText = fs.readFileSync(artifactPaths.transcriptPath, "utf-8");
1041
+ const metadataText = fs.readFileSync(artifactPaths.metadataPath, "utf-8");
1042
+ assert.doesNotMatch(inputText, new RegExp(sentinel));
1043
+ assert.doesNotMatch(transcriptText, new RegExp(sentinel));
1044
+ assert.doesNotMatch(metadataText, new RegExp(sentinel));
1045
+ assert.match(inputText, /\[prompt redacted\]/);
1046
+ assert.match(transcriptText, /\[prompt redacted\]/);
1047
+ assert.equal((JSON.parse(metadataText) as { task?: string }).task, "[prompt redacted]");
1048
+ });
1049
+
1050
+ it("background writes a failure stub to output artifacts when no output was produced", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1051
+ mockPi.onCall({ output: "", stderr: "model unavailable", exitCode: 1 });
1052
+ const id = `async-empty-failure-artifact-${Date.now().toString(36)}`;
1053
+ executeAsyncSingle(id, {
1054
+ agent: "worker",
1055
+ task: "Fail before output",
1056
+ agentConfig: makeAgent("worker", { completionGuard: false }),
1057
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1058
+ artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 },
1059
+ artifactsDir: path.join(tempDir, ".pi-subagents", "artifacts"),
1060
+ shareEnabled: false,
1061
+ maxSubagentDepth: 2,
1062
+ acceptance: false,
1063
+ });
1064
+
1065
+ const payload = await readAsyncPayload(id);
1066
+ assert.equal(payload.success, false);
1067
+ const outputPath = payload.results[0]?.artifactPaths?.outputPath;
1068
+ assert.ok(outputPath, "should expose an output artifact path");
1069
+ const artifact = fs.readFileSync(outputPath, "utf-8");
1070
+ assert.match(artifact, /Subagent run failed before producing output\./);
1071
+ assert.match(artifact, /Error:\nmodel unavailable/);
1072
+ assert.match(artifact, /Transcript:/);
1073
+ assert.match(artifact, /Metadata:/);
1074
+ });
1075
+
1076
+ it("recreates project-local artifact directories removed before async completion", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1077
+ mockPi.onCall({ delay: 800, output: "completed after artifact cleanup" });
1078
+ const id = `async-artifact-dir-recovery-${Date.now().toString(36)}`;
1079
+ const artifactsDir = path.join(tempDir, ".pi-subagents", "artifacts");
1080
+ executeAsyncSingle(id, {
1081
+ agent: "worker",
1082
+ task: "Complete after generated artifacts are cleaned",
1083
+ agentConfig: makeAgent("worker", { completionGuard: false }),
1084
+ ctx: { pi: { events: { emit() {} } }, cwd: tempDir, currentSessionId: "session-1" },
1085
+ artifactConfig: { enabled: true, includeInput: true, includeOutput: true, includeJsonl: false, includeMetadata: true, cleanupDays: 7 },
1086
+ artifactsDir,
1087
+ shareEnabled: false,
1088
+ maxSubagentDepth: 2,
1089
+ acceptance: false,
1090
+ });
1091
+
1092
+ await waitForMockPiCall(mockPi, 0);
1093
+ assert.equal(fs.existsSync(artifactsDir), true);
1094
+ fs.rmSync(path.join(tempDir, ".pi-subagents"), { recursive: true, force: true });
1095
+
1096
+ const payload = await readAsyncPayload(id);
1097
+ assert.equal(payload.success, true);
1098
+ assert.equal(payload.results[0]?.outputSaveError, undefined);
1099
+ assert.equal(payload.results[0]?.metadataSaveError, undefined);
1100
+ const outputPath = payload.results[0]?.artifactPaths?.outputPath;
1101
+ const metadataPath = payload.results[0]?.artifactPaths?.metadataPath;
1102
+ assert.ok(outputPath && metadataPath);
1103
+ assert.equal(fs.readFileSync(outputPath, "utf-8"), "completed after artifact cleanup");
1104
+ assert.equal((JSON.parse(fs.readFileSync(metadataPath, "utf-8")) as { exitCode?: number }).exitCode, 0);
1105
+ });
1106
+
1107
+ it("background preserves retry lifecycle from an oversized agent_end aggregate", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1108
+ mockPi.onCall({ steps: [
1109
+ { jsonl: [
1110
+ events.assistantMessage("retrying oversized async response"),
1111
+ { type: "agent_end", messages: ["x".repeat(64 * 1024)], willRetry: true },
1112
+ ] },
1113
+ { delay: 1400, jsonl: [events.assistantMessage("settled after oversized aggregate"), { type: "agent_end", willRetry: false }, { type: "agent_settled" }] },
1114
+ ] });
1115
+ const id = `async-lifecycle-oversized-retry-${Date.now().toString(36)}`;
1116
+ const startedAt = Date.now();
1117
+ launchProtocolTest(id);
1118
+ const payload = await readAsyncPayload(id);
1119
+ assert.equal(payload.success, true);
1120
+ assert.equal(payload.results[0]?.output, "settled after oversized aggregate");
1121
+ assert.ok(Date.now() - startedAt >= 1200, "projected agent_end must cancel the retry drain");
1122
+ });
1123
+
1124
+ it("background cancels final drain while agent_end reports a retry and waits for agent_settled", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1125
+ mockPi.onCall({ steps: [
1126
+ { jsonl: [events.assistantMessage("retrying async response"), { type: "agent_end", willRetry: true }] },
1127
+ { delay: 1400, jsonl: [events.assistantMessage("settled async response"), { type: "agent_end", willRetry: false }, { type: "agent_settled" }] },
1128
+ ] });
1129
+ const id = `async-lifecycle-retry-${Date.now().toString(36)}`;
1130
+ const startedAt = Date.now();
1131
+ launchProtocolTest(id);
1132
+ const payload = await readAsyncPayload(id);
1133
+ assert.equal(payload.success, true);
1134
+ assert.equal(payload.results[0]?.output, "settled async response");
1135
+ assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during the retry delay");
1136
+ });
1137
+
1138
+ it("background does not drain on settlement from a compaction attempt that will retry", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1139
+ mockPi.onCall({ steps: [
1140
+ { jsonl: [{ type: "compaction_end", willRetry: true }, { type: "agent_settled" }] },
1141
+ { delay: 1400, jsonl: [events.assistantMessage("settled after compaction retry"), { type: "agent_start" }, { type: "agent_end", willRetry: false }, { type: "agent_settled" }] },
1142
+ ] });
1143
+ const id = `async-lifecycle-compaction-retry-${Date.now().toString(36)}`;
1144
+ const startedAt = Date.now();
1145
+ launchProtocolTest(id);
1146
+ const payload = await readAsyncPayload(id);
1147
+ assert.equal(payload.success, true);
1148
+ assert.equal(payload.results[0]?.output, "settled after compaction retry");
1149
+ assert.ok(Date.now() - startedAt >= 1200, "background runner must not terminate during compaction retry");
1150
+ });
1151
+
1152
+ it("background treats agent_settled as a clean terminal watermark", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1153
+ mockPi.onCall({ jsonl: [mockAssistantMessage("settled async without a terminal assistant stop", "tool_use"), { type: "agent_settled" }], keepAliveAfterFinalMessageMs: 15_000 });
1154
+ const id = `async-lifecycle-settled-${Date.now().toString(36)}`;
1155
+ const startedAt = Date.now();
1156
+ launchProtocolTest(id);
1157
+ const payload = await readAsyncPayload(id);
1158
+ assert.equal(payload.success, true);
1159
+ assert.equal(payload.results[0]?.error, undefined);
1160
+ assert.equal(payload.results[0]?.output, "settled async without a terminal assistant stop");
1161
+ assert.ok(Date.now() - startedAt < 10_000, "agent_settled should trigger bounded child cleanup");
1162
+ });
1163
+
1164
+ it("does not report successful compaction settlement as a failure", { skip: !isAsyncAvailable() ? "jiti not available" : undefined }, async () => {
1165
+ mockPi.onCall({
1166
+ jsonl: [{ type: "compaction_start" }, mockAssistantMessage("settled after compaction"), { type: "agent_settled" }],
1167
+ keepAliveAfterFinalMessageMs: 15_000,
1168
+ });
1169
+ const id = `async-lifecycle-compaction-success-${Date.now().toString(36)}`;
1170
+ launchProtocolTest(id);
1171
+ const payload = await readAsyncPayload(id);
1172
+ assert.equal(payload.success, true);
1173
+ assert.equal(payload.results[0]?.success, true);
1174
+ assert.equal(payload.results[0]?.error, undefined);
1175
+ assert.equal(payload.results[0]?.output, "settled after compaction");
1176
+ assert.equal(payload.results[0]?.effects?.settlementDiagnostic, undefined);
1177
+ const status = await waitForAsyncState(id, (candidate) => candidate.state === "complete");
1178
+ assert.equal(status.steps?.[0]?.effects?.settlementDiagnostic, undefined);
1179
+ });
1180
+
1181
+ });