@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
@@ -9,7 +9,6 @@ import { registerAgent } from "../../src/api/agents.ts";
9
9
  import { clearRuntimeAgentsForPi } from "../../src/agents/runtime-agent-registry.ts";
10
10
  import { scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
11
11
  import { updateActiveRunIndex } from "../../src/runs/background/active-run-index.ts";
12
- import { SUBAGENT_FANOUT_CHILD_ENV } from "../../src/runs/shared/pi-args.ts";
13
12
  import { getArtifactPaths, getArtifactsDir } from "../../src/shared/artifacts.ts";
14
13
  import { ASYNC_DIR, DIRS } from "../../src/shared/types.ts";
15
14
  import type { WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
@@ -1514,4 +1513,4 @@ describe("subagents-inspect-rpc command", { skip: !available ? "slash-commands.t
1514
1513
  assert.equal(widgetCalls.length, 0);
1515
1514
  assert.match(notifications[0] ?? "", /RPC surfaces/);
1516
1515
  });
1517
- });
1516
+ });
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { syncBuiltinESMExports } from "node:module";
5
+ import { describe, it } from "node:test";
6
+ import { createResultWatcher } from "../../src/runs/background/result-watcher.ts";
7
+ import registerSubagentNotify from "../../src/runs/background/notify.ts";
8
+ import { SUBAGENT_ASYNC_COMPLETE_EVENT, type SubagentState } from "../../src/shared/types.ts";
9
+ import { makeMinimalCtx } from "../support/helpers.ts";
10
+ import { installAsyncExecutionHooks, available, createSubagentExecutor, ASYNC_DIR, RESULTS_DIR, tempDir } from "../support/async-execution-fixture.ts";
11
+
12
+ function barrier() {
13
+ let resolve!: () => void;
14
+ const promise = new Promise<void>((done) => { resolve = done; });
15
+ return { promise, resolve };
16
+ }
17
+ async function bounded(promise: Promise<unknown>) {
18
+ let timer: ReturnType<typeof setTimeout>;
19
+ try { await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("publication barrier timed out")), 12000); })]); }
20
+ finally { clearTimeout(timer!); }
21
+ }
22
+
23
+ describe("host workflow result publication", { skip: !available }, () => {
24
+ installAsyncExecutionHooks();
25
+ for (const outcome of ["complete", "failed", "stopped", "retry-error"] as const) {
26
+ it(`retains real Darwin demand until deferred indexed workflow publication settles (${outcome})`, async () => {
27
+ const polled = barrier(), published = barrier(), delivered = barrier(), retired = barrier(), failed = barrier(), emitted = barrier();
28
+ let held = true, attempted = false, notifications = 0, refreshes = 0;
29
+ let statusWrites = 0, statusDeferred = false, statusRecovered = false;
30
+ let poll: ReturnType<typeof setInterval> | undefined;
31
+ const sessionId = "workflow-publication-session", owner = "workflow-publication-owner";
32
+ const state: SubagentState = { baseCwd: tempDir, currentSessionId: sessionId, completionOwnerId: owner,
33
+ asyncJobs: new Map(), foregroundControls: new Map(), lastForegroundControlId: null,
34
+ cleanupTimers: new Map(), lastUiContext: null, poller: null, completionSeen: new Map(),
35
+ watcher: null, watcherRestartTimer: null, resultFileCoalescer: { schedule: () => false, clear() {} },
36
+ };
37
+ const pi = { getSessionName: () => undefined, events: { on: () => () => {}, emit(event: string, data: unknown) {
38
+ if (event === SUBAGENT_ASYNC_COMPLETE_EVENT) { assert.equal((data as { completionOwnerId: string }).completionOwnerId, owner); delivered.resolve(); }
39
+ } }, sendMessage() { notifications++; } };
40
+ const notifier = registerSubagentNotify(pi, state, { batchConfig: { enabled: false } });
41
+ const watcher = createResultWatcher(pi, state, RESULTS_DIR, 60000, { platform: "darwin", coalesceDelayMs: 0, notifier,
42
+ hasDeliveryDemand: () => [...state.asyncJobs.values()].some((job) => job.status === "running" || job.status === "queued"),
43
+ timers: { setTimeout, clearTimeout, setInterval: ((handler: () => void, delay: number) => {
44
+ assert.equal(delay, 3000);
45
+ poll = setInterval(() => { handler(); if (attempted) polled.resolve(); }, delay); return poll;
46
+ }) as typeof setInterval, clearInterval: ((handle: ReturnType<typeof setInterval>) => { clearInterval(handle); poll = undefined; retired.resolve(); }) as typeof clearInterval },
47
+ });
48
+ const originalWrite = fs.writeFileSync, originalRename = fs.renameSync, originalAppend = fs.appendFileSync;
49
+ fs.writeFileSync = ((file, ...args) => {
50
+ if (String(file).includes(`${path.sep}result-pending${path.sep}`)) {
51
+ attempted = true;
52
+ if (held) throw Object.assign(new Error("injected workflow capacity failure"), { code: "ENOSPC" });
53
+ if (outcome === "retry-error") throw Object.assign(new Error("injected workflow retry EIO"), { code: "EIO" });
54
+ }
55
+ return originalWrite(file, ...args);
56
+ }) as typeof fs.writeFileSync;
57
+ fs.renameSync = ((from, to) => {
58
+ if (path.basename(String(to)) === "status.json") {
59
+ if (++statusWrites > 1 && !attempted) { statusDeferred = true; throw Object.assign(new Error("injected running status capacity failure"), { code: "ENOSPC" }); }
60
+ if (statusDeferred && held && attempted) statusRecovered = true;
61
+ }
62
+ originalRename(from, to); if (path.dirname(String(to)) === RESULTS_DIR) published.resolve();
63
+ }) as typeof fs.renameSync;
64
+ fs.appendFileSync = ((file, data, ...args) => {
65
+ originalAppend(file, data, ...args);
66
+ if (String(data).includes('"type":"subagent.workflow.result_write_failed"')) failed.resolve();
67
+ if (String(data).includes('"type":"subagent.workflow.emit"')) emitted.resolve();
68
+ }) as typeof fs.appendFileSync;
69
+ syncBuiltinESMExports();
70
+ try {
71
+ const executor = createSubagentExecutor!({ pi, state, config: {}, asyncByDefault: false, tempArtifactsDir: tempDir,
72
+ getSubagentSessionRoot: () => tempDir, expandTilde: (p: string) => p, discoverAgents: () => ({ agents: [] }),
73
+ refreshResultDelivery: () => { refreshes++; watcher.refreshResultDelivery(); },
74
+ });
75
+ const ctx = makeMinimalCtx(tempDir); ctx.sessionManager.getSessionId = () => sessionId;
76
+ const script = outcome === "stopped" ? "await new Promise(() => {})" : outcome === "failed" ? "throw new Error('workflow script failed')" : "return 'done'";
77
+ const launch = await executor.execute("workflow-publication", { workflowScript: `emit('progress'); ${script}`, async: true }, new AbortController().signal, undefined, ctx);
78
+ assert.notEqual(launch.isError, true);
79
+ const id = launch.details.asyncId!;
80
+ watcher.startResultWatcher();
81
+ if (outcome === "stopped") {
82
+ await bounded(emitted.promise);
83
+ const stop = await executor.execute("workflow-stop", { action: "stop", id }, new AbortController().signal, undefined, ctx);
84
+ assert.notEqual(stop.isError, true);
85
+ assert.match(stop.content[0]?.text ?? "", /Stop requested/);
86
+ assert.equal(state.workflowControllers?.get(id)?.signal.aborted, true);
87
+ }
88
+ await bounded(polled.promise);
89
+ const status = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf8"));
90
+ assert.equal(status.state, "running", "terminal status must not precede indexed workflow result publication");
91
+ assert.equal(state.asyncJobs.get(id)?.status, "running");
92
+ assert.equal(statusDeferred, true); assert.equal(statusRecovered, true);
93
+ assert.equal(fs.existsSync(path.join(ASYNC_DIR, ".active-runs", id)), true);
94
+ assert.ok(poll); assert.equal(notifications, 0); assert.equal(refreshes, 0);
95
+ held = false;
96
+ if (outcome === "retry-error") {
97
+ await bounded(failed.promise);
98
+ // Allow the awaited failure callback to finish its existing cleanup.
99
+ await new Promise<void>((resolve) => setImmediate(resolve));
100
+ assert.equal(state.workflowControllers?.has(id), false);
101
+ assert.equal(state.asyncJobs.get(id)?.status, "running");
102
+ assert.equal(notifications, 0); assert.equal(refreshes, 0);
103
+ assert.match(fs.readFileSync(path.join(ASYNC_DIR, id, "events.jsonl"), "utf8"), /injected workflow retry EIO/);
104
+ assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${id}.json`)), false);
105
+ return;
106
+ }
107
+ await bounded(Promise.all([published.promise, delivered.promise]));
108
+ assert.equal(state.asyncJobs.get(id)?.status, outcome);
109
+ const terminalStatus = JSON.parse(fs.readFileSync(path.join(ASYNC_DIR, id, "status.json"), "utf8"));
110
+ assert.equal(terminalStatus.state, outcome);
111
+ if (outcome === "stopped") assert.equal(terminalStatus.stopped, true);
112
+ assert.equal(notifications, 1); assert.equal(refreshes, 1);
113
+ assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${id}.json`)), false);
114
+ // The existing demand timer retires itself after terminal publication.
115
+ await bounded(retired.promise);
116
+ assert.equal(poll, undefined);
117
+ } finally {
118
+ held = false;
119
+ try { await bounded(outcome === "retry-error" ? failed.promise : published.promise); }
120
+ finally {
121
+ watcher.stopResultWatcher(); notifier.dispose();
122
+ fs.writeFileSync = originalWrite; fs.renameSync = originalRename; fs.appendFileSync = originalAppend; syncBuiltinESMExports();
123
+ }
124
+ }
125
+ });
126
+ }
127
+ });
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Integration tests for the durable steer inbox of async WORKFLOW runs.
3
+ *
4
+ * `watchAsyncControlInbox` is installed only by subagent-runner.ts, and the runner never executes
5
+ * workflow scripts, so before this fix no process consumed `control/steer-requests/` for a
6
+ * workflow run: a queued request file sat on disk until the run ended and the requester's
7
+ * "delivered" receipt was never backed by a delivery.
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { events, makeAgent, makeMinimalCtx } from "../support/helpers.ts";
15
+ import { requestAsyncSteer, steerInboxClosedPath, steerRequestsDir } from "../../src/runs/background/control-channel.ts";
16
+ import type { AsyncStatusPayload } from "../support/async-execution-fixture.ts";
17
+ import type { SteeringStatus } from "../../src/shared/types.ts";
18
+ import {
19
+ installAsyncExecutionHooks, available, isAsyncAvailable, ASYNC_DIR,
20
+ createSubagentExecutor, waitForMockPiCall, waitForAsyncState, tempDir, mockPi,
21
+ makeAsyncExecutor, readAsyncPayload, RESULTS_DIR, waitForAsyncEvent,
22
+ } from "../support/async-execution-fixture.ts";
23
+
24
+ type SteeringTargets = { steering?: { recent: Array<{ id: string; targets: Array<{ index: number; state: string; reason?: string }> }> } };
25
+
26
+ function recentSteering(status: AsyncStatusPayload, requestId: string): { index: number; state: string; reason?: string } | undefined {
27
+ return (status as SteeringTargets).steering?.recent?.find((request) => request.id === requestId)?.targets[0];
28
+ }
29
+
30
+ describe("async workflow steer inbox", { skip: !available ? "pi packages not available" : undefined }, () => {
31
+ installAsyncExecutionHooks();
32
+
33
+ for (const outcome of ["complete", "failed", "stopped"]) it(`settles steering without terminal status when result index publication fails (${outcome})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
34
+ const release = path.join(tempDir, "index-failure-release");
35
+ const indexPath = path.join(RESULTS_DIR, "result-index");
36
+ mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
37
+ const ctx = makeMinimalCtx(tempDir);
38
+ ctx.sessionManager.getSessionId = () => "session-index-steering";
39
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
40
+ const launch = await executor.execute("index-steering", {
41
+ workflowScript: `await runs.run("A", { agent: "worker", task: "Wait" }); ${outcome === "failed" ? 'throw new Error("script failed")' : 'return "done"'}`,
42
+ async: true,
43
+ }, new AbortController().signal, undefined, ctx);
44
+ assert.equal(launch.isError, undefined);
45
+ const runId = launch.details!.asyncId as string;
46
+ const asyncDir = path.join(ASYNC_DIR, runId);
47
+ await waitForMockPiCall(mockPi, 0, 10_000);
48
+ const session = mockPi.sessions[0]!.session!;
49
+ const originalSteer = session.steer;
50
+ let entered = false;
51
+ let releaseSteer!: () => void;
52
+ const blocked = new Promise<void>((resolve) => { releaseSteer = resolve; });
53
+ session.steer = async () => { entered = true; await blocked; };
54
+ try {
55
+ requestAsyncSteer(asyncDir, { id: "queued", message: "later", mode: "follow_up" });
56
+ requestAsyncSteer(asyncDir, { id: "unresolved", message: "pending" });
57
+ await waitForAsyncState(runId, (candidate) => entered && recentSteering(candidate, "queued")?.state === "queued" && recentSteering(candidate, "unresolved")?.state === "routed", 15_000);
58
+ fs.rmSync(indexPath, { recursive: true, force: true });
59
+ fs.mkdirSync(RESULTS_DIR, { recursive: true });
60
+ fs.writeFileSync(indexPath, "not a directory");
61
+ if (outcome === "stopped") {
62
+ const stop = await executor.execute("stop-index-steering", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx);
63
+ assert.notEqual(stop.isError, true);
64
+ } else fs.writeFileSync(release, "go");
65
+ await waitForAsyncEvent(runId, "subagent.workflow.result_write_failed");
66
+ const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
67
+ const status = JSON.parse(before) as { state: string; endedAt?: number; activityState?: string; steering: SteeringStatus };
68
+ assert.equal(status.state, "running", "failed index publication cannot authorize terminal status");
69
+ assert.equal(status.endedAt, undefined);
70
+ assert.equal(status.activityState, "needs_attention");
71
+ assert.equal(status.steering.pending, 0);
72
+ assert.equal(status.steering.failed, 2);
73
+ assert.equal(status.steering.delivered, 0);
74
+ assert.ok(status.steering.recent.every((request) => request.targets[0]?.state === "failed" && request.targets[0].reason === `run became ${outcome} before steering delivery settled; delivery unconfirmed`));
75
+ assert.equal(fs.existsSync(path.join(ASYNC_DIR, ".active-runs", runId)), true);
76
+ assert.equal(fs.existsSync(path.join(RESULTS_DIR, `${runId}.json`)), false);
77
+ assert.equal(fs.existsSync(path.join(RESULTS_DIR, "result-pending", "session-index-steering", `${runId}.json`)), true);
78
+ const unpublished = JSON.parse(fs.readFileSync(path.join(RESULTS_DIR, "result-pending", "session-index-steering", `${runId}.json`), "utf8"));
79
+ assert.equal(unpublished.state, outcome, "computed outcome is retained only in unpublished result");
80
+ if (outcome !== "complete") assert.equal(unpublished.activityState, "needs_attention", "failure finalization preserves steering attention");
81
+ assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true);
82
+ const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
83
+ assert.equal(journal.split("\n").filter((line) => line.includes('"type":"subagent.steer.failed"')).length, 2);
84
+ assert.doesNotMatch(journal, /"type":"subagent.workflow.completed"/);
85
+ releaseSteer();
86
+ await new Promise((resolve) => setTimeout(resolve, 350));
87
+ assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before);
88
+ assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journal);
89
+ } finally {
90
+ session.steer = originalSteer;
91
+ releaseSteer();
92
+ fs.writeFileSync(release, "go");
93
+ fs.rmSync(indexPath, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ for (const shutdown of [false, true]) it(`accounts for 21 same-scan requests beyond display history (${shutdown ? "shutdown" : "delivered"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
98
+ const release = path.join(tempDir, "batch-accounting-release");
99
+ mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
100
+ const ctx = makeMinimalCtx(tempDir);
101
+ ctx.sessionManager.getSessionId = () => "session-batch-accounting";
102
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
103
+ const launch = await executor.execute("batch-accounting", { workflowScript: `return await runs.run("A", { agent: "worker", task: "Wait" });`, async: true }, new AbortController().signal, undefined, ctx);
104
+ assert.equal(launch.isError, undefined);
105
+ const runId = launch.details!.asyncId as string;
106
+ const asyncDir = path.join(ASYNC_DIR, runId);
107
+ await waitForMockPiCall(mockPi, 0, 10_000);
108
+ const session = mockPi.sessions[0]!.session!;
109
+ const originalSteer = session.steer.bind(session);
110
+ const originalFollowUp = session.followUp.bind(session);
111
+ let releaseSteers!: () => void;
112
+ const blocked = new Promise<void>((resolve) => { releaseSteers = resolve; });
113
+ let calls = 0;
114
+ session.followUp = async (text) => {
115
+ calls++;
116
+ assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), []);
117
+ await originalFollowUp(text);
118
+ };
119
+ session.steer = async (text) => {
120
+ calls++;
121
+ assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), [], "entire batch consumed before first delivery callback");
122
+ if (shutdown) await blocked;
123
+ else await originalSteer(text);
124
+ };
125
+ const accounting = (candidate: AsyncStatusPayload) => (candidate as { steering?: SteeringStatus }).steering;
126
+ try {
127
+ for (let index = 0; index < 21; index++) requestAsyncSteer(asyncDir, { id: `batch-${index}`, message: `batch-${index}`, targetIndex: 0, ts: index + 1, ...(shutdown && (index === 0 || index === 20) ? { mode: "follow_up" as const } : {}) });
128
+ const active = await waitForAsyncState(runId, (candidate) => calls === 21 && accounting(candidate)?.recent.every((request) => request.targets[0]?.state === (shutdown ? request.id === "batch-20" ? "queued" : "routed" : "delivered")) === true, 15_000);
129
+ assert.equal(accounting(active)!.requested, 21);
130
+ assert.equal(accounting(active)!.pending, shutdown ? 21 : 0, "queued acknowledgments still count once as pending");
131
+ if (shutdown) {
132
+ const queuedEvents = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.type === "subagent.steer.queued");
133
+ assert.deepEqual(queuedEvents.map((event) => event.requestId), ["batch-0", "batch-20"], "both evicted and retained queued receipts remain pending");
134
+ }
135
+ assert.equal(accounting(active)!.recent.length, 20);
136
+ assert.equal(accounting(active)!.recent.some((request) => request.id === "batch-0"), false, "first unresolved receipt was evicted");
137
+ fs.writeFileSync(release, "go");
138
+ const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
139
+ const counters = accounting(terminal)!;
140
+ assert.equal(counters.pending, 0);
141
+ assert.equal(counters.delivered, shutdown ? 0 : 21);
142
+ assert.equal(counters.failed, shutdown ? 21 : 0);
143
+ assert.equal(counters.recent.length, 20, "history cap must not grow");
144
+ if (shutdown) assert.ok(counters.recent.every((request) => request.targets[0]?.state === "failed" && request.targets[0].reason?.includes("delivery unconfirmed")));
145
+ const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
146
+ const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
147
+ const terminalEvents = journal.trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.requestId?.startsWith("batch-") && event.type === `subagent.steer.${shutdown ? "failed" : "delivered"}`);
148
+ assert.equal(terminalEvents.length, counters.delivered + counters.failed);
149
+ assert.equal(new Set(terminalEvents.map((event) => event.requestId)).size, 21, "every request has exactly one terminal event");
150
+ releaseSteers();
151
+ await new Promise((resolve) => setTimeout(resolve, 350));
152
+ assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before);
153
+ assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journal);
154
+ } finally {
155
+ session.steer = originalSteer;
156
+ session.followUp = originalFollowUp;
157
+ releaseSteers();
158
+ fs.writeFileSync(release, "go");
159
+ }
160
+ });
161
+
162
+ for (const stop of [false, true]) it(`settles consumed pending deliveries before ${stop ? "stop" : "completion"} and ignores late callbacks`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
163
+ const release = path.join(tempDir, "pending-steer-release");
164
+ mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("done")] }] });
165
+ const ctx = makeMinimalCtx(tempDir);
166
+ ctx.sessionManager.getSessionId = () => "session-pending-steer";
167
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
168
+ const launch = await executor.execute("pending-steer", { workflowScript: `return await runs.run("A", { agent: "worker", task: "Wait" });`, async: true }, new AbortController().signal, undefined, ctx);
169
+ assert.equal(launch.isError, undefined);
170
+ const runId = launch.details!.asyncId as string;
171
+ const asyncDir = path.join(ASYNC_DIR, runId);
172
+ await waitForMockPiCall(mockPi, 0, 10_000);
173
+ const session = mockPi.sessions[0]!.session!;
174
+ const originalSteer = session.steer;
175
+ let settle!: () => void;
176
+ let entered = false;
177
+ session.steer = async () => {
178
+ entered = true;
179
+ await new Promise<void>((resolve, reject) => { settle = stop ? () => reject(new Error("late rejection")) : resolve; });
180
+ };
181
+ try {
182
+ requestAsyncSteer(asyncDir, { id: "pending", message: "pending", targetIndex: 0 });
183
+ await waitForAsyncState(runId, (candidate) => entered && recentSteering(candidate, "pending")?.state === "routed", 15_000);
184
+ assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), [], "pending request is consumed, not available to file drain");
185
+ if (stop) {
186
+ requestAsyncSteer(asyncDir, { id: "file-resident", message: "not consumed", targetIndexes: [0, 99] });
187
+ const stopped = await executor.execute("stop-pending", { action: "stop", id: runId }, new AbortController().signal, undefined, ctx);
188
+ assert.notEqual(stopped.isError, true, stopped.content[0]?.text);
189
+ } else fs.writeFileSync(release, "go");
190
+ const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === (stop ? "stopped" : "complete"), 15_000);
191
+ assert.equal(recentSteering(terminal, "pending")?.state, "failed");
192
+ assert.match(recentSteering(terminal, "pending")!.reason!, /before steering delivery settled/);
193
+ assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true);
194
+ if (stop) {
195
+ const targets = (terminal as SteeringTargets).steering!.recent.find((request) => request.id === "file-resident")!.targets;
196
+ assert.deepEqual(targets.map(({ index, state }) => [index, state]), [[0, "failed"], [99, "failed"]]);
197
+ assert.ok(targets.every(({ reason }) => reason?.includes("before steering request was consumed")));
198
+ }
199
+ const before = fs.readFileSync(path.join(asyncDir, "status.json"), "utf8");
200
+ const journalBefore = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8");
201
+ assert.equal(journalBefore.split("\n").filter((line) => line.includes('"type":"subagent.steer.failed"') && line.includes('"requestId":"pending"')).length, 1);
202
+ settle();
203
+ await new Promise((resolve) => setTimeout(resolve, 350));
204
+ assert.equal(fs.readFileSync(path.join(asyncDir, "status.json"), "utf8"), before, "late callback must not rewrite durable terminal state");
205
+ assert.equal(fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8"), journalBefore, "late callback must not append contradictory evidence");
206
+ } finally {
207
+ session.steer = originalSteer;
208
+ settle?.();
209
+ fs.writeFileSync(release, "go");
210
+ }
211
+ });
212
+
213
+ it("delivers an inbox steer request to a live async workflow child", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
214
+ const release = path.join(tempDir, "workflow-steer-release");
215
+ mockPi.onCall({ steps: [{ waitForPath: release, jsonl: [events.assistantMessage("steered workflow child")] }] });
216
+ const ctx = makeMinimalCtx(tempDir);
217
+ ctx.sessionManager.getSessionId = () => "session-workflow-steer";
218
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
219
+
220
+ const launch = await executor.execute(
221
+ `workflow-steer-inbox-${Date.now().toString(36)}`,
222
+ { workflowScript: `return await runs.run("waits", { agent: "worker", task: "Wait for guidance" });`, async: true },
223
+ new AbortController().signal,
224
+ undefined,
225
+ ctx,
226
+ );
227
+ assert.equal(launch.isError, undefined, launch.content[0]?.text);
228
+ const runId = launch.details?.asyncId as string;
229
+ assert.ok(runId, "expected an async workflow run id");
230
+ const asyncDir = path.join(ASYNC_DIR, runId);
231
+
232
+ try {
233
+ // The child is now live and parked on the release path, so the workflow run is genuinely
234
+ // steerable: the only question is whether anything consumes its inbox.
235
+ await waitForMockPiCall(mockPi, 0, 10_000);
236
+ requestAsyncSteer(asyncDir, { message: "Focus on the failing test.", id: "workflow-steer-1", ts: Date.now() });
237
+
238
+ const status = await waitForAsyncState(runId, (candidate) => recentSteering(candidate, "workflow-steer-1") !== undefined, 15_000);
239
+ const target = recentSteering(status, "workflow-steer-1");
240
+ assert.equal(target?.state, "delivered", `expected a real delivery, got ${JSON.stringify(target)}`);
241
+ // The request file must be consumed, not left on disk for the life of the run.
242
+ assert.deepEqual(fs.readdirSync(steerRequestsDir(asyncDir)), []);
243
+
244
+ const steers = fs.readFileSync(path.join(mockPi.dir, "steers.jsonl"), "utf-8").trim().split("\n").map((line) => JSON.parse(line) as { text: string });
245
+ assert.match(steers[0]?.text ?? "", /Focus on the failing test\./, "the live child must receive the steer text");
246
+
247
+ const eventsText = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf-8");
248
+ assert.match(eventsText, /"type":"subagent\.steer\.routed"[^\n]*"requestId":"workflow-steer-1"/);
249
+ assert.match(eventsText, /"type":"subagent\.steer\.delivered"[^\n]*"requestId":"workflow-steer-1"/);
250
+ requestAsyncSteer(asyncDir, { message: "Follow up later", id: "workflow-follow-up", mode: "follow_up" });
251
+ const queued = await waitForAsyncState(runId, (candidate) => recentSteering(candidate, "workflow-follow-up")?.state === "queued", 15_000);
252
+ const counters = (queued as { steering: SteeringStatus }).steering;
253
+ assert.equal(counters.requested, 2);
254
+ assert.equal(counters.delivered, 1);
255
+ assert.equal(counters.pending, 1, "routed to queued must not double-count the pending target");
256
+ } finally {
257
+ fs.writeFileSync(release, "go");
258
+ }
259
+
260
+ const payload = await readAsyncPayload(runId);
261
+ assert.equal(payload.success, true);
262
+ const terminal = await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
263
+ const counters = (terminal as { steering: SteeringStatus }).steering;
264
+ assert.equal(counters.pending, 0);
265
+ assert.equal(counters.delivered, 1);
266
+ assert.equal(counters.failed, 1);
267
+ assert.equal(recentSteering(terminal, "workflow-follow-up")?.state, "failed");
268
+ assert.match(recentSteering(terminal, "workflow-follow-up")!.reason!, /delivery unconfirmed/);
269
+ });
270
+
271
+ for (const parallel of [false, true]) it(`routes every explicit target without sibling fallback (${parallel ? "parallel" : "sequential"})`, { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
272
+ const releaseA = path.join(tempDir, "target-release-a");
273
+ const releaseB = path.join(tempDir, "target-release-b");
274
+ mockPi.onCall({ steps: [{ waitForPath: releaseA, jsonl: [events.assistantMessage("A done")] }] });
275
+ mockPi.onCall({ steps: [{ waitForPath: releaseB, jsonl: [events.assistantMessage("B done")] }] });
276
+ const ctx = makeMinimalCtx(tempDir);
277
+ ctx.sessionManager.getSessionId = () => "session-workflow-targets";
278
+ const executor = makeAsyncExecutor([makeAgent("worker", { completionGuard: false })]);
279
+ const launch = await executor.execute("workflow-targets", {
280
+ workflowScript: parallel
281
+ ? `return await Promise.all([runs.run("A", { agent: "worker", task: "A" }), runs.run("B", { agent: "worker", task: "B" })]);`
282
+ : `await runs.run("A", { agent: "worker", task: "A" }); return await runs.run("B", { agent: "worker", task: "B" });`,
283
+ async: true,
284
+ }, new AbortController().signal, undefined, ctx);
285
+ assert.equal(launch.isError, undefined, launch.content[0]?.text);
286
+ const runId = launch.details?.asyncId as string;
287
+ const asyncDir = path.join(ASYNC_DIR, runId);
288
+ const deliveredTexts = (): string[] => {
289
+ const file = path.join(mockPi.dir, "steers.jsonl");
290
+ return fs.existsSync(file) ? fs.readFileSync(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line).text) : [];
291
+ };
292
+ const check = async (id: string, address: { targetIndex?: number; targetIndexes?: number[] }, expected: Array<[number, string]>, deliveries: number, alreadyWritten = false) => {
293
+ if (!alreadyWritten) requestAsyncSteer(asyncDir, { id, message: id, ...address });
294
+ const status = await waitForAsyncState(runId, (candidate) => {
295
+ const targets = (candidate as SteeringTargets).steering?.recent.find((request) => request.id === id)?.targets;
296
+ return targets !== undefined && targets.every((target) => target.state === "delivered" || target.state === "failed");
297
+ }, 15_000);
298
+ const targets = (status as SteeringTargets).steering!.recent.find((request) => request.id === id)!.targets;
299
+ assert.deepEqual(targets.map(({ index, state }) => [index, state]), expected);
300
+ assert.equal(deliveredTexts().filter((text) => text.includes(id)).length, deliveries);
301
+ const journal = fs.readFileSync(path.join(asyncDir, "events.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line)).filter((event) => event.requestId === id);
302
+ assert.equal(journal.filter((event) => event.type === "subagent.steer.requested").length, 1);
303
+ for (const [index, state] of expected) assert.equal(journal.filter((event) => event.index === index && event.type === `subagent.steer.${state}`).length, 1);
304
+ return targets;
305
+ };
306
+ try {
307
+ await waitForMockPiCall(mockPi, 0, 10_000);
308
+ // Hold a transport-written request outside the watched inbox until A disappears.
309
+ // This controls queue-to-consumption delay without changing the watcher or child lifecycle.
310
+ const heldDir = path.join(tempDir, "held-steer");
311
+ if (!parallel) {
312
+ requestAsyncSteer(heldDir, { id: "disappeared-A", message: "disappeared-A", targetIndex: 0 });
313
+ fs.writeFileSync(releaseA, "go");
314
+ }
315
+ await waitForMockPiCall(mockPi, 1, 10_000);
316
+ await waitForAsyncState(runId, (candidate) => (candidate.steps?.length ?? 0) === 2, 15_000);
317
+ if (parallel) {
318
+ const targets = await check("ambiguous", {}, [[0, "failed"]], 0);
319
+ assert.match(targets[0]!.reason!, /2 live children/);
320
+ } else {
321
+ for (const file of fs.readdirSync(steerRequestsDir(heldDir))) fs.renameSync(path.join(steerRequestsDir(heldDir), file), path.join(steerRequestsDir(asyncDir), file));
322
+ await check("disappeared-A", {}, [[0, "failed"]], 0, true);
323
+ const targets = await check("dead-A", { targetIndex: 0 }, [[0, "failed"]], 0);
324
+ assert.match(targets[0]!.reason!, /not live/);
325
+ await check("unique-B", {}, [[1, "delivered"]], 1);
326
+ }
327
+ await check("aggregate", { targetIndexes: [0, 1] }, [[0, parallel ? "delivered" : "failed"], [1, "delivered"]], parallel ? 2 : 1);
328
+ if (parallel) {
329
+ const deliveries = fs.readFileSync(path.join(mockPi.dir, "steers.jsonl"), "utf8").trim().split("\n").map((line) => JSON.parse(line) as { text: string; sessionId: string });
330
+ assert.equal(new Set(deliveries.filter(({ text }) => text.includes("aggregate")).map(({ sessionId }) => sessionId)).size, 2, "aggregate must reach two distinct children");
331
+ }
332
+ await check("out-of-range", { targetIndex: 99 }, [[99, "failed"]], 0);
333
+ await check("mixed-range", { targetIndexes: [1, 99] }, [[1, "delivered"], [99, "failed"]], 1);
334
+ } finally {
335
+ fs.writeFileSync(releaseA, "go");
336
+ fs.writeFileSync(releaseB, "go");
337
+ }
338
+ assert.equal((await readAsyncPayload(runId)).success, true);
339
+ });
340
+
341
+ it("closes the inbox and fails a late steer request when the workflow ends", { skip: !isAsyncAvailable() || !createSubagentExecutor ? "jiti or executor not available" : undefined }, async () => {
342
+ const ctx = makeMinimalCtx(tempDir);
343
+ ctx.sessionManager.getSessionId = () => "session-workflow-steer-closed";
344
+ const executor = makeAsyncExecutor([]);
345
+
346
+ const launch = await executor.execute(
347
+ `workflow-steer-closed-${Date.now().toString(36)}`,
348
+ { workflowScript: "return 'done'", async: true },
349
+ new AbortController().signal,
350
+ undefined,
351
+ ctx,
352
+ );
353
+ assert.equal(launch.isError, undefined, launch.content[0]?.text);
354
+ const runId = launch.details?.asyncId as string;
355
+ const asyncDir = path.join(ASYNC_DIR, runId);
356
+
357
+ await waitForAsyncState(runId, (candidate) => candidate.state === "complete", 15_000);
358
+ // A terminal run must refuse new steering rather than accept a request nothing will read.
359
+ assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true, "the steer inbox must be closed at teardown");
360
+ assert.throws(
361
+ () => requestAsyncSteer(asyncDir, { message: "Too late.", id: "workflow-steer-late", ts: Date.now() }),
362
+ /no longer accepts steering requests/,
363
+ );
364
+ });
365
+ });
@@ -0,0 +1,31 @@
1
+ import assert from "node:assert/strict";
2
+ import { pathToFileURL } from "node:url";
3
+ import { AgentSession } from "@selesai/code";
4
+ import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core";
5
+ import { createModels } from "@earendil-works/pi-ai";
6
+
7
+ const state = globalThis.__pi085Smoke = { AgentSession, BACKGROUND_CONTEXT, createModels, started: 0, stopped: 0, faux: undefined };
8
+ const { loadRunnerChildSessionFactory } = await import(pathToFileURL(`${process.env.SMOKE_EXTENSION}/src/runs/background/runner-child-sessions.ts`).href);
9
+ const factory = await loadRunnerChildSessionFactory({});
10
+ const errors = [];
11
+ let started = 0, stopped = 0;
12
+ try {
13
+ const child = await factory.create({
14
+ cwd: process.cwd(), storage: { kind: "memory" }, model: "pi085-smoke/local", tools: [],
15
+ extensionPaths: [`${process.cwd()}/pi085-extension.ts`], ambientExtensions: false,
16
+ noSkills: true, noContextFiles: true, runtime: {},
17
+ hooks: [{ name: "lifecycle", factory(pi) {
18
+ pi.on("session_start", () => { started++; });
19
+ pi.on("session_shutdown", () => { stopped++; });
20
+ } }], onExtensionError(error) { errors.push(error); },
21
+ });
22
+ try {
23
+ assert.equal(child.modelId, "pi085-smoke/local");
24
+ await child.prompt("Return the local scripted response.");
25
+ assert.equal(state.faux.state.callCount, 1);
26
+ assert.ok(child.messages.some(m => m.role === "assistant" && m.content.some(c => c.type === "text" && c.text === "local response verified")));
27
+ } finally { await child.dispose(); }
28
+ assert.deepEqual([started, stopped, state.started, state.stopped], [1, 1, 1, 1]);
29
+ assert.deepEqual(errors, []);
30
+ console.log("PASS public SDK/default child factory: local prompt, API identity, startup/shutdown");
31
+ } finally { await factory.dispose(); }
@@ -0,0 +1,74 @@
1
+ // Run with Node >=22.19.0: node --experimental-strip-types test/smoke/pi085-clean-install.mjs [artifact-dir] [0.85.0|0.85.1]
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const source = fileURLToPath(new URL("../../", import.meta.url));
10
+ const version = process.argv[3] ?? "0.85.0";
11
+ assert.ok(["0.85.0", "0.85.1"].includes(version), "requires an explicitly supported smoke version");
12
+ const isPi0850 = version === "0.85.0";
13
+ const root = process.argv[2] ? path.resolve(process.argv[2]) : fs.mkdtempSync(path.join(os.tmpdir(), "pi085-smoke-"));
14
+ fs.mkdirSync(root, { recursive: true });
15
+ const host = path.join(root, "host");
16
+ const extension = path.join(root, "extension");
17
+ const cwd = path.join(root, "cwd");
18
+ for (const dir of [host, extension, cwd, path.join(root, "home")]) fs.mkdirSync(dir, { recursive: true });
19
+ assert.ok(!fs.existsSync(path.join(host, "node_modules")), "requires a pristine host install");
20
+ assert.ok(!fs.existsSync(path.join(extension, "node_modules")), "requires a pristine extension install");
21
+ const env = {
22
+ ...process.env, HOME: path.join(root, "home"), USERPROFILE: path.join(root, "home"), SELESAI_CODING_AGENT_DIR: path.join(root, "agent"),
23
+ XDG_CACHE_HOME: path.join(root, "cache"), npm_config_cache: path.join(root, "npm-cache"),
24
+ NODE_COMPILE_CACHE: path.join(root, "node-cache"), JITI_FS_CACHE: "false", PI_OFFLINE: "1",
25
+ };
26
+ // Do not inherit loader/alias overrides or operator credentials into the child.
27
+ for (const key of Object.keys(env)) {
28
+ if (/API_KEY|TOKEN|SECRET|PASSWORD/.test(key) || ["NODE_OPTIONS", "NODE_PATH", "JITI_ALIAS"].includes(key)) delete env[key];
29
+ }
30
+ function run(name, command, args, workdir, extra = {}, success = true) {
31
+ const result = spawnSync(command, args, { cwd: workdir, env: { ...env, ...extra }, encoding: "utf8", timeout: 180000, maxBuffer: 10 * 1024 * 1024 });
32
+ fs.writeFileSync(path.join(root, `${name}.log`), `${result.stdout ?? ""}${result.stderr ?? ""}`);
33
+ assert.ifError(result.error);
34
+ if (success) assert.equal(result.status, 0, `${name}: ${result.stdout}\n${result.stderr}`);
35
+ else assert.notEqual(result.status, 0, `${name} unexpectedly passed`);
36
+ return result;
37
+ }
38
+ fs.writeFileSync(path.join(host, "package.json"), JSON.stringify({ private: true, dependencies: { "@selesai/code": version } }));
39
+ run("host-install", "npm", ["install", "--no-audit", "--no-fund"], host);
40
+ const packed = JSON.parse(run("pack", "npm", ["pack", "--json", "--pack-destination", root], source).stdout)[0];
41
+ assert.ok(packed.files.some(file => file.path === "runner-server-preload.mjs"), "preload must ship");
42
+ fs.writeFileSync(path.join(extension, "package.json"), JSON.stringify({ private: true, dependencies: { "pi-subagents": `file:${path.join(root, packed.filename)}` } }));
43
+ run("extension-install", "npm", ["install", "--no-audit", "--no-fund"], extension);
44
+ const installed = path.join(extension, "node_modules/pi-subagents");
45
+ const pi = path.join(host, "node_modules/@selesai/code");
46
+ const { createJiti } = await import(pathToFileURL(path.join(extension, "node_modules/jiti/lib/jiti.mjs")).href);
47
+ const jitiLoader = createJiti(import.meta.url, { fsCache: false });
48
+ const { resolveHostPeerAliases, findHostPeerPackageDir, resolvePackageSubpath } = await jitiLoader.import(path.join(installed, "src/runs/background/runner-aliases.ts"));
49
+ assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-server"), undefined, "host must remain missing server");
50
+ if (!isPi0850) assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-client"), undefined, "stable host must remain missing client");
51
+ const pristine = run("pristine-public-sdk", process.execPath, ["--input-type=module", "-e", `await import(${JSON.stringify(pathToFileURL(resolvePackageSubpath(pi, ".")).href)})`], cwd, {}, !isPi0850);
52
+ if (isPi0850) assert.match(pristine.stderr, /Cannot find package '@earendil-works\/pi-server'/);
53
+ const resolved = resolveHostPeerAliases(pi);
54
+ assert.deepEqual(resolved.missing, []);
55
+ assert.deepEqual(resolved.supplemental, isPi0850 ? ["@earendil-works/pi-server", "@earendil-works/pi-server/unix"] : []);
56
+ if (!isPi0850) {
57
+ for (const specifier of ["@earendil-works/pi-server", "@earendil-works/pi-server/unix", "@earendil-works/pi-client/unix"]) assert.equal(resolved.aliases[specifier], undefined);
58
+ }
59
+ fs.writeFileSync(path.join(root, "aliases.json"), JSON.stringify(resolved, null, 2));
60
+ for (const file of ["pi085-child.ts", "pi085-extension.ts"]) fs.copyFileSync(new URL(file, import.meta.url), path.join(cwd, file));
61
+ const childEnv = { SMOKE_EXTENSION: installed, JITI_ALIAS: JSON.stringify(resolved.aliases) };
62
+ const jiti = path.join(extension, "node_modules/jiti/lib/jiti-cli.mjs");
63
+ const args = [jiti, path.join(cwd, "pi085-child.ts")];
64
+ // The real file-extension gate must fail with aliases alone, not just an import mock.
65
+ if (isPi0850) {
66
+ const negative = run("without-preload", process.execPath, args, cwd, childEnv, false);
67
+ assert.match(negative.stderr, /Model "pi085-smoke\/local" not found/);
68
+ }
69
+ const preload = resolved.supplemental.length ? ["--import", pathToFileURL(path.join(installed, "runner-server-preload.mjs")).href] : [];
70
+ const positive = run("child", process.execPath, [...preload, ...args], cwd, childEnv);
71
+ assert.match(positive.stdout, /PASS public SDK\/default child factory/);
72
+ assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-server"), undefined);
73
+ if (!isPi0850) assert.equal(findHostPeerPackageDir(pi, "@earendil-works/pi-client"), undefined);
74
+ console.log(`${positive.stdout.trim()}\nArtifacts: ${root}`);
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import { AgentSession } from "@selesai/code";
3
+ import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core";
4
+ import { createModels, fauxProvider, fauxAssistantMessage } from "@earendil-works/pi-ai";
5
+
6
+ export default function(pi) {
7
+ const state = globalThis.__pi085Smoke;
8
+ assert.equal(AgentSession, state.AgentSession);
9
+ assert.equal(BACKGROUND_CONTEXT, state.BACKGROUND_CONTEXT);
10
+ assert.equal(createModels, state.createModels);
11
+ state.faux = fauxProvider({ provider: "pi085-smoke", models: [{ id: "local" }], tokensPerSecond: 100000 });
12
+ state.faux.setResponses([fauxAssistantMessage("local response verified")]);
13
+ pi.registerProvider(state.faux.provider);
14
+ pi.on("session_start", () => { state.started++; });
15
+ pi.on("session_shutdown", () => { state.stopped++; });
16
+ }