@selesai/code 0.2.1 → 0.3.1

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 (284) hide show
  1. package/dist/config.d.ts +4 -15
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +5 -45
  4. package/dist/config.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +1 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +8 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/diagnostics.d.ts +1 -0
  10. package/dist/core/diagnostics.d.ts.map +1 -1
  11. package/dist/core/diagnostics.js.map +1 -1
  12. package/dist/core/model-registry.d.ts +4 -1
  13. package/dist/core/model-registry.d.ts.map +1 -1
  14. package/dist/core/model-registry.js +101 -51
  15. package/dist/core/model-registry.js.map +1 -1
  16. package/dist/core/package-manager.d.ts +10 -0
  17. package/dist/core/package-manager.d.ts.map +1 -1
  18. package/dist/core/package-manager.js +93 -8
  19. package/dist/core/package-manager.js.map +1 -1
  20. package/dist/core/package-manager.test.d.ts +2 -0
  21. package/dist/core/package-manager.test.d.ts.map +1 -0
  22. package/dist/core/package-manager.test.js +20 -0
  23. package/dist/core/package-manager.test.js.map +1 -0
  24. package/dist/core/resource-loader.d.ts +3 -0
  25. package/dist/core/resource-loader.d.ts.map +1 -1
  26. package/dist/core/resource-loader.js +10 -0
  27. package/dist/core/resource-loader.js.map +1 -1
  28. package/dist/core/settings-manager.d.ts +1 -0
  29. package/dist/core/settings-manager.d.ts.map +1 -1
  30. package/dist/core/settings-manager.js.map +1 -1
  31. package/dist/defaults/models.json +64 -113
  32. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  33. package/dist/extensions/caveman/index.js +116 -0
  34. package/dist/extensions/caveman/package.json +8 -0
  35. package/dist/extensions/caveman/test/extension.test.js +185 -0
  36. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  37. package/dist/extensions/copy-turn.ts +2 -2
  38. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  39. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  40. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  41. package/dist/extensions/pi-intercom/config.ts +2 -2
  42. package/dist/extensions/pi-intercom/index.ts +2 -2
  43. package/dist/extensions/pi-intercom/package.json +2 -2
  44. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  45. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  46. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  47. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
  48. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  49. package/dist/extensions/pi-powerline-footer/index.ts +40 -47
  50. package/dist/extensions/pi-powerline-footer/package.json +2 -2
  51. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
  52. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
  53. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
  54. package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
  55. package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
  56. package/dist/extensions/pi-powerline-footer/types.ts +7 -7
  57. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  58. package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
  59. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  60. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  61. package/dist/extensions/pi-subagents/README.md +224 -52
  62. package/dist/extensions/pi-subagents/agents/architect.md +188 -0
  63. package/dist/extensions/pi-subagents/agents/builder.md +120 -0
  64. package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
  65. package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
  66. package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
  67. package/dist/extensions/pi-subagents/install.mjs +93 -0
  68. package/dist/extensions/pi-subagents/package.json +4 -5
  69. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  70. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  71. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  72. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  73. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  74. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  75. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  76. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  77. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  78. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  79. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  80. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  81. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  82. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  83. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  84. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  85. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  86. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  87. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  88. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  89. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  90. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  91. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  92. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  93. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  94. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  95. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  96. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  97. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  98. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  99. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  100. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  101. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  102. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  103. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  104. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  105. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  106. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  107. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  108. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  109. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  110. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  111. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  112. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  113. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  114. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  115. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  116. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  117. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  118. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  119. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  120. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  121. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  122. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  123. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  124. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  125. package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
  126. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  127. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  128. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  129. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  130. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  131. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  132. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  133. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  134. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  135. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  136. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  137. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  138. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  139. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  140. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  141. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  142. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  143. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  144. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  145. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  146. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  147. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  148. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  149. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  150. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  151. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  152. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  153. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  154. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  155. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  156. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  157. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  158. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  159. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  160. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  161. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  162. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  163. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  164. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  165. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  166. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  167. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  168. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  169. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  170. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  171. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  172. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  173. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  174. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  175. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  176. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  177. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  178. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  179. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  180. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  181. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  182. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  183. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  184. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  185. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  186. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  187. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  188. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  189. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  190. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  191. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  192. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  193. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  194. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  195. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  196. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  197. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  198. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  199. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  200. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  201. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  202. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  203. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  204. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  205. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  206. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  207. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  208. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  209. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  210. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  211. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  212. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  213. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  215. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  216. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  217. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  218. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  219. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  220. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  221. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  222. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  223. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  224. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  225. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  226. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  227. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  228. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  229. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  230. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  231. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  232. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  233. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  234. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  235. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  236. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  237. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  238. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  239. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  240. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  241. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  242. package/dist/extensions/pi-web-agent/package.json +2 -2
  243. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
  244. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  245. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
  246. package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
  247. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  248. package/dist/extensions/question/helpers.ts +3 -3
  249. package/dist/extensions/question/index.ts +2 -2
  250. package/dist/extensions/question/navigation.ts +2 -2
  251. package/dist/extensions/question/package.json +2 -2
  252. package/dist/extensions/question/question-list.ts +2 -2
  253. package/dist/extensions/question/tests/question-list.test.ts +2 -2
  254. package/dist/extensions/question/tui-adapter.ts +2 -2
  255. package/dist/extensions/question/types.ts +2 -2
  256. package/dist/extensions/question/ui-protocol.ts +2 -2
  257. package/dist/extensions/rtk.ts +2 -2
  258. package/dist/extensions/tokenin-onboarding.ts +2 -2
  259. package/dist/extensions/undo.ts +1 -1
  260. package/dist/extensions/web-agent-onboarding.ts +3 -3
  261. package/dist/extensions/workflow/adapter.ts +16 -8
  262. package/dist/extensions/workflow/extension.ts +1 -1
  263. package/dist/extensions/workflow/package.json +1 -1
  264. package/dist/main.d.ts.map +1 -1
  265. package/dist/main.js +4 -0
  266. package/dist/main.js.map +1 -1
  267. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  268. package/dist/modes/interactive/components/assistant-message.js +2 -0
  269. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  270. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  271. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  272. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  273. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  274. package/dist/skills/caveman/SKILL.md +49 -0
  275. package/dist/skills/handoff-text/SKILL.md +13 -0
  276. package/dist/utils/thinking-tags.d.ts +7 -0
  277. package/dist/utils/thinking-tags.d.ts.map +1 -0
  278. package/dist/utils/thinking-tags.js +63 -0
  279. package/dist/utils/thinking-tags.js.map +1 -0
  280. package/docs/docs.json +4 -0
  281. package/docs/extensions.md +2 -0
  282. package/docs/settings.md +15 -0
  283. package/docs/shared-host-extensions.md +109 -0
  284. package/package.json +2 -2
