@selesai/code 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/dist/extensions/handoff-new.test.ts +8 -9
  2. package/dist/extensions/handoff-new.ts +6 -8
  3. package/dist/extensions/pi-intercom/broker/spawn.test.ts +31 -0
  4. package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
  5. package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
  6. package/dist/extensions/pi-subagents/README.md +602 -220
  7. package/dist/extensions/pi-subagents/banner.png +0 -0
  8. package/dist/extensions/pi-subagents/index.ts +1 -0
  9. package/dist/extensions/pi-subagents/install.mjs +3 -3
  10. package/dist/extensions/pi-subagents/package-lock.json +3405 -0
  11. package/dist/extensions/pi-subagents/package.json +26 -14
  12. package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
  13. package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
  14. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
  15. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
  16. package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
  17. package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
  18. package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
  20. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
  21. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
  22. package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
  23. package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
  24. package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
  25. package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
  26. package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
  27. package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
  28. package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
  29. package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
  30. package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
  31. package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
  32. package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
  33. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
  34. package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
  35. package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
  36. package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
  37. package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
  38. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
  39. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
  40. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
  41. package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
  42. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
  43. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
  44. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
  45. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
  46. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
  47. package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
  48. package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
  49. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
  50. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
  51. package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
  52. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
  53. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
  54. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
  55. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
  56. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
  57. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
  58. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
  59. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
  61. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
  62. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
  63. package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
  64. package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
  65. package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
  66. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
  67. package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
  68. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
  69. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
  70. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
  71. package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
  72. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
  73. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
  74. package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
  75. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
  76. package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
  77. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
  78. package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
  79. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
  80. package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
  81. package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
  84. package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
  86. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
  87. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
  88. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
  89. package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
  90. package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
  92. package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
  93. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
  94. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
  95. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
  96. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
  97. package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
  98. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
  99. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
  100. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
  101. package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
  102. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
  103. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
  104. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
  105. package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
  106. package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
  107. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
  108. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
  109. package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
  110. package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
  111. package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
  112. package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
  113. package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
  114. package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
  115. package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
  116. package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
  117. package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
  118. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
  119. package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
  120. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
  121. package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
  122. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
  123. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
  124. package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
  125. package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
  126. package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
  127. package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
  128. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
  129. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
  130. package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
  131. package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
  132. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
  133. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
  134. package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
  135. package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
  136. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
  137. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
  138. package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
  139. package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
  140. package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
  141. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
  142. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
  143. package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
  144. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
  145. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
  146. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
  147. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
  148. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
  149. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
  150. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
  151. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
  152. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
  153. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
  154. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
  155. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
  156. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
  157. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
  158. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
  159. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
  160. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
  161. package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
  162. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
  163. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
  164. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
  165. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
  166. package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
  167. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
  168. package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
  169. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
  170. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
  171. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
  172. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
  173. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
  174. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
  175. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
  176. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
  177. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
  178. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
  179. package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
  180. package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
  181. package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
  182. package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
  183. package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
  184. package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
  185. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
  186. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
  187. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
  188. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
  189. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
  190. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
  191. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
  192. package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
  193. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
  194. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
  195. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
  196. package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
  197. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
  198. package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
  199. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
  200. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
  201. package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
  202. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
  203. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
  204. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
  205. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
  206. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
  207. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
  208. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
  209. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
  210. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
  211. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
  212. package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
  213. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
  214. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
  215. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
  216. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
  217. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
  218. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
  219. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
  220. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
  221. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
  222. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
  223. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
  224. package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
  225. package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
  226. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
  227. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
  228. package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
  229. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
  230. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
  231. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
  232. package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
  233. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
  234. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
  235. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
  236. package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
  237. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
  238. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
  239. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
  240. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
  241. package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
  242. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
  243. package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
  244. package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
  245. package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
  246. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
  247. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
  248. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
  249. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
  250. package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
  251. package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
  252. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
  253. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
  254. package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
  255. package/dist/extensions/question/batch.ts +39 -0
  256. package/dist/extensions/question/dialog-adapter.ts +23 -1
  257. package/dist/extensions/question/index.ts +237 -7
  258. package/dist/extensions/question/schemas.ts +28 -15
  259. package/dist/extensions/question/tests/batch.test.ts +26 -0
  260. package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
  261. package/dist/extensions/question/tui-adapter.ts +2 -3
  262. package/dist/extensions/question/types.ts +37 -0
  263. package/dist/extensions/question/ui-protocol.ts +3 -3
  264. package/dist/skills/batch-grill-me/SKILL.md +3 -1
  265. package/package.json +2 -1
@@ -4,7 +4,10 @@ import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { beforeEach, describe, it } from "node:test";
6
6
 
7
+ import { scheduledRunStorePath } from "../../src/runs/background/scheduled-runs.ts";
8
+ import { SUBAGENT_FANOUT_CHILD_ENV } from "../../src/runs/shared/pi-args.ts";
7
9
  import { ASYNC_DIR } from "../../src/shared/types.ts";