@@ -0,0 +1,517 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, describe, it } from "node:test";
6
+ import { writeSteerRequestToDir } from "../../src/runs/background/control-channel.ts";
7
+ import {
8
+ SUBAGENT_CHILD_AGENT_ENV,
9
+ SUBAGENT_CHILD_INDEX_ENV,
10
+ SUBAGENT_FANOUT_CHILD_ENV,
11
+ SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,
12
+ SUBAGENT_ORCHESTRATOR_TARGET_ENV,
13
+ SUBAGENT_RUN_ID_ENV,
14
+ SUBAGENT_STEER_INBOX_ENV,
15
+ SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
16
+ } from "../../src/runs/shared/pi-args.ts";
17
+ import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "../../src/runs/shared/structured-output.ts";
18
+ import { TOOL_BUDGET_ENV } from "../../src/runs/shared/tool-budget.ts";
19
+ import registerSubagentPromptRuntime, {
20
+ CHILD_FANOUT_BOUNDARY_INSTRUCTIONS,
21
+ CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS,
22
+ SUBAGENT_INTERCOM_SESSION_NAME_ENV,
23
+ rewriteSubagentPrompt,
24
+ stripInheritedSkills,
25
+ stripParentOnlySubagentMessages,
26
+ stripProjectContext,
27
+ stripSubagentOrchestrationSkill,
28
+ } from "../../src/runs/shared/subagent-prompt-runtime.ts";
29
+
30
+ const envSnapshot = {
31
+ SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT: process.env.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT,
32
+ SELESAI_SUBAGENT_INHERIT_SKILLS: process.env.SELESAI_SUBAGENT_INHERIT_SKILLS,
33
+ SELESAI_SUBAGENT_INTERCOM_SESSION_NAME: process.env.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME,
34
+ SELESAI_SUBAGENT_FANOUT_CHILD: process.env.SELESAI_SUBAGENT_FANOUT_CHILD,
35
+ SELESAI_SUBAGENT_STEER_INBOX: process.env.SELESAI_SUBAGENT_STEER_INBOX,
36
+ SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE,
37
+ SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA: process.env.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA,
38
+ SELESAI_SUBAGENT_TOOL_BUDGET: process.env.SELESAI_SUBAGENT_TOOL_BUDGET,
39
+ SELESAI_SUBAGENT_ORCHESTRATOR_TARGET: process.env.SELESAI_SUBAGENT_ORCHESTRATOR_TARGET,
40
+ SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID: process.env.SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID,
41
+ SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR: process.env.SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR,
42
+ SELESAI_SUBAGENT_RUN_ID: process.env.SELESAI_SUBAGENT_RUN_ID,
43
+ SELESAI_SUBAGENT_CHILD_AGENT: process.env.SELESAI_SUBAGENT_CHILD_AGENT,
44
+ SELESAI_SUBAGENT_CHILD_INDEX: process.env.SELESAI_SUBAGENT_CHILD_INDEX,
45
+ };
46
+
47
+ const SKILLS_SECTION = "\n\nThe following skills provide specialized instructions for specific tasks.\nUse the read tool to load a skill's file when the task matches its description.\nWhen a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\n\n<available_skills>\n <skill>\n <name>safe-bash</name>\n <description>desc</description>\n <location>/tmp/SKILL.md</location>\n </skill>\n <skill>\n <name>pi-subagents</name>\n <description>delegate to subagents</description>\n <location>/tmp/pi-subagents/SKILL.md</location>\n </skill>\n</available_skills>";
48
+
49
+ const BASE_PROMPT = [
50
+ "You are a subagent.",
51
+ "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n## /repo/AGENTS.md\n\nProject rules\n\n",
52
+ SKILLS_SECTION,
53
+ "\nCurrent date: 2026-04-16",
54
+ "\nCurrent working directory: /repo",
55
+ ].join("");
56
+
57
+ const PROMPT_WITH_EXPLICIT_SKILL = [
58
+ "You are a subagent.\n\n<skill name=\"explicit\">\nKeep this section\n</skill>",
59
+ "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n## /repo/AGENTS.md\n\nProject rules\n\n",
60
+ SKILLS_SECTION,
61
+ "\nCurrent date: 2026-04-16",
62
+ ].join("");
63
+
64
+ const CONFIGURED_SKILLS_SECTION = "\n\nThe following configured skills are available to this subagent.\nUse the read tool to load a skill's file when the task matches its description.\nWhen a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\n\n<available_skills>\n <skill>\n <name>configured-skill</name>\n <description>explicit agent skill</description>\n <location>/tmp/configured-skill/SKILL.md</location>\n </skill>\n</available_skills>";
65
+
66
+ afterEach(() => {
67
+ if (envSnapshot.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT === undefined) delete process.env.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT;
68
+ else process.env.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT = envSnapshot.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT;
69
+ if (envSnapshot.SELESAI_SUBAGENT_INHERIT_SKILLS === undefined) delete process.env.SELESAI_SUBAGENT_INHERIT_SKILLS;
70
+ else process.env.SELESAI_SUBAGENT_INHERIT_SKILLS = envSnapshot.SELESAI_SUBAGENT_INHERIT_SKILLS;
71
+ if (envSnapshot.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME === undefined) delete process.env.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME;
72
+ else process.env.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME = envSnapshot.SELESAI_SUBAGENT_INTERCOM_SESSION_NAME;
73
+ if (envSnapshot.SELESAI_SUBAGENT_FANOUT_CHILD === undefined) delete process.env.SELESAI_SUBAGENT_FANOUT_CHILD;
74
+ else process.env.SELESAI_SUBAGENT_FANOUT_CHILD = envSnapshot.SELESAI_SUBAGENT_FANOUT_CHILD;
75
+ if (envSnapshot.SELESAI_SUBAGENT_STEER_INBOX === undefined) delete process.env[SUBAGENT_STEER_INBOX_ENV];
76
+ else process.env[SUBAGENT_STEER_INBOX_ENV] = envSnapshot.SELESAI_SUBAGENT_STEER_INBOX;
77
+ if (envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE === undefined) delete process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
78
+ else process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_CAPTURE;
79
+ if (envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA === undefined) delete process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
80
+ else process.env[STRUCTURED_OUTPUT_SCHEMA_ENV] = envSnapshot.SELESAI_SUBAGENT_STRUCTURED_OUTPUT_SCHEMA;
81
+ if (envSnapshot.SELESAI_SUBAGENT_TOOL_BUDGET === undefined) delete process.env[TOOL_BUDGET_ENV];
82
+ else process.env[TOOL_BUDGET_ENV] = envSnapshot.SELESAI_SUBAGENT_TOOL_BUDGET;
83
+ if (envSnapshot.SELESAI_SUBAGENT_ORCHESTRATOR_TARGET === undefined) delete process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV];
84
+ else process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = envSnapshot.SELESAI_SUBAGENT_ORCHESTRATOR_TARGET;
85
+ if (envSnapshot.SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID === undefined) delete process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV];
86
+ else process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV] = envSnapshot.SELESAI_SUBAGENT_ORCHESTRATOR_SESSION_ID;
87
+ if (envSnapshot.SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR === undefined) delete process.env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV];
88
+ else process.env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV] = envSnapshot.SELESAI_SUBAGENT_SUPERVISOR_CHANNEL_DIR;
89
+ if (envSnapshot.SELESAI_SUBAGENT_RUN_ID === undefined) delete process.env[SUBAGENT_RUN_ID_ENV];
90
+ else process.env[SUBAGENT_RUN_ID_ENV] = envSnapshot.SELESAI_SUBAGENT_RUN_ID;
91
+ if (envSnapshot.SELESAI_SUBAGENT_CHILD_AGENT === undefined) delete process.env[SUBAGENT_CHILD_AGENT_ENV];
92
+ else process.env[SUBAGENT_CHILD_AGENT_ENV] = envSnapshot.SELESAI_SUBAGENT_CHILD_AGENT;
93
+ if (envSnapshot.SELESAI_SUBAGENT_CHILD_INDEX === undefined) delete process.env[SUBAGENT_CHILD_INDEX_ENV];
94
+ else process.env[SUBAGENT_CHILD_INDEX_ENV] = envSnapshot.SELESAI_SUBAGENT_CHILD_INDEX;
95
+ });
96
+
97
+ function setSupervisorEnv(): void {
98
+ process.env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = "subagent-chat-parent";
99
+ process.env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV] = "session-parent";
100
+ process.env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV] = path.join(os.tmpdir(), "subagent-supervisor-runtime-test");
101
+ process.env[SUBAGENT_RUN_ID_ENV] = "run-123";
102
+ process.env[SUBAGENT_CHILD_AGENT_ENV] = "worker";
103
+ process.env[SUBAGENT_CHILD_INDEX_ENV] = "0";
104
+ }
105
+
106
+ describe("subagent prompt runtime", () => {
107
+ it("nudges after the tool budget soft limit and blocks configured tools after hard", () => {
108
+ const handlers = new Map<string, (payload: { toolName?: string }) => unknown>();
109
+ const sent: string[] = [];
110
+ process.env[TOOL_BUDGET_ENV] = JSON.stringify({ soft: 2, hard: 2, block: ["read"] });
111
+
112
+ registerSubagentPromptRuntime({
113
+ on(event: string, handler: (payload: { toolName?: string }) => unknown) {
114
+ handlers.set(event, handler);
115
+ },
116
+ sendUserMessage(content: string) {
117
+ sent.push(content);
118
+ },
119
+ } as { on(event: string, handler: (payload: { toolName?: string }) => unknown): void; sendUserMessage(content: string): void });
120
+
121
+ const toolCall = handlers.get("tool_call");
122
+ assert.ok(toolCall, "tool_call handler should be registered");
123
+ assert.equal(toolCall({ toolName: "grep" }), undefined);
124
+ assert.equal(toolCall({ toolName: "grep" }), undefined);
125
+ assert.equal(sent.length, 1);
126
+ assert.match(sent[0] ?? "", /soft limit reached/);
127
+ assert.deepEqual(toolCall({ toolName: "read" }), {
128
+ block: true,
129
+ reason: "Tool budget hard limit reached after 3 tool calls (hard 2). The 'read' tool is blocked so you can finalize from the context you already have.",
130
+ });
131
+ assert.equal(toolCall({ toolName: "write" }), undefined);
132
+ });
133
+
134
+ it("delivers steering inbox requests as mid-run user messages", () => {
135
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-steering-runtime-"));
136
+ try {
137
+ const inbox = path.join(dir, "steer");
138
+ process.env[SUBAGENT_STEER_INBOX_ENV] = inbox;
139
+ const handlers = new Map<string, (payload?: unknown) => unknown>();
140
+ const sent: Array<{ content: string; options: { deliverAs: string } }> = [];
141
+
142
+ registerSubagentPromptRuntime({
143
+ on(event: string, handler: (payload?: unknown) => unknown) {
144
+ handlers.set(event, handler);
145
+ },
146
+ sendUserMessage(content: string, options: { deliverAs: string }) {
147
+ sent.push({ content, options });
148
+ },
149
+ } as { on(event: string, handler: (payload?: unknown) => unknown): void; sendUserMessage(content: string, options: { deliverAs: string }): void });
150
+
151
+ writeSteerRequestToDir(inbox, { type: "steer", id: "steer-1", ts: 1, message: "Focus on tests." });
152
+ handlers.get("message_start")?.({});
153
+ handlers.get("session_shutdown")?.({});
154
+
155
+ assert.equal(sent.length, 1);
156
+ assert.equal(sent[0]?.options.deliverAs, "steer");
157
+ assert.match(sent[0]?.content ?? "", /Mid-run steering/);
158
+ assert.match(sent[0]?.content ?? "", /Focus on tests\./);
159
+ assert.deepEqual(fs.readdirSync(inbox).filter((entry) => entry.endsWith(".json")), []);
160
+ } finally {
161
+ fs.rmSync(dir, { recursive: true, force: true });
162
+ }
163
+ });
164
+
165
+ it("registered structured_output tool accepts valid schema output and writes the capture file", async () => {
166
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "subagent-structured-runtime-"));
167
+ try {
168
+ const schemaPath = path.join(dir, "schema.json");
169
+ const outputPath = path.join(dir, "output.json");
170
+ fs.writeFileSync(schemaPath, JSON.stringify({ type: "object", required: ["ok"], properties: { ok: { type: "boolean" } } }), "utf-8");
171
+ process.env[STRUCTURED_OUTPUT_SCHEMA_ENV] = schemaPath;
172
+ process.env[STRUCTURED_OUTPUT_CAPTURE_ENV] = outputPath;
173
+ let execute: ((_id: string, params: { value: unknown }) => Promise<{ terminate?: boolean }>) | undefined;
174
+
175
+ registerSubagentPromptRuntime({
176
+ registerTool(tool: { name: string; execute: (_id: string, params: { value: unknown }) => Promise<{ terminate?: boolean }> }) {
177
+ if (tool.name === "structured_output") execute = tool.execute;
178
+ },
179
+ on() {},
180
+ } as { registerTool(tool: { name: string; execute: (_id: string, params: { value: unknown }) => Promise<{ terminate?: boolean }> }): void; on(): void });
181
+
182
+ assert.ok(execute, "structured_output tool should be registered");
183
+ const result = await execute("tool-1", { value: { ok: true } });
184
+ assert.equal(result.terminate, true);
185
+ assert.deepEqual(JSON.parse(fs.readFileSync(outputPath, "utf-8")), { ok: true });
186
+ } finally {
187
+ fs.rmSync(dir, { recursive: true, force: true });
188
+ }
189
+ });
190
+
191
+ it("strips only the project context block", () => {
192
+ const rewritten = stripProjectContext(BASE_PROMPT);
193
+ assert.ok(!rewritten.includes("# Project Context"));
194
+ assert.ok(rewritten.includes("The following skills provide specialized instructions for specific tasks."));
195
+ assert.ok(rewritten.includes("Current date: 2026-04-16"));
196
+ });
197
+
198
+ it("strips only the inherited skills block", () => {
199
+ const rewritten = stripInheritedSkills(BASE_PROMPT);
200
+ assert.ok(rewritten.includes("# Project Context"));
201
+ assert.ok(!rewritten.includes("<available_skills>"));
202
+ assert.ok(rewritten.includes("Current date: 2026-04-16"));
203
+ });
204
+
205
+ it("can strip both inherited sections together", () => {
206
+ const rewritten = rewriteSubagentPrompt(BASE_PROMPT, {
207
+ inheritProjectContext: false,
208
+ inheritSkills: false,
209
+ });
210
+ assert.ok(!rewritten.includes("# Project Context"));
211
+ assert.ok(!rewritten.includes("<available_skills>"));
212
+ assert.ok(rewritten.includes("Current working directory: /repo"));
213
+ });
214
+
215
+ it("injects a child-only boundary that forbids proposing or running subagents", () => {
216
+ const rewritten = rewriteSubagentPrompt(BASE_PROMPT, {
217
+ inheritProjectContext: true,
218
+ inheritSkills: true,
219
+ });
220
+
221
+ assert.ok(rewritten.startsWith(CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS));
222
+ assert.ok(rewritten.includes("Do not propose or run subagents."));
223
+ assert.ok(rewritten.includes("If you need to edit files, use the available editing tools."));
224
+ assert.ok(!rewritten.includes("call the actual edit/write tools"));
225
+ assert.ok(rewritten.includes("Do not print tool-call syntax, patches, or pseudo-tool calls as text."));
226
+ assert.equal(rewriteSubagentPrompt(rewritten, { inheritProjectContext: true, inheritSkills: true }).indexOf(CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS), 0);
227
+ assert.equal(rewriteSubagentPrompt(rewritten, { inheritProjectContext: true, inheritSkills: true }).lastIndexOf(CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS), 0);
228
+ });
229
+
230
+ it("replaces inherited child boundaries with the fanout boundary when authorized", () => {
231
+ const strictPrompt = `${CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS}\n\n${BASE_PROMPT}`;
232
+ const rewritten = rewriteSubagentPrompt(strictPrompt, {
233
+ inheritProjectContext: true,
234
+ inheritSkills: true,
235
+ fanoutChild: true,
236
+ });
237
+
238
+ assert.ok(rewritten.startsWith(CHILD_FANOUT_BOUNDARY_INSTRUCTIONS));
239
+ assert.ok(rewritten.includes("You may use the `subagent` tool only for the fanout work explicitly requested in this task."));
240
+ assert.ok(rewritten.includes("If you need to edit files, use the available editing tools."));
241
+ assert.ok(!rewritten.includes("call the actual edit/write tools"));
242
+ assert.ok(!rewritten.includes("Do not propose or run subagents."));
243
+ assert.equal(rewritten.lastIndexOf(CHILD_FANOUT_BOUNDARY_INSTRUCTIONS), 0);
244
+ });
245
+
246
+ it("replaces inherited fanout boundaries with the strict boundary when fanout is not authorized", () => {
247
+ const fanoutPrompt = `${CHILD_FANOUT_BOUNDARY_INSTRUCTIONS}\n\n${BASE_PROMPT}`;
248
+ const rewritten = rewriteSubagentPrompt(fanoutPrompt, {
249
+ inheritProjectContext: true,
250
+ inheritSkills: true,
251
+ });
252
+
253
+ assert.ok(rewritten.startsWith(CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS));
254
+ assert.ok(!rewritten.includes("explicit fanout responsibility"));
255
+ assert.equal(rewritten.lastIndexOf(CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS), 0);
256
+ });
257
+
258
+ it("keeps explicitly injected skill content when inherited skills are stripped", () => {
259
+ const rewritten = rewriteSubagentPrompt(PROMPT_WITH_EXPLICIT_SKILL, {
260
+ inheritProjectContext: false,
261
+ inheritSkills: false,
262
+ });
263
+ assert.ok(rewritten.includes("<skill name=\"explicit\">"));
264
+ assert.ok(!rewritten.includes("<available_skills>"));
265
+ assert.ok(!rewritten.includes("# Project Context"));
266
+ });
267
+
268
+ it("keeps configured lazy skill references when inherited skills are stripped", () => {
269
+ const prompt = [
270
+ "You are a subagent.",
271
+ CONFIGURED_SKILLS_SECTION,
272
+ "\n\n# Project Context\n\nProject-specific instructions and guidelines:\n\n## /repo/AGENTS.md\n\nProject rules\n\n",
273
+ SKILLS_SECTION,
274
+ "\nCurrent date: 2026-04-16",
275
+ ].join("");
276
+ const rewritten = rewriteSubagentPrompt(prompt, {
277
+ inheritProjectContext: false,
278
+ inheritSkills: false,
279
+ });
280
+
281
+ assert.ok(rewritten.includes("<name>configured-skill</name>"));
282
+ assert.ok(rewritten.includes("/tmp/configured-skill/SKILL.md"));
283
+ assert.ok(!rewritten.includes("<name>safe-bash</name>"));
284
+ assert.ok(!rewritten.includes("# Project Context"));
285
+ });
286
+
287
+ it("strips the subagent orchestration skill even when inherited skills remain", () => {
288
+ const rewritten = rewriteSubagentPrompt(BASE_PROMPT, {
289
+ inheritProjectContext: true,
290
+ inheritSkills: true,
291
+ });
292
+
293
+ assert.ok(rewritten.includes("<name>safe-bash</name>"));
294
+ assert.ok(!rewritten.includes("<name>pi-subagents</name>"));
295
+ assert.ok(!rewritten.includes("delegate to subagents"));
296
+ });
297
+
298
+ it("strips explicit pi-subagents skill injection from child prompts", () => {
299
+ const prompt = "Before\n\n<skill name=\"pi-subagents\">\nDo not keep this.\n</skill>\n\n<skill name=\"safe-bash\">\nKeep this.\n</skill>\nAfter";
300
+ const rewritten = stripSubagentOrchestrationSkill(prompt);
301
+
302
+ assert.ok(!rewritten.includes("Do not keep this"));
303
+ assert.ok(rewritten.includes("<skill name=\"safe-bash\">"));
304
+ });
305
+
306
+ it("strips parent-only subagent custom messages from forked child context", () => {
307
+ const user = { role: "user", content: "Task" };
308
+ const instruction = { role: "custom", customType: "subagent-orchestration-instructions", content: "Subagent orchestration is enabled." };
309
+ const slashResult = { role: "custom", customType: "subagent-slash-result", content: "## Orchestration" };
310
+ const slashTextResult = { role: "custom", customType: "subagent-slash-text-result", content: "Subagent profiles" };
311
+ const notify = { role: "custom", customType: "subagent-notify", content: "Background task completed" };
312
+ const control = { role: "custom", customType: "subagent_control_notice", content: "needs attention" };
313
+ const otherCustom = { role: "custom", customType: "other", content: "keep" };
314
+
315
+ assert.deepEqual(stripParentOnlySubagentMessages([user, instruction, slashResult, slashTextResult, notify, control, otherCustom]), [user, otherCustom]);
316
+ });
317
+
318
+ it("strips prior parent subagent tool calls and results from forked child context", () => {
319
+ const user = { role: "user", content: "Task" };
320
+ const subagentResult = { role: "toolResult", toolName: "subagent", content: "subagent results" };
321
+ const readResult = { role: "toolResult", toolName: "read", content: "file contents" };
322
+ const mixedAssistant = {
323
+ role: "assistant",
324
+ content: [
325
+ { type: "text", text: "I will inspect the repo." },
326
+ { type: "toolCall", name: "subagent", input: { agent: "worker" } },
327
+ { type: "toolCall", name: "read", input: { path: "README.md" } },
328
+ ],
329
+ };
330
+ const pureSubagentCall = {
331
+ role: "assistant",
332
+ content: [{ type: "toolCall", name: "subagent", input: { agent: "reviewer" } }],
333
+ };
334
+
335
+ assert.deepEqual(
336
+ stripParentOnlySubagentMessages([user, subagentResult, readResult, mixedAssistant, pureSubagentCall]),
337
+ [
338
+ user,
339
+ readResult,
340
+ {
341
+ role: "assistant",
342
+ content: [
343
+ { type: "text", text: "I will inspect the repo." },
344
+ { type: "toolCall", name: "read", input: { path: "README.md" } },
345
+ ],
346
+ },
347
+ ],
348
+ );
349
+ });
350
+
351
+ it("preserves live nested subagent calls and results in fanout child context", () => {
352
+ const user = { role: "user", content: "Task" };
353
+ const subagentResult = { role: "toolResult", toolName: "subagent", content: "OK" };
354
+ const subagentCall = { role: "assistant", content: [{ type: "toolCall", name: "subagent", input: { agent: "delegate" } }] };
355
+ const instruction = { role: "custom", customType: "subagent-orchestration-instructions", content: "Subagent orchestration is enabled." };
356
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
357
+
358
+ assert.deepEqual(stripParentOnlySubagentMessages([user, subagentCall, subagentResult, instruction]), [user, subagentCall, subagentResult]);
359
+ });
360
+
361
+ it("defers native supervisor registration until runtime events and respects installed pi-intercom tools", async () => {
362
+ setSupervisorEnv();
363
+ const handlers = new Map<string, (payload?: unknown) => unknown>();
364
+ const registered: string[] = [];
365
+
366
+ registerSubagentPromptRuntime({
367
+ on(event: string, handler: (payload?: unknown) => unknown) {
368
+ handlers.set(event, handler);
369
+ },
370
+ getAllTools: () => [{ name: "intercom" }, { name: "contact_supervisor" }],
371
+ registerTool(tool: { name: string }) {
372
+ registered.push(tool.name);
373
+ },
374
+ } as { on(event: string, handler: (payload?: unknown) => unknown): void; getAllTools(): Array<{ name: string }>; registerTool(tool: { name: string }): void });
375
+
376
+ assert.deepEqual(registered, []);
377
+ handlers.get("session_start")?.({});
378
+ await handlers.get("before_agent_start")?.({ systemPrompt: BASE_PROMPT });
379
+ assert.deepEqual(registered, []);
380
+ });
381
+
382
+ it("keeps installed pi-intercom while filling only a missing child contact_supervisor tool", async () => {
383
+ setSupervisorEnv();
384
+ const handlers = new Map<string, (payload?: unknown) => unknown>();
385
+ const registered: string[] = [];
386
+
387
+ registerSubagentPromptRuntime({
388
+ on(event: string, handler: (payload?: unknown) => unknown) {
389
+ handlers.set(event, handler);
390
+ },
391
+ getAllTools: () => [{ name: "intercom" }, ...registered.map((name) => ({ name }))],
392
+ registerTool(tool: { name: string }) {
393
+ registered.push(tool.name);
394
+ },
395
+ } as { on(event: string, handler: (payload?: unknown) => unknown): void; getAllTools(): Array<{ name: string }>; registerTool(tool: { name: string }): void });
396
+
397
+ handlers.get("session_start")?.({});
398
+ await handlers.get("before_agent_start")?.({ systemPrompt: BASE_PROMPT });
399
+
400
+ assert.deepEqual(registered, ["contact_supervisor"]);
401
+ });
402
+
403
+ it("registers native supervisor tools at runtime when pi-intercom is absent", async () => {
404
+ setSupervisorEnv();
405
+ const handlers = new Map<string, (payload?: unknown) => unknown>();
406
+ const registered: string[] = [];
407
+
408
+ registerSubagentPromptRuntime({
409
+ on(event: string, handler: (payload?: unknown) => unknown) {
410
+ handlers.set(event, handler);
411
+ },
412
+ getAllTools: () => registered.map((name) => ({ name })),
413
+ registerTool(tool: { name: string }) {
414
+ registered.push(tool.name);
415
+ },
416
+ } as { on(event: string, handler: (payload?: unknown) => unknown): void; getAllTools(): Array<{ name: string }>; registerTool(tool: { name: string }): void });
417
+
418
+ handlers.get("session_start")?.({});
419
+ assert.deepEqual(registered, ["contact_supervisor"]);
420
+
421
+ await handlers.get("before_agent_start")?.({ systemPrompt: BASE_PROMPT });
422
+ assert.deepEqual(registered, ["contact_supervisor", "intercom"]);
423
+ });
424
+
425
+ it("sets the child intercom session name from env during agent startup", async () => {
426
+ let sessionName: string | undefined;
427
+ let beforeAgentStart: ((event: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) | undefined;
428
+ process.env[SUBAGENT_INTERCOM_SESSION_NAME_ENV] = "subagent-worker-78f659a3";
429
+
430
+ registerSubagentPromptRuntime({
431
+ on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) {
432
+ if (event === "before_agent_start") beforeAgentStart = handler;
433
+ },
434
+ setSessionName(name: string) {
435
+ sessionName = name;
436
+ },
437
+ } as { on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>): void; setSessionName(name: string): void });
438
+
439
+ await beforeAgentStart?.({ systemPrompt: BASE_PROMPT });
440
+
441
+ assert.equal(sessionName, "subagent-worker-78f659a3");
442
+ });
443
+
444
+ it("rewrites the final child-visible prompt through before_agent_start", async () => {
445
+ let beforeAgentStart: ((event: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) | undefined;
446
+ registerSubagentPromptRuntime({
447
+ on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) {
448
+ if (event === "before_agent_start") beforeAgentStart = handler;
449
+ },
450
+ } as { on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>): void });
451
+
452
+ assert.ok(beforeAgentStart, "expected before_agent_start handler");
453
+ process.env.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT = "0";
454
+ process.env.SELESAI_SUBAGENT_INHERIT_SKILLS = "0";
455
+
456
+ const rewritten = await beforeAgentStart?.({ systemPrompt: BASE_PROMPT });
457
+ assert.ok(rewritten);
458
+ assert.ok(!rewritten.systemPrompt.includes("# Project Context"));
459
+ assert.ok(!rewritten.systemPrompt.includes("<available_skills>"));
460
+ assert.ok(rewritten.systemPrompt.includes("Current date: 2026-04-16"));
461
+ });
462
+
463
+ it("uses the fanout boundary through before_agent_start when fanout env is set", async () => {
464
+ let beforeAgentStart: ((event: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) | undefined;
465
+ registerSubagentPromptRuntime({
466
+ on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>) {
467
+ if (event === "before_agent_start") beforeAgentStart = handler;
468
+ },
469
+ } as { on(event: string, handler: (payload: { systemPrompt: string }) => Promise<{ systemPrompt: string } | undefined>): void });
470
+
471
+ process.env.SELESAI_SUBAGENT_INHERIT_PROJECT_CONTEXT = "1";
472
+ process.env.SELESAI_SUBAGENT_INHERIT_SKILLS = "1";
473
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
474
+
475
+ const rewritten = await beforeAgentStart?.({ systemPrompt: BASE_PROMPT });
476
+ assert.ok(rewritten);
477
+ assert.ok(rewritten.systemPrompt.startsWith(CHILD_FANOUT_BOUNDARY_INSTRUCTIONS));
478
+ });
479
+
480
+ it("filters parent-only artifacts from polluted fork context while preserving ordinary history", () => {
481
+ let contextHandler: ((event: { messages: unknown[] }) => { messages: unknown[] } | undefined) | undefined;
482
+ registerSubagentPromptRuntime({
483
+ on(event: string, handler: (payload: { messages: unknown[] }) => { messages: unknown[] } | undefined) {
484
+ if (event === "context") contextHandler = handler;
485
+ },
486
+ } as { on(event: string, handler: (payload: { messages: unknown[] }) => { messages: unknown[] } | undefined): void });
487
+
488
+ const priorParentTurn = { role: "user", content: "Earlier we said planner → worker → reviewers → worker." };
489
+ const currentTask = { role: "user", content: "Now implement only the assigned fix." };
490
+ const instruction = { role: "custom", customType: "subagent-orchestration-instructions", content: "Subagent orchestration is enabled." };
491
+ const slashResult = { role: "custom", customType: "subagent-slash-result", content: "## Orchestration" };
492
+ const subagentResult = { role: "toolResult", toolName: "subagent", content: "subagent results" };
493
+ const subagentCall = { role: "assistant", content: [{ type: "toolCall", name: "subagent", input: { agent: "worker" } }] };
494
+ const otherCustom = { role: "custom", customType: "other", content: "keep" };
495
+
496
+ assert.deepEqual(contextHandler?.({ messages: [priorParentTurn, instruction, slashResult, subagentCall, subagentResult, otherCustom, currentTask] }), {
497
+ messages: [priorParentTurn, otherCustom, currentTask],
498
+ });
499
+ });
500
+
501
+ it("does not rewrite child context when no parent-only artifacts are present", () => {
502
+ let contextHandler: ((event: { messages: unknown[] }) => { messages: unknown[] } | undefined) | undefined;
503
+ registerSubagentPromptRuntime({
504
+ on(event: string, handler: (payload: { messages: unknown[] }) => { messages: unknown[] } | undefined) {
505
+ if (event === "context") contextHandler = handler;
506
+ },
507
+ } as { on(event: string, handler: (payload: { messages: unknown[] }) => { messages: unknown[] } | undefined): void });
508
+
509
+ const messages = [
510
+ { role: "user", content: "Task" },
511
+ { role: "toolResult", toolName: "read", content: "file" },
512
+ { role: "assistant", content: [{ type: "toolCall", name: "read", input: { path: "README.md" } }] },
513
+ ];
514
+
515
+ assert.equal(contextHandler?.({ messages }), undefined);
516
+ });
517
+ });
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import * as path from "node:path";
3
+ import { describe, it } from "node:test";
4
+ import {
5
+ ASYNC_DIR,
6
+ CHAIN_RUNS_DIR,
7
+ RESULTS_DIR,
8
+ TEMP_ARTIFACTS_DIR,
9
+ TEMP_ROOT_DIR,
10
+ getAsyncConfigPath,
11
+ resolveTempScopeId,
12
+ } from "../../src/shared/types.ts";
13
+
14
+ describe("resolveTempScopeId", () => {
15
+ it("prefers uid when available", () => {
16
+ const scope = resolveTempScopeId({
17
+ getuid: () => 501,
18
+ env: { USER: "alice" },
19
+ userInfo: () => ({ username: "alice" }),
20
+ });
21
+ assert.equal(scope, "uid-501");
22
+ });
23
+
24
+ it("falls back to environment usernames when uid is unavailable", () => {
25
+ const scope = resolveTempScopeId({
26
+ getuid: undefined,
27
+ env: { USERNAME: "Alice Example" },
28
+ userInfo: () => ({ username: "ignored" }),
29
+ });
30
+ assert.equal(scope, "user-Alice-Example");
31
+ });
32
+
33
+ it("falls back to os.userInfo when environment is missing", () => {
34
+ const scope = resolveTempScopeId({
35
+ getuid: undefined,
36
+ env: {},
37
+ userInfo: () => ({ username: "svc_account" }),
38
+ });
39
+ assert.equal(scope, "user-svc_account");
40
+ });
41
+
42
+ it("falls back to home path when os.userInfo throws", () => {
43
+ const scope = resolveTempScopeId({
44
+ getuid: undefined,
45
+ env: {},
46
+ userInfo: () => {
47
+ throw new Error("uv_os_get_passwd returned ENOENT");
48
+ },
49
+ homedir: () => "/home/12345/app user",
50
+ });
51
+ assert.equal(scope, "home-home-12345-app-user");
52
+ });
53
+ });
54
+
55
+ describe("shared temp paths", () => {
56
+ it("anchors shared temp directories under one scoped root", () => {
57
+ assert.equal(path.dirname(RESULTS_DIR), TEMP_ROOT_DIR);
58
+ assert.equal(path.dirname(ASYNC_DIR), TEMP_ROOT_DIR);
59
+ assert.equal(path.dirname(CHAIN_RUNS_DIR), TEMP_ROOT_DIR);
60
+ assert.equal(path.dirname(TEMP_ARTIFACTS_DIR), TEMP_ROOT_DIR);
61
+ assert.match(path.basename(TEMP_ROOT_DIR), /^pi-subagents-/);
62
+ assert.equal(path.basename(RESULTS_DIR), "async-subagent-results");
63
+ assert.equal(path.basename(ASYNC_DIR), "async-subagent-runs");
64
+ assert.equal(path.basename(CHAIN_RUNS_DIR), "chain-runs");
65
+ assert.equal(path.basename(TEMP_ARTIFACTS_DIR), "artifacts");
66
+ });
67
+
68
+ it("writes async config files under the same scoped temp root", () => {
69
+ assert.equal(path.dirname(getAsyncConfigPath("abc123")), TEMP_ROOT_DIR);
70
+ assert.equal(path.basename(getAsyncConfigPath("abc123")), "async-cfg-abc123.json");
71
+ });
72
+ });
@@ -0,0 +1,52 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ DEFAULT_TOOL_BUDGET_BLOCK,
5
+ decodeToolBudgetEnv,
6
+ encodeToolBudgetEnv,
7
+ initialToolBudgetState,
8
+ shouldBlockToolForBudget,
9
+ toolBudgetBlockedMessage,
10
+ toolBudgetSoftNudge,
11
+ toolBudgetState,
12
+ validateToolBudgetConfig,
13
+ } from "../../src/runs/shared/tool-budget.ts";
14
+
15
+ describe("tool-budget module", () => {
16
+ it("defaults block tools to read/search tools", () => {
17
+ const resolved = validateToolBudgetConfig({ hard: 5 });
18
+ assert.deepEqual(resolved.budget, { hard: 5, block: [...DEFAULT_TOOL_BUDGET_BLOCK] });
19
+ });
20
+
21
+ it("accepts soft and wildcard block", () => {
22
+ const resolved = validateToolBudgetConfig({ soft: 2, hard: 4, block: "*" });
23
+ assert.deepEqual(resolved.budget, { soft: 2, hard: 4, block: "*" });
24
+ });
25
+
26
+ it("rejects unsafe configs", () => {
27
+ assert.equal(validateToolBudgetConfig({ hard: 0 }).error, "toolBudget.hard must be an integer >= 1.");
28
+ assert.equal(validateToolBudgetConfig({ soft: 5, hard: 4 }).error, "toolBudget.soft must be <= toolBudget.hard.");
29
+ assert.equal(validateToolBudgetConfig({ hard: 4, block: [] }).error, "toolBudget.block must contain at least one tool name.");
30
+ assert.equal(validateToolBudgetConfig({ hard: 4, block: [""] }).error, "toolBudget.block must contain non-empty tool names.");
31
+ });
32
+
33
+ it("serializes and decodes env config", () => {
34
+ const budget = { soft: 2, hard: 4, block: ["read"] };
35
+ assert.deepEqual(decodeToolBudgetEnv(encodeToolBudgetEnv(budget)), budget);
36
+ });
37
+
38
+ it("tracks state and block decisions", () => {
39
+ const budget = { soft: 2, hard: 3, block: ["read"] };
40
+ assert.deepEqual(initialToolBudgetState(budget), { soft: 2, hard: 3, block: ["read"], toolCount: 0, outcome: "within-budget" });
41
+ assert.equal(toolBudgetState(budget, 2).outcome, "soft-reached");
42
+ assert.equal(toolBudgetState(budget, 4, "read").outcome, "hard-blocked");
43
+ assert.equal(shouldBlockToolForBudget(budget, "read", 4), true);
44
+ assert.equal(shouldBlockToolForBudget(budget, "write", 4), false);
45
+ });
46
+
47
+ it("formats user-facing budget messages", () => {
48
+ const budget = { soft: 2, hard: 3, block: ["read"] };
49
+ assert.match(toolBudgetSoftNudge(budget, 2), /soft limit reached after 2 tool calls/);
50
+ assert.match(toolBudgetBlockedMessage(budget, "read", 4), /'read' tool is blocked/);
51
+ });
52
+ });