10
+ import type { WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
8
11
 
9
12
  const SLASH_RESULT_TYPE = "subagent-slash-result";
10
13
  const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
@@ -51,13 +54,19 @@ interface SlashLiveStateModule {
51
54
  resolveSlashMessageDetails?: typeof import("../../src/slash/slash-live-state.ts").resolveSlashMessageDetails;
52
55
  }
53
56
 
57
+ interface WatchdogRegisterModule {
58
+ registerMainWatchdog?: typeof import("../../src/watchdog/register-main.ts").registerMainWatchdog;
59
+ }
60
+
54
61
  let registerSlashCommands: RegisterSlashCommandsModule["registerSlashCommands"];
62
+ let registerMainWatchdog: WatchdogRegisterModule["registerMainWatchdog"];
55
63
  let clearSlashSnapshots: SlashLiveStateModule["clearSlashSnapshots"];
56
64
  let getSlashRenderableSnapshot: SlashLiveStateModule["getSlashRenderableSnapshot"];
57
65
  let resolveSlashMessageDetails: SlashLiveStateModule["resolveSlashMessageDetails"];
58
66
  let available = true;
59
67
  try {
60
68
  ({ registerSlashCommands } = await import("../../src/slash/slash-commands.ts") as RegisterSlashCommandsModule);
69
+ ({ registerMainWatchdog } = await import("../../src/watchdog/register-main.ts") as WatchdogRegisterModule);
61
70
  ({ clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails } = await import("../../src/slash/slash-live-state.ts") as SlashLiveStateModule);
62
71
  } catch {
63
72
  available = false;
@@ -88,6 +97,9 @@ function createState(cwd: string) {
88
97
  baseCwd: cwd,
89
98
  currentSessionId: null,
90
99
  asyncJobs: new Map(),
100
+ foregroundRuns: new Map(),
101
+ foregroundControls: new Map(),
102
+ lastForegroundControlId: null,
91
103
  cleanupTimers: new Map(),
92
104
  lastUiContext: null,
93
105
  poller: null,
@@ -103,13 +115,17 @@ function createState(cwd: string) {
103
115
 
104
116
  async function withIsolatedHome<T>(fn: () => Promise<T>): Promise<T> {
105
117
  const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-slash-home-"));
118
+ const previousAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
106
119
  const previousHome = process.env.HOME;
107
120
  const previousUserProfile = process.env.USERPROFILE;
121
+ process.env.SELESAI_CODING_AGENT_DIR = path.join(home, ".selesai", "agent");
108
122
  process.env.HOME = home;
109
123
  process.env.USERPROFILE = home;
110
124
  try {
111
125
  return await fn();
112
126
  } finally {
127
+ if (previousAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
128
+ else process.env.SELESAI_CODING_AGENT_DIR = previousAgentDir;
113
129
  if (previousHome === undefined) delete process.env.HOME;
114
130
  else process.env.HOME = previousHome;
115
131
  if (previousUserProfile === undefined) delete process.env.USERPROFILE;
@@ -126,10 +142,19 @@ function createCommandContext(
126
142
  custom: (...args: unknown[]) => Promise<unknown>;
127
143
  notify: (message: string, type?: string) => void;
128
144
  confirm: (title: string, message: string) => Promise<boolean>;
145
+ select: (title: string, choices: string[]) => Promise<string | undefined>;
146
+ editor: (title: string, prefill: string) => Promise<string | undefined>;
129
147
  setStatus: (key: string, text: string | undefined) => void;
130
148
  setToolsExpanded: (expanded: boolean) => void;
131
149
  sessionManager: unknown;
132
- modelRegistry: { getAvailable: () => Array<{ provider: string; id: string }>; find?: (provider: string, id: string) => unknown };
150
+ modelRegistry: {
151
+ refresh?: () => void;
152
+ getAvailable: () => Array<{ provider: string; id: string; reasoning?: boolean; thinkingLevelMap?: Record<string, string | null> }>;
153
+ find?: (provider: string, id: string) => unknown;
154
+ hasConfiguredAuth?: (model: unknown) => boolean;
155
+ };
156
+ model: { provider: string; id: string };
157
+ thinkingLevel: string;
133
158
  }> = {},
134
159
  ) {
135
160
  return {
@@ -138,12 +163,16 @@ function createCommandContext(
138
163
  ui: {
139
164
  notify: overrides.notify ?? ((_message: string) => {}),
140
165
  confirm: overrides.confirm ?? (async () => false),
166
+ select: overrides.select ?? (async () => undefined),
167
+ editor: overrides.editor ?? (async () => undefined),
141
168
  setStatus: overrides.setStatus ?? ((_key: string, _text: string | undefined) => {}),
142
169
  setToolsExpanded: overrides.setToolsExpanded ?? ((_expanded: boolean) => {}),
143
170
  onTerminalInput: () => () => {},
144
- custom: overrides.custom ?? (async () => undefined),
171
+ ...(overrides.custom ? { custom: overrides.custom } : {}),
145
172
  },
146
- modelRegistry: overrides.modelRegistry ?? { getAvailable: () => [], find: () => undefined },
173
+ model: overrides.model,
174
+ thinkingLevel: overrides.thinkingLevel,
175
+ modelRegistry: overrides.modelRegistry ?? { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true },
147
176
  sessionManager: overrides.sessionManager ?? {
148
177
  getSessionFile: () => null,
149
178
  getSessionId: () => "session-test",
@@ -166,6 +195,25 @@ function writeProjectChain(root: string, fileName: string, content: string): voi
166
195
  fs.writeFileSync(path.join(root, ".selesai", "chains", fileName), content, "utf-8");
167
196
  }
168
197
 
198
+ function createWatchdogHarness(review?: WatchdogReviewFunction) {
199
+ const commands = new Map<string, RegisteredSlashCommand>();
200
+ const renderers = new Map<string, (message: { content: string; details?: unknown }, options: { expanded: boolean }, theme: { fg(name: string, value: string): string; bold(value: string): string }) => { render(width: number): string[] } | undefined>();
201
+ const sent: unknown[] = [];
202
+ const pi = {
203
+ events: createEventBus(),
204
+ on() {},
205
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
206
+ registerShortcut() {},
207
+ registerMessageRenderer(type: string, renderer: (message: { content: string; details?: unknown }, options: { expanded: boolean }, theme: { fg(name: string, value: string): string; bold(value: string): string }) => { render(width: number): string[] } | undefined) {
208
+ renderers.set(type, renderer);
209
+ },
210
+ getThinkingLevel() { return "medium" as const; },
211
+ sendMessage(message: unknown) { sent.push(message); },
212
+ };
213
+ const runtime = registerMainWatchdog!(pi as never, review ? { review } : undefined);
214
+ return { commands, renderers, runtime, sent };
215
+ }
216
+
169
217
  async function captureSlashCommandParams(
170
218
  commandName: string,
171
219
  args: string,
@@ -212,6 +260,219 @@ async function captureSlashCommandParams(
212
260
  });
213
261
  }
214
262
 
263
+ describe("subagents watchdog slash command", { skip: !available ? "watchdog command not importable" : undefined }, () => {
264
+ it("shows default-off status with runtime state, sources, and review seam", async () => {
265
+ await withIsolatedHome(async () => {
266
+ await withTempProject("pi-watchdog-status-", async (root) => {
267
+ const { commands, sent } = createWatchdogHarness();
268
+ await commands.get("subagents-watchdog")!.handler("", createCommandContext({ cwd: root }));
269
+
270
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
271
+ assert.match(content, /Subagent watchdog/);
272
+ assert.match(content, /Main: off \(default off\)/);
273
+ assert.match(content, /Runtime: idle/);
274
+ assert.match(content, /Review model call: real model review/);
275
+ assert.match(content, /Sources:/);
276
+ });
277
+ });
278
+ });
279
+
280
+ it("recommends and saves a strong complementary watchdog model", async () => {
281
+ await withIsolatedHome(async () => {
282
+ await withTempProject("pi-watchdog-model-", async (root) => {
283
+ const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
284
+ const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
285
+ const models = [gpt, opus];
286
+ const modelRegistry = {
287
+ getAvailable: () => models,
288
+ find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
289
+ hasConfiguredAuth: (model: unknown) => Boolean(model),
290
+ };
291
+ const ctx = createCommandContext({ cwd: root, model: gpt, modelRegistry });
292
+ const { commands, sent } = createWatchdogHarness();
293
+
294
+ await commands.get("subagents-watchdog")!.handler("recommend-model", ctx);
295
+ await commands.get("subagents-watchdog")!.handler("model recommended", ctx);
296
+
297
+ const recommendation = String((sent[0] as { content?: unknown }).content ?? "");
298
+ assert.match(recommendation, /Recommended: anthropic\/claude-opus-4-8:high/);
299
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
300
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
301
+ assert.equal(settings.subagents.watchdog.main.model, "anthropic/claude-opus-4-8");
302
+ assert.equal(settings.subagents.watchdog.main.thinking, "high");
303
+ assert.equal(settings.subagents.watchdog.enabled, undefined);
304
+ assert.match(String((sent[1] as { content?: unknown }).content ?? ""), /Run \/subagents-watchdog on if the watchdog is still off/);
305
+ });
306
+ });
307
+ });
308
+
309
+ it("supports session-scoped recommended watchdog models without writing settings", async () => {
310
+ await withIsolatedHome(async () => {
311
+ await withTempProject("pi-watchdog-session-model-", async (root) => {
312
+ const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
313
+ const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
314
+ const models = [opus, gpt];
315
+ const modelRegistry = {
316
+ getAvailable: () => models,
317
+ find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
318
+ hasConfiguredAuth: (model: unknown) => Boolean(model),
319
+ };
320
+ const { commands, sent } = createWatchdogHarness();
321
+
322
+ await commands.get("subagents-watchdog")!.handler("session model recommended", createCommandContext({ cwd: root, model: opus, modelRegistry }));
323
+
324
+ assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "settings.json")), false);
325
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
326
+ assert.match(content, /session model: openai-codex\/gpt-5\.5:high/);
327
+ assert.match(content, /Main model: openai-codex\/gpt-5\.5 \(session override\)/);
328
+ assert.match(content, /Main thinking: high/);
329
+ });
330
+ });
331
+ });
332
+
333
+ it("shows explicit watchdog model thinking accurately when no thinking is configured", async () => {
334
+ await withIsolatedHome(async () => {
335
+ await withTempProject("pi-watchdog-status-model-", async (root) => {
336
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
337
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
338
+ fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { watchdog: { enabled: true, main: { model: "openai-codex/gpt-5.5" } } } }, null, 2), "utf-8");
339
+ const gpt = { provider: "openai-codex", id: "gpt-5.5", reasoning: true };
340
+ const opus = { provider: "anthropic", id: "claude-opus-4-8", reasoning: true };
341
+ const models = [gpt, opus];
342
+ const modelRegistry = {
343
+ getAvailable: () => models,
344
+ find: (provider: string, id: string) => models.find((entry) => entry.provider === provider && entry.id === id),
345
+ hasConfiguredAuth: (model: unknown) => Boolean(model),
346
+ };
347
+ const { commands, sent } = createWatchdogHarness();
348
+
349
+ await commands.get("subagents-watchdog")!.handler("status", createCommandContext({ cwd: root, model: gpt, modelRegistry }));
350
+
351
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
352
+ assert.match(content, /Main model: openai-codex\/gpt-5\.5 \(configured\)/);
353
+ assert.match(content, /Main thinking: off \(default for explicit watchdog model\)/);
354
+ });
355
+ });
356
+ });
357
+
358
+ it("writes only user watchdog enabled settings and preserves existing settings", async () => {
359
+ await withIsolatedHome(async () => {
360
+ await withTempProject("pi-watchdog-toggle-", async (root) => {
361
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
362
+ const projectSettingsPath = path.join(root, ".selesai", "settings.json");
363
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
364
+ fs.writeFileSync(settingsPath, JSON.stringify({
365
+ other: true,
366
+ subagents: {
367
+ agentOverrides: { scout: { model: "openai/test" } },
368
+ watchdog: { agentEndTimeoutMs: 1234, main: { enabled: false, model: "openai/watchdog" } },
369
+ },
370
+ }, null, 2), "utf-8");
371
+ fs.writeFileSync(projectSettingsPath, JSON.stringify({ subagents: { defaultModel: "anthropic/project" } }, null, 2), "utf-8");
372
+ const projectBefore = fs.readFileSync(projectSettingsPath, "utf-8");
373
+ const { commands, sent } = createWatchdogHarness();
374
+
375
+ await commands.get("subagents-watchdog")!.handler("on", createCommandContext({ cwd: root }));
376
+ let settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
377
+ assert.equal(settings.other, true);
378
+ assert.equal(settings.subagents.agentOverrides.scout.model, "openai/test");
379
+ assert.equal(settings.subagents.watchdog.agentEndTimeoutMs, 1234);
380
+ assert.equal(settings.subagents.watchdog.enabled, true);
381
+ assert.equal(settings.subagents.watchdog.main.enabled, true);
382
+ assert.equal(settings.subagents.watchdog.main.model, "openai/watchdog");
383
+ assert.equal(fs.readFileSync(projectSettingsPath, "utf-8"), projectBefore);
384
+
385
+ await commands.get("subagents-watchdog")!.handler("off", createCommandContext({ cwd: root }));
386
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
387
+ assert.equal(settings.subagents.watchdog.enabled, false);
388
+ assert.equal(settings.subagents.watchdog.main.enabled, false);
389
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /saved to user settings/);
390
+ });
391
+ });
392
+ });
393
+
394
+ it("uses session on/off overrides without writing settings files", async () => {
395
+ await withIsolatedHome(async () => {
396
+ await withTempProject("pi-watchdog-session-", async (root) => {
397
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
398
+ const projectSettingsPath = path.join(root, ".selesai", "settings.json");
399
+ const { commands, sent } = createWatchdogHarness();
400
+
401
+ await commands.get("subagents-watchdog")!.handler("session on", createCommandContext({ cwd: root }));
402
+ await commands.get("subagents-watchdog")!.handler("session off", createCommandContext({ cwd: root }));
403
+
404
+ assert.equal(fs.existsSync(settingsPath), false);
405
+ assert.equal(fs.existsSync(projectSettingsPath), false);
406
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /session override: on/i);
407
+ assert.match(String((sent[1] as { content?: unknown }).content ?? ""), /session override: off/i);
408
+ });
409
+ });
410
+ });
411
+
412
+ it("sends deterministic concern and blocker warning messages through the renderer path", async () => {
413
+ await withIsolatedHome(async () => {
414
+ const { commands, renderers, sent } = createWatchdogHarness();
415
+ await commands.get("subagents-watchdog")!.handler("test concern check the concern", createCommandContext());
416
+ await commands.get("subagents-watchdog")!.handler("test blocker check the blocker", createCommandContext());
417
+
418
+ const concern = sent[0] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
419
+ const blocker = sent[1] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
420
+ assert.equal(concern.customType, "subagent_watchdog_warning");
421
+ assert.equal(concern.display, true);
422
+ assert.equal(concern.details?.severity, "concern");
423
+ assert.equal(concern.details?.source, "main");
424
+ assert.equal(concern.details?.state, "displayed");
425
+ assert.match(concern.content ?? "", /source="main"/);
426
+ assert.match(concern.content ?? "", /<state>displayed<\/state>/);
427
+ assert.match(concern.content ?? "", /<recommended_action>/);
428
+ assert.equal(blocker.details?.severity, "blocker");
429
+ assert.match(blocker.content ?? "", /<blocker_guidance>/);
430
+
431
+ const renderer = renderers.get("subagent_watchdog_warning")!;
432
+ const rendered = renderer(blocker as never, { expanded: true }, { fg: (_name, value) => value, bold: (value) => value })!.render(100).join("\n");
433
+ assert.match(rendered, /Subagent watchdog Blocker \(displayed\): check the blocker/);
434
+ assert.match(rendered, /Manual \/subagents-watchdog test blocker message/);
435
+ });
436
+ });
437
+
438
+ it("sends accepted review warnings as visible custom watchdog messages", async () => {
439
+ await withIsolatedHome(async () => {
440
+ await withTempProject("pi-watchdog-review-warning-", async (root) => {
441
+ const review: WatchdogReviewFunction = (request) => {
442
+ assert.equal(request.emitWarning({
443
+ severity: "concern",
444
+ category: "test-gap",
445
+ confidence: "high",
446
+ source: "main",
447
+ summary: "Focused validation is missing",
448
+ evidence: "The reviewed turn delta says changes were made but contains no test command.",
449
+ recommendedAction: "Run the focused watchdog tests before accepting the turn.",
450
+ }), true);
451
+ return { stopReason: "stop" };
452
+ };
453
+ const { runtime, sent } = createWatchdogHarness(review);
454
+
455
+ runtime.setSessionEnabled(true, root);
456
+ runtime.handleBeforeAgentStart({ prompt: "Patch watchdog runtime." }, { cwd: root });
457
+ runtime.handleTurnEnd({
458
+ type: "turn_end",
459
+ message: { role: "assistant", content: "Changed watchdog runtime without running tests." },
460
+ toolResults: [{ role: "toolResult", toolName: "edit", content: "Edited src/watchdog/runtime.ts", isError: false }],
461
+ }, { cwd: root });
462
+ await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: root });
463
+
464
+ const message = sent[0] as { customType?: string; content?: string; display?: boolean; details?: Record<string, unknown> };
465
+ assert.equal(message.customType, "subagent_watchdog_warning");
466
+ assert.equal(message.display, true);
467
+ assert.equal(message.details?.state, "displayed");
468
+ assert.equal(message.details?.summary, "Focused validation is missing");
469
+ assert.match(message.content ?? "", /<subagent_watchdog/);
470
+ assert.match(message.content ?? "", /<recommended_action>/);
471
+ });
472
+ });
473
+ });
474
+ });
475
+
215
476
  describe("slash command custom message delivery", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
216
477
  beforeEach(() => {
217
478
  clearSlashSnapshots?.();
@@ -260,6 +521,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
260
521
  const ctx = createCommandContext({ sessionManager });
261
522
  registerSlashCommands!(pi, createState(process.cwd()));
262
523
  await commands.get("run")!.handler("scout", ctx);
524
+ await new Promise<void>((resolve) => setImmediate(resolve));
263
525
 
264
526
  assert.deepEqual(requestedParams, { agent: "scout", task: "", clarify: false, agentScope: "both" });
265
527
  assert.equal(requestedCtx, ctx);
@@ -309,6 +571,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
309
571
  log.push(`status:${text ?? "clear"}`);
310
572
  },
311
573
  }));
574
+ await new Promise<void>((resolve) => setImmediate(resolve));
312
575
 
313
576
  assert.equal(sent.length, 2);
314
577
  assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
@@ -398,6 +661,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
398
661
  log.push(`status:${text ?? "clear"}`);
399
662
  },
400
663
  }));
664
+ await new Promise<void>((resolve) => setImmediate(resolve));
401
665
 
402
666
  assert.equal(sent.length, 2);
403
667
  assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
@@ -484,6 +748,7 @@ describe("slash command custom message delivery", { skip: !available ? "slash-co
484
748
  registerSlashCommands!(pi, createState(process.cwd()));
485
749
  const args = Array.from({ length: 9 }, (_, index) => `scout \"task ${index + 1}\"`).join(" -> ");
486
750
  await commands.get("parallel")!.handler(args, createCommandContext());
751
+ await new Promise<void>((resolve) => setImmediate(resolve));
487
752
 
488
753
  assert.equal(requestedTasks, 9);
489
754
  assert.equal(sent.length, 2);
@@ -592,6 +857,47 @@ Review {previous}
592
857
  });
593
858
  });
594
859
 
860
+ it("/run-chain returns while its foreground subagent is still running", async () => {
861
+ await withTempProject("pi-run-chain-nonblocking-", async (root) => {
862
+ writeProjectChain(root, "slow.chain.md", `---
863
+ name: slow
864
+ description: Slow chain
865
+ ---
866
+
867
+ ## scout
868
+
869
+ Think for a long time about {task}
870
+ `);
871
+ const commands = new Map<string, RegisteredSlashCommand>();
872
+ const events = createEventBus();
873
+ let requestId: string | undefined;
874
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
875
+ const payload = data as { requestId: string };
876
+ requestId = payload.requestId;
877
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
878
+ });
879
+ const pi = {
880
+ events,
881
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
882
+ registerShortcut() {},
883
+ sendMessage(_message: unknown) {},
884
+ };
885
+ registerSlashCommands!(pi, createState(root));
886
+
887
+ const returned = await Promise.race([
888
+ commands.get("run-chain")!.handler("slow -- test UI responsiveness", createCommandContext({ cwd: root })).then(() => true),
889
+ new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 25)),
890
+ ]);
891
+ assert.equal(returned, true, "the slash command should not hold the UI while the child runs");
892
+ assert.ok(requestId);
893
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
894
+ requestId,
895
+ result: { content: [{ type: "text", text: "done" }], details: { mode: "chain", results: [] } },
896
+ isError: false,
897
+ });
898
+ });
899
+ });
900
+
595
901
  it("/run-chain launches a saved JSON chain with dynamic fanout", async () => {
596
902
  await withTempProject("pi-run-chain-json-dynamic-", async (root) => {
597
903
  writeProjectChain(root, "dynamic-review.chain.json", JSON.stringify({
@@ -1249,6 +1555,463 @@ describe("subagent profiles slash commands", { skip: !available ? "slash-command
1249
1555
  });
1250
1556
  });
1251
1557
 
1558
+ describe("subagents admin slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
1559
+ beforeEach(() => {
1560
+ clearSlashSnapshots?.();
1561
+ });
1562
+
1563
+ function registerAdmin(sent: unknown[]) {
1564
+ const commands = new Map<string, RegisteredSlashCommand>();
1565
+ const pi = {
1566
+ events: createEventBus(),
1567
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1568
+ registerShortcut() {},
1569
+ sendMessage(message: unknown) { sent.push(message); },
1570
+ };
1571
+ registerSlashCommands!(pi, createState(process.cwd()));
1572
+ return commands;
1573
+ }
1574
+
1575
+ it("registers /subagents", async () => {
1576
+ await withIsolatedHome(async () => {
1577
+ const commands = registerAdmin([]);
1578
+ assert.equal(commands.has("subagents"), true);
1579
+ });
1580
+ });
1581
+
1582
+ it("treats agent and scope selector cancellation as a silent no-op", async () => {
1583
+ await withIsolatedHome(async () => {
1584
+ await withTempProject("pi-subagents-admin-cancel-", async (root) => {
1585
+ const sent: unknown[] = [];
1586
+ const notifications: string[] = [];
1587
+ const commands = registerAdmin(sent);
1588
+
1589
+ await commands.get("subagents")!.handler("", createCommandContext({
1590
+ cwd: root,
1591
+ hasUI: true,
1592
+ select: async () => undefined,
1593
+ notify: (message) => notifications.push(message),
1594
+ }));
1595
+
1596
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1597
+ cwd: root,
1598
+ hasUI: true,
1599
+ modelRegistry: { getAvailable: () => [{ provider: "test", id: "new-model", reasoning: true }] },
1600
+ select: async (title) => title.startsWith("Select model") ? "test/new-model" : undefined,
1601
+ notify: (message) => notifications.push(message),
1602
+ }));
1603
+
1604
+ assert.deepEqual(sent, []);
1605
+ assert.deepEqual(notifications, []);
1606
+ assert.equal(fs.existsSync(path.join(root, ".selesai", "settings.json")), false);
1607
+ });
1608
+ });
1609
+ });
1610
+
1611
+ it("reports duplicate names as ambiguous without UI", async () => {
1612
+ await withIsolatedHome(async () => {
1613
+ await withTempProject("pi-subagents-admin-ambiguous-", async (root) => {
1614
+ fs.writeFileSync(path.join(root, ".selesai", "agents", "worker.md"), "---\nname: worker\ndescription: Project worker\n---\n\nWork.\n", "utf-8");
1615
+ const sent: unknown[] = [];
1616
+ const commands = registerAdmin(sent);
1617
+ await commands.get("subagents")!.handler("worker", createCommandContext({ cwd: root }));
1618
+ assert.equal(sent.length, 1);
1619
+ const content = (sent[0] as { content?: string }).content ?? "";
1620
+ assert.match(content, /Subagent 'worker' is ambiguous/);
1621
+ assert.match(content, /project:/);
1622
+ assert.match(content, /builtin:/);
1623
+ });
1624
+ });
1625
+ });
1626
+
1627
+ it("keeps same-source duplicate agents distinct in interactive pickers", async () => {
1628
+ await withIsolatedHome(async () => {
1629
+ await withTempProject("pi-subagents-admin-same-source-", async (root) => {
1630
+ const localPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "agents", "duplicate-admin-agent.md");
1631
+ const extraDir = path.join(root, "extra-agents");
1632
+ const extraPath = path.join(extraDir, "duplicate-admin-agent.md");
1633
+ fs.mkdirSync(path.dirname(localPath), { recursive: true });
1634
+ fs.mkdirSync(extraDir, { recursive: true });
1635
+ const frontmatter = "---\nname: duplicate-admin-agent\ndescription: Duplicate agent\nmodel: test/model\n---\n\n";
1636
+ fs.writeFileSync(localPath, `${frontmatter}Local prompt.\n`, "utf-8");
1637
+ fs.writeFileSync(extraPath, `${frontmatter}Extra prompt.\n`, "utf-8");
1638
+ const previousExtraDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
1639
+ process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = extraDir;
1640
+ try {
1641
+ const sent: unknown[] = [];
1642
+ const commands = registerAdmin(sent);
1643
+ await commands.get("subagents")!.handler("duplicate-admin-agent details", createCommandContext({
1644
+ cwd: root,
1645
+ hasUI: true,
1646
+ select: async (_title, choices) => {
1647
+ assert.equal(choices.length, 2);
1648
+ assert.ok(choices.some((choice) => choice.includes(localPath)));
1649
+ assert.ok(choices.some((choice) => choice.includes(extraPath)));
1650
+ return choices.find((choice) => choice.includes(localPath));
1651
+ },
1652
+ }));
1653
+ const content = (sent[0] as { content?: string }).content ?? "";
1654
+ assert.ok(content.split("\n").includes(`Path: ${localPath}`));
1655
+ } finally {
1656
+ if (previousExtraDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
1657
+ else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = previousExtraDirs;
1658
+ }
1659
+ });
1660
+ });
1661
+ });
1662
+
1663
+ it("updates a project agent model without dumping metadata first", async () => {
1664
+ await withIsolatedHome(async () => {
1665
+ await withTempProject("pi-subagents-admin-model-", async (root) => {
1666
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1667
+ fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\nmodel: anthropic/claude-sonnet-4-6\n---\n\nDo work.\n`, "utf-8");
1668
+ const sent: unknown[] = [];
1669
+ const commands = registerAdmin(sent);
1670
+ let refreshes = 0;
1671
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1672
+ cwd: root,
1673
+ hasUI: true,
1674
+ modelRegistry: {
1675
+ refresh: () => { refreshes++; },
1676
+ getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }],
1677
+ },
1678
+ select: async (_title, choices) => {
1679
+ const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
1680
+ if (projectAgent) return projectAgent;
1681
+ assert.ok(choices.includes("bluebox-azure-openai/gpt-5_6-sol"));
1682
+ return "bluebox-azure-openai/gpt-5_6-sol";
1683
+ },
1684
+ }));
1685
+
1686
+ assert.equal(refreshes, 1);
1687
+ assert.match(fs.readFileSync(agentPath, "utf-8"), /^model: bluebox-azure-openai\/gpt-5_6-sol$/m);
1688
+ assert.equal(sent.length, 1, "only the compact confirmation should enter the conversation");
1689
+ assert.match((sent[0] as { content?: string }).content ?? "", /Updated 'worker' model/);
1690
+ });
1691
+ });
1692
+ });
1693
+
1694
+ it("updates a profile-managed user agent through settings instead of pinning frontmatter", async () => {
1695
+ await withIsolatedHome(async () => {
1696
+ await withTempProject("pi-subagents-admin-profile-", async (root) => {
1697
+ const userAgentDir = path.join(process.env.HOME!, ".selesai", "agent", "agents");
1698
+ fs.mkdirSync(userAgentDir, { recursive: true });
1699
+ const agentPath = path.join(userAgentDir, "worker.md");
1700
+ fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Profile worker\n---\n\nDo work.\n`, "utf-8");
1701
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
1702
+ fs.writeFileSync(settingsPath, JSON.stringify({
1703
+ subagents: { disableBuiltins: true, agentOverrides: { worker: { model: "anthropic/claude-opus-4-8" } } },
1704
+ }, null, 2));
1705
+
1706
+ const sent: unknown[] = [];
1707
+ const commands = registerAdmin(sent);
1708
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1709
+ cwd: root,
1710
+ hasUI: true,
1711
+ modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-luna", reasoning: true }] },
1712
+ select: async (_title, choices) =>
1713
+ choices.find((choice) => choice.startsWith("worker [user]"))
1714
+ ?? "bluebox-azure-openai/gpt-5_6-luna",
1715
+ }));
1716
+
1717
+ assert.doesNotMatch(fs.readFileSync(agentPath, "utf-8"), /^model:/m);
1718
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
1719
+ assert.equal(settings.subagents.disableBuiltins, true);
1720
+ assert.equal(settings.subagents.agentOverrides.worker.model, "bluebox-azure-openai/gpt-5_6-luna");
1721
+ });
1722
+ });
1723
+ });
1724
+
1725
+ it("does not snapshot global disableThinking into an unrelated builtin override", async () => {
1726
+ await withIsolatedHome(async () => {
1727
+ await withTempProject("pi-subagents-admin-global-thinking-", async (root) => {
1728
+ const settingsPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "settings.json");
1729
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1730
+ fs.writeFileSync(settingsPath, JSON.stringify({ subagents: { disableThinking: true } }, null, 2), "utf-8");
1731
+ const commands = registerAdmin([]);
1732
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1733
+ cwd: root,
1734
+ hasUI: true,
1735
+ modelRegistry: { getAvailable: () => [{ provider: "test", id: "new-model", reasoning: true }] },
1736
+ select: async () => "test/new-model",
1737
+ }));
1738
+
1739
+ assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents, {
1740
+ disableThinking: true,
1741
+ agentOverrides: { worker: { model: "test/new-model" } },
1742
+ });
1743
+ });
1744
+ });
1745
+ });
1746
+
1747
+ it("offers and saves max thinking only when model metadata declares it", async () => {
1748
+ await withIsolatedHome(async () => {
1749
+ await withTempProject("pi-subagents-admin-thinking-", async (root) => {
1750
+ const settingsPath = path.join(process.env.SELESAI_CODING_AGENT_DIR!, "settings.json");
1751
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1752
+ fs.writeFileSync(settingsPath, JSON.stringify({
1753
+ subagents: { agentOverrides: { worker: { model: "anthropic/claude-opus-4-8", thinking: "high" } } },
1754
+ }, null, 2));
1755
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1756
+ fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\nmodel: bluebox-azure-openai/gpt-5_6-sol\n---\n\nDo work.\n`, "utf-8");
1757
+ const commands = registerAdmin([]);
1758
+ await commands.get("subagents")!.handler("worker thinking", createCommandContext({
1759
+ cwd: root,
1760
+ hasUI: true,
1761
+ modelRegistry: {
1762
+ getAvailable: () => [{
1763
+ provider: "bluebox-azure-openai",
1764
+ id: "gpt-5_6-sol",
1765
+ reasoning: true,
1766
+ thinkingLevelMap: { minimal: null, xhigh: "xhigh", max: "max" },
1767
+ }],
1768
+ },
1769
+ select: async (_title, choices) => {
1770
+ const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
1771
+ if (projectAgent) return projectAgent;
1772
+ assert.ok(choices.includes("max"));
1773
+ assert.equal(choices.includes("minimal"), false);
1774
+ return "max";
1775
+ },
1776
+ }));
1777
+ assert.match(fs.readFileSync(agentPath, "utf-8"), /^thinking: max$/m);
1778
+ assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.worker, {
1779
+ model: "anthropic/claude-opus-4-8",
1780
+ thinking: "high",
1781
+ });
1782
+ });
1783
+ });
1784
+ });
1785
+
1786
+ it("uses the inherited session model's thinking capabilities and warns on refresh failure", async () => {
1787
+ await withIsolatedHome(async () => {
1788
+ await withTempProject("pi-subagents-admin-inherited-thinking-", async (root) => {
1789
+ fs.writeFileSync(path.join(root, ".selesai", "agents", "auditor.md"), "---\nname: auditor\ndescription: Audit things\n---\n\nAudit.\n", "utf-8");
1790
+ const notifications: Array<{ message: string; type?: string }> = [];
1791
+ const commands = registerAdmin([]);
1792
+ await commands.get("subagents")!.handler("auditor thinking", createCommandContext({
1793
+ cwd: root,
1794
+ hasUI: true,
1795
+ model: { provider: "test", id: "plain-model" },
1796
+ modelRegistry: {
1797
+ refresh: () => { throw new Error("invalid models.json"); },
1798
+ getAvailable: () => [{ provider: "test", id: "plain-model", reasoning: false }],
1799
+ },
1800
+ select: async (title, choices) => {
1801
+ assert.match(title, /Session model: test\/plain-model/);
1802
+ assert.deepEqual(choices, ["Default / inherit session thinking", "off"]);
1803
+ return undefined;
1804
+ },
1805
+ notify: (message, type) => notifications.push({ message, type }),
1806
+ }));
1807
+
1808
+ assert.equal(notifications.length, 1);
1809
+ assert.equal(notifications[0]?.type, "warning");
1810
+ assert.match(notifications[0]?.message ?? "", /using the last loaded choices.*invalid models\.json/);
1811
+ });
1812
+ });
1813
+ });
1814
+
1815
+ it("does not pin inherited settings when editing a project-owned field", async () => {
1816
+ await withIsolatedHome(async () => {
1817
+ await withTempProject("pi-subagents-admin-cross-scope-", async (root) => {
1818
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1819
+ fs.writeFileSync(agentPath, "---\nname: worker\ndescription: Test worker\n---\n\nDo work.\n", "utf-8");
1820
+ const settingsPath = path.join(process.env.HOME!, ".selesai", "agent", "settings.json");
1821
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
1822
+ fs.writeFileSync(settingsPath, JSON.stringify({
1823
+ subagents: { agentOverrides: { worker: { thinking: "high" } } },
1824
+ }, null, 2));
1825
+
1826
+ const commands = registerAdmin([]);
1827
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1828
+ cwd: root,
1829
+ hasUI: true,
1830
+ modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }] },
1831
+ select: async (_title, choices) => {
1832
+ const projectAgent = choices.find((choice) => choice.startsWith("worker [project]"));
1833
+ if (projectAgent) return projectAgent;
1834
+ assert.ok(choices.includes("bluebox-azure-openai/gpt-5_6-sol"));
1835
+ return "bluebox-azure-openai/gpt-5_6-sol";
1836
+ },
1837
+ }));
1838
+
1839
+ const updated = fs.readFileSync(agentPath, "utf-8");
1840
+ assert.match(updated, /^model: bluebox-azure-openai\/gpt-5_6-sol$/m);
1841
+ assert.doesNotMatch(updated, /^thinking:/m);
1842
+ assert.doesNotMatch(updated, /^systemPromptMode:/m);
1843
+ assert.doesNotMatch(updated, /^inheritProjectContext:/m);
1844
+ assert.equal(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides.worker.thinking, "high");
1845
+ });
1846
+ });
1847
+ });
1848
+
1849
+ it("does not write definitions from SELESAI_SUBAGENT_EXTRA_AGENT_DIRS", async () => {
1850
+ await withIsolatedHome(async () => {
1851
+ await withTempProject("pi-subagents-admin-extra-", async (root) => {
1852
+ const extraDir = path.join(root, "extra-agents");
1853
+ const agentPath = path.join(extraDir, "worker.md");
1854
+ fs.mkdirSync(extraDir, { recursive: true });
1855
+ const original = "---\nname: worker\ndescription: Read-only worker\nmodel: anthropic/claude-sonnet-4-6\n---\n\nDo work.\n";
1856
+ fs.writeFileSync(agentPath, original, "utf-8");
1857
+ const previousExtraDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
1858
+ process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = extraDir;
1859
+ try {
1860
+ const commands = registerAdmin([]);
1861
+ await commands.get("subagents")!.handler("worker model", createCommandContext({
1862
+ cwd: root,
1863
+ hasUI: true,
1864
+ modelRegistry: { getAvailable: () => [{ provider: "bluebox-azure-openai", id: "gpt-5_6-sol", reasoning: true }] },
1865
+ select: async (_title, choices) =>
1866
+ choices.find((choice) => choice.startsWith("worker [user]"))
1867
+ ?? "bluebox-azure-openai/gpt-5_6-sol",
1868
+ }));
1869
+ assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
1870
+ } finally {
1871
+ if (previousExtraDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
1872
+ else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = previousExtraDirs;
1873
+ }
1874
+ });
1875
+ });
1876
+ });
1877
+
1878
+ it("respects package-owned fields and uses settings only for unset package fields", async () => {
1879
+ await withIsolatedHome(async () => {
1880
+ await withTempProject("pi-subagents-admin-package-", async (root) => {
1881
+ const packageRoot = path.join(root, ".selesai", "npm", "node_modules", "admin-package");
1882
+ const agentPath = path.join(packageRoot, "agents", "package-admin-agent.md");
1883
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
1884
+ fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({
1885
+ name: "admin-package",
1886
+ "pi-subagents": { agents: ["./agents"] },
1887
+ }), "utf-8");
1888
+ const original = "---\nname: package-admin-agent\ndescription: Package agent\nmodel: test/original\n---\n\nPackage prompt.\n";
1889
+ fs.writeFileSync(agentPath, original, "utf-8");
1890
+ const settingsPath = path.join(root, ".selesai", "settings.json");
1891
+ fs.writeFileSync(settingsPath, JSON.stringify({
1892
+ subagents: { agentOverrides: { "package-admin-agent": { thinking: "low" } } },
1893
+ }, null, 2), "utf-8");
1894
+
1895
+ const sent: unknown[] = [];
1896
+ const commands = registerAdmin(sent);
1897
+ await commands.get("subagents")!.handler("package-admin-agent model", createCommandContext({
1898
+ cwd: root,
1899
+ hasUI: true,
1900
+ modelRegistry: { getAvailable: () => [{ provider: "test", id: "replacement", reasoning: true }] },
1901
+ select: async () => "test/replacement",
1902
+ }));
1903
+ await commands.get("subagents")!.handler("package-admin-agent prompt", createCommandContext({
1904
+ cwd: root,
1905
+ hasUI: true,
1906
+ }));
1907
+
1908
+ assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
1909
+ assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides, {
1910
+ "package-admin-agent": { thinking: "low" },
1911
+ });
1912
+ assert.match((sent[0] as { content?: string }).content ?? "", /model.*read-only package definition/);
1913
+ assert.match((sent[1] as { content?: string }).content ?? "", /systemPrompt.*read-only package definition/);
1914
+
1915
+ const configurablePath = path.join(packageRoot, "agents", "configurable-package-agent.md");
1916
+ const configurable = "---\nname: configurable-package-agent\ndescription: Configurable package agent\n---\n\nPackage prompt.\n";
1917
+ fs.writeFileSync(configurablePath, configurable, "utf-8");
1918
+ await commands.get("subagents")!.handler("configurable-package-agent model", createCommandContext({
1919
+ cwd: root,
1920
+ hasUI: true,
1921
+ modelRegistry: { getAvailable: () => [{ provider: "test", id: "replacement", reasoning: true }] },
1922
+ select: async (title) => title.startsWith("Select model") ? "test/replacement" : "project",
1923
+ }));
1924
+ assert.equal(fs.readFileSync(configurablePath, "utf-8"), configurable);
1925
+ assert.deepEqual(JSON.parse(fs.readFileSync(settingsPath, "utf-8")).subagents.agentOverrides, {
1926
+ "package-admin-agent": { thinking: "low" },
1927
+ "configurable-package-agent": { model: "test/replacement" },
1928
+ });
1929
+ });
1930
+ });
1931
+ });
1932
+
1933
+ it("edits a system prompt through Pi's native editor", async () => {
1934
+ await withIsolatedHome(async () => {
1935
+ await withTempProject("pi-subagents-admin-prompt-", async (root) => {
1936
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1937
+ fs.writeFileSync(agentPath, `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`, "utf-8");
1938
+ const status: Array<[string, string | undefined]> = [];
1939
+ let editorArgs: [string, string] | undefined;
1940
+ const commands = registerAdmin([]);
1941
+ await commands.get("subagents")!.handler("worker prompt", createCommandContext({
1942
+ cwd: root,
1943
+ hasUI: true,
1944
+ select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
1945
+ editor: async (title, prefill) => {
1946
+ editorArgs = [title, prefill];
1947
+ return "Edited prompt.\n";
1948
+ },
1949
+ setStatus: (key, text) => status.push([key, text]),
1950
+ }));
1951
+ assert.deepEqual(editorArgs, ["Edit 'worker' system prompt", "Original prompt."]);
1952
+ assert.match(fs.readFileSync(agentPath, "utf-8"), /Edited prompt\./);
1953
+ assert.deepEqual(status, []);
1954
+ });
1955
+ });
1956
+ });
1957
+
1958
+ it("cancels native system-prompt editing without mutation", async () => {
1959
+ await withIsolatedHome(async () => {
1960
+ await withTempProject("pi-subagents-admin-prompt-cancel-", async (root) => {
1961
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1962
+ const original = `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`;
1963
+ fs.writeFileSync(agentPath, original, "utf-8");
1964
+ const sent: unknown[] = [];
1965
+ const commands = registerAdmin(sent);
1966
+ await commands.get("subagents")!.handler("worker prompt", createCommandContext({
1967
+ cwd: root,
1968
+ hasUI: true,
1969
+ select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
1970
+ editor: async () => undefined,
1971
+ }));
1972
+ assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
1973
+ assert.deepEqual(sent, []);
1974
+ });
1975
+ });
1976
+ });
1977
+
1978
+ it("reports native-editor failures without rewriting the agent", async () => {
1979
+ await withIsolatedHome(async () => {
1980
+ await withTempProject("pi-subagents-admin-editor-failure-", async (root) => {
1981
+ const agentPath = path.join(root, ".selesai", "agents", "worker.md");
1982
+ const original = `---\nname: worker\ndescription: Test worker\n---\n\nOriginal prompt.\n`;
1983
+ fs.writeFileSync(agentPath, original, "utf-8");
1984
+ const sent: unknown[] = [];
1985
+ const commands = registerAdmin(sent);
1986
+ await commands.get("subagents")!.handler("worker prompt", createCommandContext({
1987
+ cwd: root,
1988
+ hasUI: true,
1989
+ select: async (_title, choices) => choices.find((choice) => choice.startsWith("worker [project]")),
1990
+ editor: async () => { throw new Error("editor unavailable"); },
1991
+ }));
1992
+ assert.equal(fs.readFileSync(agentPath, "utf-8"), original);
1993
+ assert.match((sent.at(-1) as { content?: string }).content ?? "", /Failed to update 'worker': editor unavailable/);
1994
+ });
1995
+ });
1996
+ });
1997
+
1998
+ it("shows metadata for a named subagent without requiring UI", async () => {
1999
+ await withIsolatedHome(async () => {
2000
+ await withTempProject("pi-subagents-admin-metadata-", async (root) => {
2001
+ fs.writeFileSync(path.join(root, ".selesai", "agents", "auditor.md"), `---\nname: auditor\ndescription: Audit things\nthinking: false\n---\n\nAudit.\n`, "utf-8");
2002
+ const sent: unknown[] = [];
2003
+ const commands = registerAdmin(sent);
2004
+ await commands.get("subagents")!.handler("auditor", createCommandContext({ cwd: root }));
2005
+ assert.equal(sent.length, 1);
2006
+ const content = (sent[0] as { content?: string }).content ?? "";
2007
+ assert.match(content, /Agent: auditor \(project\)/);
2008
+ assert.match(content, /^Thinking: off$/m);
2009
+ });
2010
+ });
2011
+ });
2012
+ });
2013
+
2014
+
1252
2015
  describe("subagents-doctor slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
1253
2016
  beforeEach(() => {
1254
2017
  clearSlashSnapshots?.();
@@ -1259,11 +2022,237 @@ describe("subagents-doctor slash command", { skip: !available ? "slash-commands.
1259
2022
  assert.deepEqual(params, { action: "doctor" });
1260
2023
  });
1261
2024
 
1262
- it("routes fleet to the read-only status view", async () => {
2025
+ it("keeps the textual fleet status fallback when no UI is available", async () => {
1263
2026
  const { params } = await captureSlashCommandParams("subagents-fleet", "", process.cwd());
1264
2027
  assert.deepEqual(params, { action: "status", view: "fleet" });
1265
2028
  });
1266
2029
 
2030
+ it("opens the native fleet from both the slash command and direct shortcut", async () => {
2031
+ const commands = new Map<string, RegisteredSlashCommand>();
2032
+ const shortcuts = new Map<string, { description: string; handler(ctx: unknown): Promise<void> }>();
2033
+ let opened = 0;
2034
+ let rendered = "";
2035
+ const custom = async (factory: unknown, options: unknown) => {
2036
+ opened++;
2037
+ const component = (factory as (tui: unknown, theme: unknown, keybindings: unknown, done: (value: undefined) => void) => { render(width: number): string[]; dispose(): void })(
2038
+ { terminal: { rows: 30, columns: 100 }, requestRender() {} },
2039
+ { fg: (_name: string, text: string) => text, bold: (text: string) => text },
2040
+ undefined,
2041
+ () => {},
2042
+ );
2043
+ rendered = component.render(100).join("\n");
2044
+ component.dispose();
2045
+ assert.deepEqual(options, { overlay: true, overlayOptions: { anchor: "center", width: "95%", minWidth: 60, maxHeight: "85%", margin: 1 } });
2046
+ return undefined;
2047
+ };
2048
+ const pi = {
2049
+ events: createEventBus(),
2050
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
2051
+ registerShortcut(key: string, spec: { description: string; handler(ctx: unknown): Promise<void> }) { shortcuts.set(key, spec); },
2052
+ sendMessage(_message: unknown) {},
2053
+ };
2054
+ const state = createState(process.cwd());
2055
+ const ctx = createCommandContext({ hasUI: true, custom });
2056
+ registerSlashCommands!(pi, state);
2057
+
2058
+ await commands.get("subagents-fleet")!.handler("", ctx);
2059
+ assert.equal(shortcuts.size, 1);
2060
+ const shortcut = [...shortcuts.values()][0]!;
2061
+ assert.equal(shortcut.description, "Open subagent fleet inspector");
2062
+ await shortcut.handler(ctx);
2063
+
2064
+ assert.equal(opened, 2);
2065
+ assert.match(rendered, /Subagent fleet/);
2066
+ assert.match(rendered, /inspection only/);
2067
+ });
2068
+
2069
+ it("routes subagents-stop with an id directly to the stop action", async () => {
2070
+ const { params } = await captureSlashCommandParams("subagents-stop", "run-123", process.cwd());
2071
+ assert.deepEqual(params, { action: "stop", id: "run-123" });
2072
+ });
2073
+
2074
+ it("prints exact stop commands when subagents-stop has no UI", async () => {
2075
+ await withIsolatedHome(async () => {
2076
+ const runId = `slash-stop-${Date.now().toString(36)}`;
2077
+ const asyncDir = path.join(ASYNC_DIR, runId);
2078
+ fs.mkdirSync(asyncDir, { recursive: true });
2079
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
2080
+ runId,
2081
+ sessionId: "session-test",
2082
+ mode: "single",
2083
+ state: "running",
2084
+ pid: process.pid,
2085
+ startedAt: Date.now(),
2086
+ lastUpdate: Date.now(),
2087
+ steps: [{ agent: "worker", status: "running", startedAt: Date.now() }],
2088
+ }, null, 2));
2089
+ try {
2090
+ const sent: unknown[] = [];
2091
+ const commands = new Map<string, RegisteredSlashCommand>();
2092
+ const pi = {
2093
+ events: createEventBus(),
2094
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
2095
+ registerShortcut() {},
2096
+ sendMessage(message: unknown) { sent.push(message); },
2097
+ };
2098
+ const state = createState(process.cwd());
2099
+ state.currentSessionId = "session-test";
2100
+ registerSlashCommands!(pi, state);
2101
+ await commands.get("subagents-stop")!.handler("", createCommandContext({ hasUI: false }));
2102
+
2103
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
2104
+ assert.match(content, new RegExp(`subagent\\({ action: "stop", id: "${runId}" }\\)`));
2105
+ assert.match(content, new RegExp(`/subagents-stop ${runId}`));
2106
+ } finally {
2107
+ fs.rmSync(asyncDir, { recursive: true, force: true });
2108
+ }
2109
+ });
2110
+ });
2111
+
2112
+ it("prints cancel commands for scheduled subagent runs when subagents-stop has no UI", async () => {
2113
+ await withIsolatedHome(async () => {
2114
+ await withTempProject("pi-slash-stop-scheduled-", async (root) => {
2115
+ const storePath = scheduledRunStorePath(root, "session-test");
2116
+ fs.mkdirSync(path.dirname(storePath), { recursive: true });
2117
+ fs.writeFileSync(storePath, JSON.stringify({
2118
+ version: 1,
2119
+ cwd: root,
2120
+ sessionId: "session-test",
2121
+ jobs: [{
2122
+ id: "job-1",
2123
+ name: "nightly scout",
2124
+ schedule: "+10m",
2125
+ runAt: Date.now() + 600_000,
2126
+ state: "scheduled",
2127
+ createdAt: Date.now(),
2128
+ updatedAt: Date.now(),
2129
+ cwd: root,
2130
+ sessionId: "session-test",
2131
+ params: { agent: "scout", task: "later", async: true },
2132
+ }],
2133
+ }, null, 2));
2134
+ const sent: unknown[] = [];
2135
+ const commands = new Map<string, RegisteredSlashCommand>();
2136
+ const state = createState(root);
2137
+ state.currentSessionId = "session-test";
2138
+ const pi = {
2139
+ events: createEventBus(),
2140
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
2141
+ registerShortcut() {},
2142
+ sendMessage(message: unknown) { sent.push(message); },
2143
+ };
2144
+ registerSlashCommands!(pi, state);
2145
+ await commands.get("subagents-stop")!.handler("", createCommandContext({ cwd: root, hasUI: false }));
2146
+
2147
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
2148
+ assert.match(content, /job-1 · nightly scout/);
2149
+ assert.match(content, /cancel scheduled run: subagent\({ action: "schedule-cancel", id: "job-1" }\)/);
2150
+ assert.doesNotMatch(content, /\/subagents-stop job-1/);
2151
+ });
2152
+ });
2153
+ });
2154
+
2155
+ it("routes selected scheduled subagents-stop targets through schedule-cancel", async () => {
2156
+ await withIsolatedHome(async () => {
2157
+ await withTempProject("pi-slash-stop-selected-scheduled-", async (root) => {
2158
+ const storePath = scheduledRunStorePath(root, "session-test");
2159
+ fs.mkdirSync(path.dirname(storePath), { recursive: true });
2160
+ fs.writeFileSync(storePath, JSON.stringify({
2161
+ version: 1,
2162
+ cwd: root,
2163
+ sessionId: "session-test",
2164
+ jobs: [{
2165
+ id: "job-2",
2166
+ name: "delayed worker",
2167
+ schedule: "+10m",
2168
+ runAt: Date.now() + 600_000,
2169
+ state: "scheduled",
2170
+ createdAt: Date.now(),
2171
+ updatedAt: Date.now(),
2172
+ cwd: root,
2173
+ sessionId: "session-test",
2174
+ params: { agent: "worker", task: "later", async: true },
2175
+ }],
2176
+ }, null, 2));
2177
+
2178
+ const commands = new Map<string, RegisteredSlashCommand>();
2179
+ const events = createEventBus();
2180
+ let requestedParams: unknown;
2181
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
2182
+ const payload = data as { requestId: string; params?: unknown };
2183
+ requestedParams = payload.params;
2184
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
2185
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
2186
+ requestId: payload.requestId,
2187
+ result: { content: [{ type: "text", text: "cancelled" }], details: { mode: "management", results: [] } },
2188
+ isError: false,
2189
+ });
2190
+ });
2191
+ const state = createState(root);
2192
+ state.currentSessionId = "session-test";
2193
+ const pi = {
2194
+ events,
2195
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
2196
+ registerShortcut() {},
2197
+ sendMessage(_message: unknown) {},
2198
+ };
2199
+ registerSlashCommands!(pi, state);
2200
+ await commands.get("subagents-stop")!.handler("", createCommandContext({
2201
+ cwd: root,
2202
+ hasUI: true,
2203
+ custom: async () => ({
2204
+ confirmed: true,
2205
+ target: { kind: "scheduled", id: "job-2", label: "job-2", detail: "scheduled", actionLabel: "cancel scheduled run" },
2206
+ }),
2207
+ }));
2208
+
2209
+ assert.deepEqual(requestedParams, { action: "schedule-cancel", id: "job-2" });
2210
+ });
2211
+ });
2212
+ });
2213
+
2214
+ it("prints fallback text instead of opening or enumerating the selector in child-safe fanout mode", async () => {
2215
+ await withIsolatedHome(async () => {
2216
+ const previous = process.env[SUBAGENT_FANOUT_CHILD_ENV];
2217
+ const runId = `slash-stop-child-safe-${Date.now().toString(36)}`;
2218
+ const asyncDir = path.join(ASYNC_DIR, runId);
2219
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
2220
+ fs.mkdirSync(asyncDir, { recursive: true });
2221
+ fs.writeFileSync(path.join(asyncDir, "status.json"), JSON.stringify({
2222
+ runId,
2223
+ sessionId: "session-test",
2224
+ mode: "single",
2225
+ state: "running",
2226
+ pid: process.pid,
2227
+ startedAt: Date.now(),
2228
+ lastUpdate: Date.now(),
2229
+ steps: [{ agent: "worker", status: "running", startedAt: Date.now() }],
2230
+ }, null, 2));
2231
+ try {
2232
+ const sent: unknown[] = [];
2233
+ const commands = new Map<string, RegisteredSlashCommand>();
2234
+ const pi = {
2235
+ events: createEventBus(),
2236
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
2237
+ registerShortcut() {},
2238
+ sendMessage(message: unknown) { sent.push(message); },
2239
+ };
2240
+ const state = createState(process.cwd());
2241
+ state.currentSessionId = "session-test";
2242
+ registerSlashCommands!(pi, state);
2243
+ await commands.get("subagents-stop")!.handler("", createCommandContext({ hasUI: true }));
2244
+
2245
+ const content = String((sent[0] as { content?: unknown }).content ?? "");
2246
+ assert.match(content, /Selector unavailable in child-safe fanout mode\./);
2247
+ assert.doesNotMatch(content, new RegExp(runId));
2248
+ } finally {
2249
+ fs.rmSync(asyncDir, { recursive: true, force: true });
2250
+ if (previous === undefined) delete process.env[SUBAGENT_FANOUT_CHILD_ENV];
2251
+ else process.env[SUBAGENT_FANOUT_CHILD_ENV] = previous;
2252
+ }
2253
+ });
2254
+ });
2255
+
1267
2256
  it("does not register the removed subagents-status overlay command", async () => {
1268
2257
  await withIsolatedHome(async () => {
1269
2258
  const commands = new Map<string, RegisteredSlashCommand>();