@selesai/code 0.3.0 → 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 (230) hide show
  1. package/dist/core/diagnostics.d.ts +1 -0
  2. package/dist/core/diagnostics.d.ts.map +1 -1
  3. package/dist/core/diagnostics.js.map +1 -1
  4. package/dist/core/package-manager.d.ts +10 -0
  5. package/dist/core/package-manager.d.ts.map +1 -1
  6. package/dist/core/package-manager.js +93 -8
  7. package/dist/core/package-manager.js.map +1 -1
  8. package/dist/core/package-manager.test.d.ts +2 -0
  9. package/dist/core/package-manager.test.d.ts.map +1 -0
  10. package/dist/core/package-manager.test.js +20 -0
  11. package/dist/core/package-manager.test.js.map +1 -0
  12. package/dist/core/resource-loader.d.ts +3 -0
  13. package/dist/core/resource-loader.d.ts.map +1 -1
  14. package/dist/core/resource-loader.js +10 -0
  15. package/dist/core/resource-loader.js.map +1 -1
  16. package/dist/core/settings-manager.d.ts +1 -0
  17. package/dist/core/settings-manager.d.ts.map +1 -1
  18. package/dist/core/settings-manager.js.map +1 -1
  19. package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
  20. package/dist/extensions/caveman/index.js +116 -0
  21. package/dist/extensions/caveman/package.json +8 -0
  22. package/dist/extensions/caveman/test/extension.test.js +185 -0
  23. package/dist/extensions/caveman/test/helpers.test.js +67 -0
  24. package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
  25. package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
  26. package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
  27. package/dist/extensions/pi-intercom/config.ts +2 -2
  28. package/dist/extensions/pi-intercom/index.ts +2 -2
  29. package/dist/extensions/pi-intercom/package.json +2 -2
  30. package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
  31. package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
  32. package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
  33. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
  34. package/dist/extensions/pi-powerline-footer/index.ts +9 -16
  35. package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
  36. package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
  37. package/dist/extensions/pi-rewind-hook/index.ts +1 -1
  38. package/dist/extensions/pi-rewind-hook/package.json +1 -1
  39. package/dist/extensions/pi-subagents/README.md +224 -52
  40. package/dist/extensions/pi-subagents/install.mjs +93 -0
  41. package/dist/extensions/pi-subagents/package.json +4 -5
  42. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  43. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  44. package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
  45. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
  46. package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
  47. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
  48. package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
  49. package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
  50. package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
  51. package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
  52. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
  53. package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
  54. package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
  55. package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
  56. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
  57. package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
  58. package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
  59. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
  60. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
  61. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
  62. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
  63. package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
  64. package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
  65. package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
  66. package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
  67. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
  68. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
  69. package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
  70. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
  71. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
  72. package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
  73. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
  74. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
  75. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
  76. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
  77. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
  78. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
  79. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
  80. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
  81. package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
  82. package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
  83. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
  84. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
  85. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
  86. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
  87. package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
  88. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
  89. package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
  90. package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
  91. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
  92. package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
  93. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
  94. package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
  95. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
  96. package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
  97. package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
  98. package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
  99. package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
  100. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
  101. package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
  102. package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
  103. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
  104. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
  105. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
  106. package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
  107. package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
  108. package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
  109. package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
  110. package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
  111. package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
  112. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
  113. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
  114. package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
  115. package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
  116. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
  117. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
  118. package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
  119. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
  120. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
  121. package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
  122. package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
  123. package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
  124. package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
  125. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
  126. package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
  127. package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
  128. package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
  129. package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
  130. package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
  131. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
  132. package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
  133. package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
  134. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
  135. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
  136. package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
  137. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
  138. package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
  139. package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
  140. package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
  141. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
  142. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
  143. package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
  144. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
  145. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
  146. package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
  147. package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
  148. package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
  149. package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
  150. package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
  151. package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
  152. package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
  153. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
  154. package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
  155. package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
  156. package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
  157. package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
  158. package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
  159. package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
  160. package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
  161. package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
  162. package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
  163. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
  164. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
  165. package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
  166. package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
  167. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
  168. package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
  169. package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
  170. package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
  171. package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
  172. package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
  173. package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
  174. package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
  175. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
  176. package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
  177. package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
  178. package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
  179. package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
  180. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
  181. package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
  182. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
  183. package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
  184. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
  185. package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
  186. package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
  187. package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
  188. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
  189. package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
  190. package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
  191. package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
  192. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
  193. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
  194. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
  195. package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
  196. package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
  197. package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
  198. package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
  199. package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
  200. package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
  201. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
  202. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
  203. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
  204. package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
  205. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
  206. package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
  207. package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
  208. package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
  209. package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
  210. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
  211. package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
  212. package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
  213. package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
  214. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
  215. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
  216. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
  217. package/dist/main.d.ts.map +1 -1
  218. package/dist/main.js +4 -0
  219. package/dist/main.js.map +1 -1
  220. package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
  221. package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
  222. package/dist/modes/interactive/components/first-time-setup.js +19 -5
  223. package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
  224. package/dist/skills/caveman/SKILL.md +49 -0
  225. package/dist/skills/handoff-text/SKILL.md +13 -0
  226. package/docs/docs.json +4 -0
  227. package/docs/extensions.md +2 -0
  228. package/docs/settings.md +15 -0
  229. package/docs/shared-host-extensions.md +109 -0
  230. package/package.json +1 -1
@@ -0,0 +1,1284 @@
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 { beforeEach, describe, it } from "node:test";
6
+
7
+ import { ASYNC_DIR } from "../../src/shared/types.ts";
8
+
9
+ const SLASH_RESULT_TYPE = "subagent-slash-result";
10
+ const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
11
+ const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
12
+ const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
13
+
14
+ interface EventBus {
15
+ on(event: string, handler: (data: unknown) => void): () => void;
16
+ emit(event: string, data: unknown): void;
17
+ }
18
+
19
+ type RegisteredSlashCommand = { handler(args: string, ctx: unknown): Promise<void>; getArgumentCompletions?: (prefix: string) => unknown };
20
+
21
+ interface RegisterSlashCommandsModule {
22
+ registerSlashCommands?: (
23
+ pi: {
24
+ events: EventBus;
25
+ registerCommand(
26
+ name: string,
27
+ spec: RegisteredSlashCommand,
28
+ ): void;
29
+ registerShortcut(key: string, spec: { handler(ctx: unknown): Promise<void> }): void;
30
+ sendMessage(message: unknown): void;
31
+ setModel?(model: unknown): Promise<boolean>;
32
+ },
33
+ state: {
34
+ baseCwd: string;
35
+ currentSessionId: string | null;
36
+ asyncJobs: Map<string, unknown>;
37
+ cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
38
+ lastUiContext: unknown;
39
+ poller: NodeJS.Timeout | null;
40
+ completionSeen: Map<string, number>;
41
+ watcher: unknown;
42
+ watcherRestartTimer: ReturnType<typeof setTimeout> | null;
43
+ resultFileCoalescer: { schedule(file: string, delayMs?: number): boolean; clear(): void };
44
+ },
45
+ ) => void;
46
+ }
47
+
48
+ interface SlashLiveStateModule {
49
+ clearSlashSnapshots?: typeof import("../../src/slash/slash-live-state.ts").clearSlashSnapshots;
50
+ getSlashRenderableSnapshot?: typeof import("../../src/slash/slash-live-state.ts").getSlashRenderableSnapshot;
51
+ resolveSlashMessageDetails?: typeof import("../../src/slash/slash-live-state.ts").resolveSlashMessageDetails;
52
+ }
53
+
54
+ let registerSlashCommands: RegisterSlashCommandsModule["registerSlashCommands"];
55
+ let clearSlashSnapshots: SlashLiveStateModule["clearSlashSnapshots"];
56
+ let getSlashRenderableSnapshot: SlashLiveStateModule["getSlashRenderableSnapshot"];
57
+ let resolveSlashMessageDetails: SlashLiveStateModule["resolveSlashMessageDetails"];
58
+ let available = true;
59
+ try {
60
+ ({ registerSlashCommands } = await import("../../src/slash/slash-commands.ts") as RegisterSlashCommandsModule);
61
+ ({ clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails } = await import("../../src/slash/slash-live-state.ts") as SlashLiveStateModule);
62
+ } catch {
63
+ available = false;
64
+ }
65
+
66
+ function createEventBus(): EventBus {
67
+ const handlers = new Map<string, Array<(data: unknown) => void>>();
68
+ return {
69
+ on(event, handler) {
70
+ const existing = handlers.get(event) ?? [];
71
+ existing.push(handler);
72
+ handlers.set(event, existing);
73
+ return () => {
74
+ const current = handlers.get(event) ?? [];
75
+ handlers.set(event, current.filter((entry) => entry !== handler));
76
+ };
77
+ },
78
+ emit(event, data) {
79
+ for (const handler of handlers.get(event) ?? []) {
80
+ handler(data);
81
+ }
82
+ },
83
+ };
84
+ }
85
+
86
+ function createState(cwd: string) {
87
+ return {
88
+ baseCwd: cwd,
89
+ currentSessionId: null,
90
+ asyncJobs: new Map(),
91
+ cleanupTimers: new Map(),
92
+ lastUiContext: null,
93
+ poller: null,
94
+ completionSeen: new Map(),
95
+ watcher: null,
96
+ watcherRestartTimer: null,
97
+ resultFileCoalescer: {
98
+ schedule: () => false,
99
+ clear: () => {},
100
+ },
101
+ };
102
+ }
103
+
104
+ async function withIsolatedHome<T>(fn: () => Promise<T>): Promise<T> {
105
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-slash-home-"));
106
+ const previousHome = process.env.HOME;
107
+ const previousUserProfile = process.env.USERPROFILE;
108
+ process.env.HOME = home;
109
+ process.env.USERPROFILE = home;
110
+ try {
111
+ return await fn();
112
+ } finally {
113
+ if (previousHome === undefined) delete process.env.HOME;
114
+ else process.env.HOME = previousHome;
115
+ if (previousUserProfile === undefined) delete process.env.USERPROFILE;
116
+ else process.env.USERPROFILE = previousUserProfile;
117
+ fs.rmSync(home, { recursive: true, force: true });
118
+ }
119
+ }
120
+
121
+
122
+ function createCommandContext(
123
+ overrides: Partial<{
124
+ cwd: string;
125
+ hasUI: boolean;
126
+ custom: (...args: unknown[]) => Promise<unknown>;
127
+ notify: (message: string, type?: string) => void;
128
+ confirm: (title: string, message: string) => Promise<boolean>;
129
+ setStatus: (key: string, text: string | undefined) => void;
130
+ setToolsExpanded: (expanded: boolean) => void;
131
+ sessionManager: unknown;
132
+ modelRegistry: { getAvailable: () => Array<{ provider: string; id: string }>; find?: (provider: string, id: string) => unknown };
133
+ }> = {},
134
+ ) {
135
+ return {
136
+ cwd: overrides.cwd ?? process.cwd(),
137
+ hasUI: overrides.hasUI ?? false,
138
+ ui: {
139
+ notify: overrides.notify ?? ((_message: string) => {}),
140
+ confirm: overrides.confirm ?? (async () => false),
141
+ setStatus: overrides.setStatus ?? ((_key: string, _text: string | undefined) => {}),
142
+ setToolsExpanded: overrides.setToolsExpanded ?? ((_expanded: boolean) => {}),
143
+ onTerminalInput: () => () => {},
144
+ custom: overrides.custom ?? (async () => undefined),
145
+ },
146
+ modelRegistry: overrides.modelRegistry ?? { getAvailable: () => [], find: () => undefined },
147
+ sessionManager: overrides.sessionManager ?? {
148
+ getSessionFile: () => null,
149
+ getSessionId: () => "session-test",
150
+ },
151
+ };
152
+ }
153
+
154
+ async function withTempProject<T>(prefix: string, fn: (root: string) => Promise<T>): Promise<T> {
155
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
156
+ fs.mkdirSync(path.join(root, ".selesai", "agents"), { recursive: true });
157
+ fs.mkdirSync(path.join(root, ".selesai", "chains"), { recursive: true });
158
+ try {
159
+ return await fn(root);
160
+ } finally {
161
+ fs.rmSync(root, { recursive: true, force: true });
162
+ }
163
+ }
164
+
165
+ function writeProjectChain(root: string, fileName: string, content: string): void {
166
+ fs.writeFileSync(path.join(root, ".selesai", "chains", fileName), content, "utf-8");
167
+ }
168
+
169
+ async function captureSlashCommandParams(
170
+ commandName: string,
171
+ args: string,
172
+ cwd: string,
173
+ setup?: () => void,
174
+ ): Promise<{ params: unknown; notifications: string[] }> {
175
+ return withIsolatedHome(async () => {
176
+ setup?.();
177
+ const commands = new Map<string, RegisteredSlashCommand>();
178
+ const events = createEventBus();
179
+ let requestedParams: unknown;
180
+ const notifications: string[] = [];
181
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
182
+ const payload = data as { requestId: string; params?: unknown };
183
+ requestedParams = payload.params;
184
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
185
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
186
+ requestId: payload.requestId,
187
+ result: {
188
+ content: [{ type: "text", text: `${commandName} finished` }],
189
+ details: { mode: "chain", results: [] },
190
+ },
191
+ isError: false,
192
+ });
193
+ });
194
+
195
+ const pi = {
196
+ events,
197
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
198
+ commands.set(name, spec);
199
+ },
200
+ registerShortcut() {},
201
+ sendMessage(_message: unknown) {},
202
+ };
203
+
204
+ registerSlashCommands!(pi, createState(cwd));
205
+ await commands.get(commandName)!.handler(args, createCommandContext({
206
+ cwd,
207
+ notify: (message) => {
208
+ notifications.push(message);
209
+ },
210
+ }));
211
+ return { params: requestedParams, notifications };
212
+ });
213
+ }
214
+
215
+ describe("slash command custom message delivery", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
216
+ beforeEach(() => {
217
+ clearSlashSnapshots?.();
218
+ });
219
+
220
+ it("/run accepts an agent without a task", async () => {
221
+ const sent: unknown[] = [];
222
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
223
+ const events = createEventBus();
224
+ let requestedParams: unknown;
225
+ let requestedCtx: unknown;
226
+ const sessionManager = {
227
+ flushed: false,
228
+ rewrites: 0,
229
+ getSessionFile: () => "session.jsonl",
230
+ _rewriteFile() {
231
+ this.rewrites++;
232
+ },
233
+ };
234
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
235
+ const payload = data as { requestId: string; params?: unknown; ctx?: unknown };
236
+ requestedParams = payload.params;
237
+ requestedCtx = payload.ctx;
238
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
239
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
240
+ requestId: payload.requestId,
241
+ result: {
242
+ content: [{ type: "text", text: "Commit finished" }],
243
+ details: { mode: "single", results: [] },
244
+ },
245
+ isError: false,
246
+ });
247
+ });
248
+
249
+ const pi = {
250
+ events,
251
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
252
+ commands.set(name, spec);
253
+ },
254
+ registerShortcut() {},
255
+ sendMessage(message: unknown) {
256
+ sent.push(message);
257
+ },
258
+ };
259
+
260
+ const ctx = createCommandContext({ sessionManager });
261
+ registerSlashCommands!(pi, createState(process.cwd()));
262
+ await commands.get("run")!.handler("scout", ctx);
263
+
264
+ assert.deepEqual(requestedParams, { agent: "scout", task: "", clarify: false, agentScope: "both" });
265
+ assert.equal(requestedCtx, ctx);
266
+ assert.equal(sent.length, 2);
267
+ assert.equal((sent[0] as { display?: boolean }).display, true);
268
+ assert.equal((sent[0] as { content?: string }).content, "Running subagent...");
269
+ assert.equal((sent[1] as { display?: boolean }).display, true);
270
+ assert.match((sent[1] as { content?: string }).content ?? "", /Commit finished/);
271
+ assert.equal(sessionManager.rewrites, 2);
272
+ assert.equal(sessionManager.flushed, true);
273
+ });
274
+
275
+ it("/run finalizes the slash snapshot before the last UI redraw on success", async () => {
276
+ const sent: unknown[] = [];
277
+ const log: string[] = [];
278
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
279
+ const events = createEventBus();
280
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
281
+ const requestId = (data as { requestId: string }).requestId;
282
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
283
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
284
+ requestId,
285
+ result: {
286
+ content: [{ type: "text", text: "Scout finished" }],
287
+ details: { mode: "single", results: [{ sessionFile: "/tmp/child-session.jsonl" }] },
288
+ },
289
+ isError: false,
290
+ });
291
+ });
292
+
293
+ const pi = {
294
+ events,
295
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
296
+ commands.set(name, spec);
297
+ },
298
+ registerShortcut() {},
299
+ sendMessage(message: unknown) {
300
+ sent.push(message);
301
+ log.push(`send:${(message as { display?: boolean }).display === false ? "hidden" : "visible"}`);
302
+ },
303
+ };
304
+
305
+ registerSlashCommands!(pi, createState(process.cwd()));
306
+ await commands.get("run")!.handler("scout inspect this", createCommandContext({
307
+ hasUI: true,
308
+ setStatus: (_key, text) => {
309
+ log.push(`status:${text ?? "clear"}`);
310
+ },
311
+ }));
312
+
313
+ assert.equal(sent.length, 2);
314
+ assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
315
+ assert.equal((sent[0] as { display?: boolean }).display, true);
316
+ assert.equal((sent[0] as { content?: string }).content, "inspect this");
317
+ assert.equal((sent[1] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
318
+ assert.equal((sent[1] as { display?: boolean }).display, false);
319
+ assert.match((sent[1] as { content?: string }).content ?? "", /Scout finished/);
320
+ assert.match((sent[1] as { content?: string }).content ?? "", /Child session exports\n\n- `\/tmp\/child-session\.jsonl`/);
321
+ assert.deepEqual(log, ["send:visible", "status:running...", "send:hidden", "status:clear"]);
322
+
323
+ const visibleDetails = resolveSlashMessageDetails!((sent[0] as { details?: unknown }).details);
324
+ assert.ok(visibleDetails);
325
+ const visibleSnapshot = getSlashRenderableSnapshot!(visibleDetails!);
326
+ assert.equal((visibleSnapshot.result.content[0] as { text?: string }).text, "Scout finished");
327
+ });
328
+
329
+ it("/run collapses tool detail before showing the initial live card", async () => {
330
+ const log: string[] = [];
331
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
332
+ const events = createEventBus();
333
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
334
+ const requestId = (data as { requestId: string }).requestId;
335
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
336
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
337
+ requestId,
338
+ result: { content: [{ type: "text", text: "done" }], details: { mode: "single", results: [] } },
339
+ isError: false,
340
+ });
341
+ });
342
+
343
+ const pi = {
344
+ events,
345
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
346
+ commands.set(name, spec);
347
+ },
348
+ registerShortcut() {},
349
+ sendMessage() {
350
+ log.push("send");
351
+ },
352
+ };
353
+
354
+ registerSlashCommands!(pi, createState(process.cwd()));
355
+ await commands.get("run")!.handler("scout inspect this", createCommandContext({
356
+ hasUI: true,
357
+ setToolsExpanded: (expanded) => log.push(`expanded:${String(expanded)}`),
358
+ }));
359
+
360
+ assert.deepEqual(log.slice(0, 2), ["expanded:false", "send"]);
361
+ });
362
+
363
+ it("/run finalizes the slash snapshot before the last UI redraw on error", async () => {
364
+ const sent: unknown[] = [];
365
+ const log: string[] = [];
366
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
367
+ const events = createEventBus();
368
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
369
+ const requestId = (data as { requestId: string }).requestId;
370
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
371
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
372
+ requestId,
373
+ result: {
374
+ content: [{ type: "text", text: "Subagent failed" }],
375
+ details: { mode: "single", results: [] },
376
+ },
377
+ isError: true,
378
+ errorText: "Subagent failed",
379
+ });
380
+ });
381
+
382
+ const pi = {
383
+ events,
384
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
385
+ commands.set(name, spec);
386
+ },
387
+ registerShortcut() {},
388
+ sendMessage(message: unknown) {
389
+ sent.push(message);
390
+ log.push(`send:${(message as { display?: boolean }).display === false ? "hidden" : "visible"}`);
391
+ },
392
+ };
393
+
394
+ registerSlashCommands!(pi, createState(process.cwd()));
395
+ await commands.get("run")!.handler("scout inspect this", createCommandContext({
396
+ hasUI: true,
397
+ setStatus: (_key, text) => {
398
+ log.push(`status:${text ?? "clear"}`);
399
+ },
400
+ }));
401
+
402
+ assert.equal(sent.length, 2);
403
+ assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
404
+ assert.equal((sent[0] as { display?: boolean }).display, true);
405
+ assert.equal((sent[0] as { content?: string }).content, "inspect this");
406
+ assert.equal((sent[1] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
407
+ assert.equal((sent[1] as { display?: boolean }).display, false);
408
+ assert.match((sent[1] as { content?: string }).content ?? "", /Subagent failed/);
409
+ assert.deepEqual(log, ["send:visible", "status:running...", "send:hidden", "status:clear"]);
410
+
411
+ const visibleDetails = resolveSlashMessageDetails!((sent[0] as { details?: unknown }).details);
412
+ assert.ok(visibleDetails);
413
+ const visibleSnapshot = getSlashRenderableSnapshot!(visibleDetails!);
414
+ assert.equal((visibleSnapshot.result.content[0] as { text?: string }).text, "Subagent failed");
415
+ });
416
+
417
+ it("/parallel forwards inline output behavior config", async () => {
418
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
419
+ const events = createEventBus();
420
+ let requestedParams: unknown;
421
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
422
+ const payload = data as { requestId: string; params?: unknown };
423
+ requestedParams = payload.params;
424
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
425
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
426
+ requestId: payload.requestId,
427
+ result: {
428
+ content: [{ type: "text", text: "parallel finished" }],
429
+ details: { mode: "parallel", results: [] },
430
+ },
431
+ isError: false,
432
+ });
433
+ });
434
+
435
+ const pi = {
436
+ events,
437
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
438
+ commands.set(name, spec);
439
+ },
440
+ registerShortcut() {},
441
+ sendMessage(_message: unknown) {},
442
+ };
443
+
444
+ registerSlashCommands!(pi, createState(process.cwd()));
445
+ await commands.get("parallel")!.handler("scout[output=x.md,outputMode=file-only,reads=a.md+b.md,progress] -- Review", createCommandContext());
446
+
447
+ assert.deepEqual(requestedParams, {
448
+ tasks: [{ agent: "scout", task: "Review", output: "x.md", outputMode: "file-only", reads: ["a.md", "b.md"], progress: true }],
449
+ clarify: false,
450
+ agentScope: "both",
451
+ });
452
+ });
453
+
454
+ it("/parallel no longer hard-blocks runs above the old 8-task limit before the executor responds", async () => {
455
+ const sent: unknown[] = [];
456
+ const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
457
+ const events = createEventBus();
458
+ let requestedTasks = 0;
459
+ events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
460
+ const payload = data as { requestId: string; params?: { tasks?: unknown[] } };
461
+ requestedTasks = payload.params?.tasks?.length ?? 0;
462
+ events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
463
+ events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
464
+ requestId: payload.requestId,
465
+ result: {
466
+ content: [{ type: "text", text: "parallel finished" }],
467
+ details: { mode: "parallel", results: [] },
468
+ },
469
+ isError: false,
470
+ });
471
+ });
472
+
473
+ const pi = {
474
+ events,
475
+ registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
476
+ commands.set(name, spec);
477
+ },
478
+ registerShortcut() {},
479
+ sendMessage(message: unknown) {
480
+ sent.push(message);
481
+ },
482
+ };
483
+
484
+ registerSlashCommands!(pi, createState(process.cwd()));
485
+ const args = Array.from({ length: 9 }, (_, index) => `scout \"task ${index + 1}\"`).join(" -> ");
486
+ await commands.get("parallel")!.handler(args, createCommandContext());
487
+
488
+ assert.equal(requestedTasks, 9);
489
+ assert.equal(sent.length, 2);
490
+ assert.match((sent[1] as { content?: string }).content ?? "", /parallel finished/);
491
+ });
492
+ });
493
+
494
+ describe("saved chain slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
495
+ beforeEach(() => {
496
+ clearSlashSnapshots?.();
497
+ });
498
+
499
+ it("/run and /chain accept dotted packaged runtime agent names", async () => {
500
+ await withTempProject("pi-packaged-agent-slash-", async (root) => {
501
+ fs.writeFileSync(path.join(root, ".selesai", "agents", "code-analysis.scout.md"), `---
502
+ name: scout
503
+ package: code-analysis
504
+ description: Fast recon
505
+ ---
506
+
507
+ Inspect
508
+ `, "utf-8");
509
+ fs.writeFileSync(path.join(root, ".selesai", "agents", "documentation.writer.md"), `---
510
+ name: writer
511
+ package: documentation
512
+ description: Writer
513
+ ---
514
+
515
+ Write
516
+ `, "utf-8");
517
+
518
+ const run = await captureSlashCommandParams("run", "code-analysis.scout Investigate", root);
519
+ assert.deepEqual(run.params, { agent: "code-analysis.scout", task: "Investigate", clarify: false, agentScope: "both" });
520
+
521
+ const chain = await captureSlashCommandParams("chain", "code-analysis.scout \"Scan\" -> documentation.writer", root);
522
+ assert.deepEqual((chain.params as { chain?: Array<{ agent?: string; task?: string }> }).chain?.map(({ agent, task }) => ({ agent, task })), [
523
+ { agent: "code-analysis.scout", task: "Scan" },
524
+ { agent: "documentation.writer", task: undefined },
525
+ ]);
526
+
527
+ await withIsolatedHome(async () => {
528
+ const commands = new Map<string, RegisteredSlashCommand>();
529
+ const pi = {
530
+ events: createEventBus(),
531
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
532
+ registerShortcut() {},
533
+ sendMessage(_message: unknown) {},
534
+ };
535
+ registerSlashCommands!(pi, createState(root));
536
+ const runCompletions = commands.get("run")!.getArgumentCompletions!("code-") as Array<{ value: string; label: string }>;
537
+ assert.deepEqual(runCompletions.map((completion) => completion.value), ["code-analysis.scout"]);
538
+ const chainCompletions = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout \"Scan\" -> doc") as Array<{ value: string; label: string }>;
539
+ assert.deepEqual(chainCompletions.map((completion) => completion.value), ["code-analysis.scout \"Scan\" -> documentation.writer"]);
540
+ // Regression: bare group-ish syntax inside a `--` shared task is plain text, not
541
+ // a group separator, so it must not resume agent completion past the task.
542
+ const pipeInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do x | doc");
543
+ assert.equal(pipeInTask, null);
544
+ const openParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do (doc");
545
+ assert.equal(openParenInTask, null);
546
+ const closeParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do ) doc");
547
+ assert.equal(closeParenInTask, null);
548
+ const balancedParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do (x) doc");
549
+ assert.equal(balancedParenInTask, null);
550
+ // Inside an actual parallel group, `|` still separates tasks and completes agents.
551
+ const groupCompletions = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout \"Scan\" -> (documentation.writer \"w\" | code") as Array<{ value: string; label: string }>;
552
+ assert.deepEqual(groupCompletions.map((completion) => completion.value), ["code-analysis.scout \"Scan\" -> (documentation.writer \"w\" | code-analysis.scout"]);
553
+ });
554
+ });
555
+ });
556
+
557
+ it("/run-chain launches a saved chain with a shared task", async () => {
558
+ await withTempProject("pi-run-chain-success-", async (root) => {
559
+ writeProjectChain(root, "review-flow.chain.md", `---
560
+ name: review-flow
561
+ description: Review flow
562
+ ---
563
+
564
+ ## scout
565
+
566
+ Scan {task}
567
+
568
+ ## reviewer
569
+
570
+ Review {previous}
571
+ `);
572
+
573
+ const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit the auth flow", root);
574
+ const runParams = params as {
575
+ chain?: Array<{ agent?: string; task?: string }>;
576
+ task?: string;
577
+ clarify?: boolean;
578
+ agentScope?: string;
579
+ async?: unknown;
580
+ context?: unknown;
581
+ };
582
+
583
+ assert.deepEqual(runParams.chain?.map(({ agent, task }) => ({ agent, task })), [
584
+ { agent: "scout", task: "Scan {task}" },
585
+ { agent: "reviewer", task: "Review {previous}" },
586
+ ]);
587
+ assert.equal(runParams.task, "Audit the auth flow");
588
+ assert.equal(runParams.clarify, false);
589
+ assert.equal(runParams.agentScope, "both");
590
+ assert.equal(runParams.async, undefined);
591
+ assert.equal(runParams.context, undefined);
592
+ });
593
+ });
594
+
595
+ it("/run-chain launches a saved JSON chain with dynamic fanout", async () => {
596
+ await withTempProject("pi-run-chain-json-dynamic-", async (root) => {
597
+ writeProjectChain(root, "dynamic-review.chain.json", JSON.stringify({
598
+ name: "dynamic-review",
599
+ description: "Dynamic review flow",
600
+ chain: [
601
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
602
+ {
603
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
604
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
605
+ collect: { as: "reviews" },
606
+ },
607
+ ],
608
+ }));
609
+
610
+ const { params } = await captureSlashCommandParams("run-chain", "dynamic-review -- Audit", root);
611
+ const runParams = params as { chain?: Array<Record<string, unknown>>; task?: string; clarify?: boolean; agentScope?: string };
612
+
613
+ assert.equal(runParams.task, "Audit");
614
+ assert.equal(runParams.clarify, false);
615
+ assert.equal(runParams.agentScope, "both");
616
+ assert.equal(runParams.chain?.[0]?.agent, "scout");
617
+ assert.deepEqual(runParams.chain?.[1]?.expand, { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 });
618
+ assert.deepEqual(runParams.chain?.[1]?.collect, { as: "reviews" });
619
+ });
620
+ });
621
+
622
+ it("/run-chain preserves saved JSON chain acceptance contracts", async () => {
623
+ await withTempProject("pi-run-chain-json-acceptance-", async (root) => {
624
+ writeProjectChain(root, "verified-flow.chain.json", JSON.stringify({
625
+ name: "verified-flow",
626
+ description: "Verified flow",
627
+ chain: [
628
+ {
629
+ agent: "worker",
630
+ task: "Implement fix",
631
+ acceptance: {
632
+ level: "verified",
633
+ verify: [{ id: "tests", command: "npm test" }],
634
+ },
635
+ },
636
+ ],
637
+ }));
638
+
639
+ const { params } = await captureSlashCommandParams("run-chain", "verified-flow -- Audit", root);
640
+ assert.deepEqual((params as { chain?: Array<{ acceptance?: unknown }> }).chain?.[0]?.acceptance, {
641
+ level: "verified",
642
+ verify: [{ id: "tests", command: "npm test" }],
643
+ });
644
+ });
645
+ });
646
+
647
+ it("/run-chain launches and completes packaged saved chains by dotted runtime name", async () => {
648
+ await withTempProject("pi-run-chain-packaged-", async (root) => {
649
+ writeProjectChain(root, "code-analysis.review-flow.chain.md", `---
650
+ name: review-flow
651
+ package: code-analysis
652
+ description: Review flow
653
+ ---
654
+
655
+ ## code-analysis.scout
656
+
657
+ Scan {task}
658
+ `);
659
+
660
+ const { params } = await captureSlashCommandParams("run-chain", "code-analysis.review-flow -- Audit", root);
661
+ assert.equal((params as { task?: string }).task, "Audit");
662
+ assert.deepEqual((params as { chain?: Array<{ agent?: string; task?: string }> }).chain?.map(({ agent, task }) => ({ agent, task })), [
663
+ { agent: "code-analysis.scout", task: "Scan {task}" },
664
+ ]);
665
+
666
+ await withIsolatedHome(async () => {
667
+ const commands = new Map<string, RegisteredSlashCommand>();
668
+ const pi = {
669
+ events: createEventBus(),
670
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
671
+ registerShortcut() {},
672
+ sendMessage(_message: unknown) {},
673
+ };
674
+ registerSlashCommands!(pi, createState(root));
675
+ const completions = commands.get("run-chain")!.getArgumentCompletions!("code-") as Array<{ value: string; label: string }>;
676
+ assert.deepEqual(completions.map((completion) => completion.value), ["code-analysis.review-flow"]);
677
+ });
678
+ });
679
+ });
680
+
681
+ it("/run-chain reports an unknown saved chain without launching", async () => {
682
+ await withTempProject("pi-run-chain-unknown-", async (root) => {
683
+ const { params, notifications } = await captureSlashCommandParams("run-chain", "missing -- Do work", root);
684
+
685
+ assert.equal(params, undefined);
686
+ assert.deepEqual(notifications, ["Unknown chain: missing"]);
687
+ });
688
+ });
689
+
690
+ it("/run-chain suggests saved chain names", async () => {
691
+ await withTempProject("pi-run-chain-completions-", async (root) => {
692
+ writeProjectChain(root, "review-flow.chain.md", `---
693
+ name: review-flow
694
+ description: Review flow
695
+ ---
696
+
697
+ ## scout
698
+
699
+ Scan
700
+ `);
701
+ writeProjectChain(root, "release-flow.chain.md", `---
702
+ name: release-flow
703
+ description: Release flow
704
+ ---
705
+
706
+ ## planner
707
+
708
+ Plan
709
+ `);
710
+ writeProjectChain(root, "triage.chain.md", `---
711
+ name: triage
712
+ description: Triage flow
713
+ ---
714
+
715
+ ## scout
716
+
717
+ Triage
718
+ `);
719
+
720
+ await withIsolatedHome(async () => {
721
+ const commands = new Map<string, RegisteredSlashCommand>();
722
+ const pi = {
723
+ events: createEventBus(),
724
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
725
+ commands.set(name, spec);
726
+ },
727
+ registerShortcut() {},
728
+ sendMessage(_message: unknown) {},
729
+ };
730
+
731
+ registerSlashCommands!(pi, createState(root));
732
+ const completions = commands.get("run-chain")!.getArgumentCompletions!("re") as Array<{ value: string; label: string }>;
733
+ assert.deepEqual(completions.map((completion) => completion.value).sort(), ["release-flow", "review-flow"]);
734
+ assert.deepEqual(completions.map((completion) => completion.label).sort(), ["release-flow", "review-flow"]);
735
+ assert.equal(commands.get("run-chain")!.getArgumentCompletions!("review-flow -- "), null);
736
+ });
737
+ });
738
+ });
739
+
740
+ it("/run-chain maps --bg to async execution", async () => {
741
+ await withTempProject("pi-run-chain-bg-", async (root) => {
742
+ writeProjectChain(root, "review-flow.chain.md", `---
743
+ name: review-flow
744
+ description: Review flow
745
+ ---
746
+
747
+ ## scout
748
+
749
+ Scan
750
+ `);
751
+
752
+ const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit --bg", root);
753
+
754
+ assert.equal((params as { async?: unknown }).async, true);
755
+ assert.equal((params as { context?: unknown }).context, undefined);
756
+ });
757
+ });
758
+
759
+ it("/run-chain maps --fork to forked context", async () => {
760
+ await withTempProject("pi-run-chain-fork-", async (root) => {
761
+ writeProjectChain(root, "review-flow.chain.md", `---
762
+ name: review-flow
763
+ description: Review flow
764
+ ---
765
+
766
+ ## scout
767
+
768
+ Scan
769
+ `);
770
+
771
+ const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit --fork", root);
772
+
773
+ assert.equal((params as { context?: unknown }).context, "fork");
774
+ assert.equal((params as { async?: unknown }).async, undefined);
775
+ });
776
+ });
777
+
778
+ it("/run-chain prefers a project saved chain over a same-named user chain", async () => {
779
+ await withTempProject("pi-run-chain-priority-", async (root) => {
780
+ writeProjectChain(root, "review-flow.chain.md", `---
781
+ name: review-flow
782
+ description: Project review flow
783
+ ---
784
+
785
+ ## scout
786
+
787
+ Project chain task
788
+ `);
789
+
790
+ const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Shared task", root, () => {
791
+ const userChainsDir = path.join(os.homedir(), ".selesai", "agent", "chains");
792
+ fs.mkdirSync(userChainsDir, { recursive: true });
793
+ fs.writeFileSync(path.join(userChainsDir, "review-flow.chain.md"), `---
794
+ name: review-flow
795
+ description: User review flow
796
+ ---
797
+
798
+ ## scout
799
+
800
+ User chain task
801
+ `, "utf-8");
802
+ });
803
+
804
+ assert.equal((params as { chain?: Array<{ task?: string }> }).chain?.[0]?.task, "Project chain task");
805
+ });
806
+ });
807
+
808
+ it("/run-chain resolves saved outputSchema files at the command boundary", async () => {
809
+ await withTempProject("pi-run-chain-schema-", async (root) => {
810
+ const schemasDir = path.join(root, ".selesai", "chains", "schemas");
811
+ fs.mkdirSync(schemasDir, { recursive: true });
812
+ fs.writeFileSync(path.join(schemasDir, "finding.schema.json"), JSON.stringify({ type: "object", properties: { ok: { type: "boolean" } } }), "utf-8");
813
+ writeProjectChain(root, "schema-flow.chain.md", `---
814
+ name: schema-flow
815
+ description: Schema flow
816
+ ---
817
+
818
+ ## scout
819
+ outputSchema: ./schemas/finding.schema.json
820
+
821
+ Gather context
822
+ `);
823
+
824
+ const { params } = await captureSlashCommandParams("run-chain", "schema-flow -- Shared task", root);
825
+
826
+ assert.deepEqual((params as { chain?: Array<{ outputSchema?: unknown }> }).chain?.[0]?.outputSchema, {
827
+ type: "object",
828
+ properties: { ok: { type: "boolean" } },
829
+ });
830
+ });
831
+ });
832
+
833
+ it("/run-chain preserves saved step behavior fields", async () => {
834
+ await withTempProject("pi-run-chain-fields-", async (root) => {
835
+ writeProjectChain(root, "field-flow.chain.md", `---
836
+ name: field-flow
837
+ description: Field flow
838
+ ---
839
+
840
+ ## scout
841
+ output: context.md
842
+ outputMode: file-only
843
+ reads: input.md, notes.md
844
+ model: openai/gpt-5.5
845
+ skills: research, audit
846
+ progress: true
847
+
848
+ Gather context
849
+ `);
850
+
851
+ const { params } = await captureSlashCommandParams("run-chain", "field-flow -- Shared task", root);
852
+
853
+ assert.deepEqual((params as { chain?: unknown[] }).chain?.[0], {
854
+ agent: "scout",
855
+ task: "Gather context",
856
+ output: "context.md",
857
+ outputMode: "file-only",
858
+ reads: ["input.md", "notes.md"],
859
+ progress: true,
860
+ skill: ["research", "audit"],
861
+ model: "openai/gpt-5.5",
862
+ });
863
+ });
864
+ });
865
+
866
+ it("/chain parses a parenthesized parallel group into a { parallel: [...] } step", async () => {
867
+ await withTempProject("pi-chain-group-slash-", async (root) => {
868
+ for (const name of ["scout", "reviewer", "writer"]) {
869
+ fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
870
+ }
871
+
872
+ const { params, notifications } = await captureSlashCommandParams("chain", 'scout "scan" -> (reviewer "A" | reviewer "B") -> writer "fix"', root);
873
+ assert.deepEqual(notifications, []);
874
+ const built = params as { chain?: Array<Record<string, unknown>>; task?: string };
875
+ assert.equal(built.task, "scan");
876
+ assert.equal(built.chain?.length, 3);
877
+ assert.equal(built.chain?.[0]?.agent, "scout");
878
+ const parallel = built.chain?.[1]?.parallel as Array<{ agent: string; task: string }>;
879
+ assert.ok(Array.isArray(parallel), "second step should be a parallel group");
880
+ assert.deepEqual(parallel.map(({ agent, task }) => ({ agent, task })), [
881
+ { agent: "reviewer", task: "A" },
882
+ { agent: "reviewer", task: "B" },
883
+ ]);
884
+ assert.equal(built.chain?.[2]?.agent, "writer");
885
+ });
886
+ });
887
+
888
+ it("/chain reports parallel-group errors as notifications and does not launch", async () => {
889
+ await withTempProject("pi-chain-group-error-", async (root) => {
890
+ for (const name of ["scout", "reviewer"]) {
891
+ fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
892
+ }
893
+
894
+ const { params, notifications } = await captureSlashCommandParams("chain", 'scout "scan" -> (reviewer "A")', root);
895
+ assert.equal(params, undefined);
896
+ assert.equal(notifications.length, 1);
897
+ assert.match(notifications[0] ?? "", /at least two/i);
898
+ });
899
+ });
900
+
901
+ it("/chain carries inline metadata and group options through to params", async () => {
902
+ await withTempProject("pi-chain-group-meta-", async (root) => {
903
+ for (const name of ["scout", "reviewer", "writer"]) {
904
+ fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
905
+ }
906
+
907
+ const { params, notifications } = await captureSlashCommandParams(
908
+ "chain",
909
+ 'scout[as=ctx,phase=recon] "scan" -> (reviewer "A" | writer "B")[concurrency=2,failFast]',
910
+ root,
911
+ );
912
+ assert.deepEqual(notifications, []);
913
+ const built = params as { chain?: Array<Record<string, unknown>> };
914
+ assert.equal(built.chain?.[0]?.as, "ctx");
915
+ assert.equal(built.chain?.[0]?.phase, "recon");
916
+ const group = built.chain?.[1] as Record<string, unknown>;
917
+ assert.equal((group.parallel as unknown[]).length, 2);
918
+ assert.equal(group.concurrency, 2);
919
+ assert.equal(group.failFast, true);
920
+ });
921
+ });
922
+
923
+ it("/chain tab-completion works inside parallel groups", async () => {
924
+ await withTempProject("pi-chain-group-complete-", async (root) => {
925
+ for (const name of ["scout", "reviewer", "writer"]) {
926
+ fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
927
+ }
928
+ await withIsolatedHome(async () => {
929
+ const commands = new Map<string, RegisteredSlashCommand>();
930
+ const pi = {
931
+ events: createEventBus(),
932
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
933
+ registerShortcut() {},
934
+ sendMessage(_message: unknown) {},
935
+ };
936
+ registerSlashCommands!(pi, createState(root));
937
+ const complete = (prefix: string) =>
938
+ (commands.get("chain")!.getArgumentCompletions!(prefix) as Array<{ value: string }> | null)?.map((c) => c.value) ?? null;
939
+
940
+ // after `(`
941
+ assert.deepEqual(complete('scout "scan" -> (rev'), ['scout "scan" -> (reviewer']);
942
+ // after `|`
943
+ assert.deepEqual(complete('scout "scan" -> (reviewer "A" | wr'), ['scout "scan" -> (reviewer "A" | writer']);
944
+ // after a bare `|` a space is inserted before every suggested agent
945
+ const barePipe = complete('scout "scan" -> (reviewer "A" |');
946
+ assert.ok(barePipe && barePipe.length > 0);
947
+ assert.ok(barePipe.every((v) => v.startsWith('scout "scan" -> (reviewer "A" | ')));
948
+ assert.ok(barePipe.includes('scout "scan" -> (reviewer "A" | writer'));
949
+ // inside an open quote: no agent completion
950
+ assert.equal(complete('scout "scan'), null);
951
+ });
952
+ });
953
+ });
954
+ });
955
+
956
+
957
+ describe("subagents-models slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
958
+ beforeEach(() => {
959
+ clearSlashSnapshots?.();
960
+ });
961
+
962
+ it("routes to the models tool action", async () => {
963
+ const { params } = await captureSlashCommandParams("subagents-models", "", process.cwd());
964
+ assert.deepEqual(params, { action: "models" });
965
+ });
966
+
967
+ it("passes an optional builtin filter", async () => {
968
+ const { params } = await captureSlashCommandParams("subagents-models", "scout", process.cwd());
969
+ assert.deepEqual(params, { action: "models", agent: "scout" });
970
+ });
971
+
972
+ it("rejects invalid builtin filters without launching", async () => {
973
+ const { params, notifications } = await captureSlashCommandParams("subagents-models", "not-a-builtin", process.cwd());
974
+ assert.equal(params, undefined);
975
+ assert.deepEqual(notifications, ["Unknown builtin agent: not-a-builtin"]);
976
+ });
977
+
978
+ it("suggests builtin agent names", async () => {
979
+ await withIsolatedHome(async () => {
980
+ const commands = new Map<string, RegisteredSlashCommand>();
981
+ const pi = {
982
+ events: createEventBus(),
983
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
984
+ commands.set(name, spec);
985
+ },
986
+ registerShortcut() {},
987
+ sendMessage(_message: unknown) {},
988
+ };
989
+
990
+ registerSlashCommands!(pi, createState(process.cwd()));
991
+ const completions = commands.get("subagents-models")!.getArgumentCompletions!("sc") as Array<{ value: string; label: string }>;
992
+ assert.deepEqual(completions.map((completion) => completion.value), ["scout"]);
993
+ });
994
+ });
995
+ });
996
+
997
+ describe("subagent cost slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
998
+ it("reports parent and child usage from the current session branch", async () => {
999
+ const sent: unknown[] = [];
1000
+ const commands = new Map<string, RegisteredSlashCommand>();
1001
+ const pi = {
1002
+ events: createEventBus(),
1003
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
1004
+ commands.set(name, spec);
1005
+ },
1006
+ registerShortcut() {},
1007
+ sendMessage(message: unknown) { sent.push(message); },
1008
+ };
1009
+ const parentUsage = {
1010
+ input: 100,
1011
+ output: 50,
1012
+ cacheRead: 10,
1013
+ cacheWrite: 5,
1014
+ cost: { input: 0.001, output: 0.002, cacheRead: 0, cacheWrite: 0, total: 0.003 },
1015
+ };
1016
+ const childUsage = { input: 20, output: 10, cacheRead: 2, cacheWrite: 1, cost: 0.004, turns: 1 };
1017
+ const slashChildUsage = { input: 30, output: 15, cacheRead: 0, cacheWrite: 0, cost: 0.005, turns: 2 };
1018
+ registerSlashCommands!(pi, createState(process.cwd()));
1019
+ await commands.get("subagent-cost")!.handler("", createCommandContext({
1020
+ sessionManager: {
1021
+ getBranch: () => [
1022
+ { type: "message", message: { role: "assistant", usage: parentUsage } },
1023
+ {
1024
+ type: "message",
1025
+ message: {
1026
+ role: "toolResult",
1027
+ toolName: "subagent",
1028
+ details: {
1029
+ mode: "single",
1030
+ results: [{ agent: "worker", task: "fix", exitCode: 0, messages: [], usage: childUsage, sessionFile: "/tmp/worker.jsonl" }],
1031
+ },
1032
+ },
1033
+ },
1034
+ {
1035
+ type: "custom_message",
1036
+ customType: SLASH_RESULT_TYPE,
1037
+ details: {
1038
+ requestId: "slash-1",
1039
+ result: {
1040
+ content: [{ type: "text", text: "done" }],
1041
+ details: {
1042
+ mode: "single",
1043
+ results: [{ agent: "reviewer", task: "review", exitCode: 0, messages: [], usage: slashChildUsage }],
1044
+ },
1045
+ },
1046
+ },
1047
+ },
1048
+ ],
1049
+ },
1050
+ }));
1051
+
1052
+ const output = String((sent[0] as { content?: unknown }).content ?? "");
1053
+ assert.match(output, /Parent: ↑100 ↓50 \$0\.0030/);
1054
+ assert.match(output, /Child 1 \(worker\): ↑20 ↓10 \$0\.0040/);
1055
+ assert.match(output, /Session: \/tmp\/worker\.jsonl/);
1056
+ assert.match(output, /Child 2 \(reviewer\): ↑30 ↓15 \$0\.0050/);
1057
+ assert.match(output, /Children: ↑50 ↓25 \$0\.0090/);
1058
+ assert.match(output, /Total: ↑150 ↓75 \$0\.0120/);
1059
+ });
1060
+ });
1061
+
1062
+ describe("subagent profiles slash commands", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
1063
+ it("lists saved profiles", async () => {
1064
+ await withIsolatedHome(async () => {
1065
+ const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
1066
+ fs.mkdirSync(profilesDir, { recursive: true });
1067
+ fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({ subagents: { agentOverrides: {} } }));
1068
+ fs.writeFileSync(path.join(profilesDir, "openai-codex.quality.json"), JSON.stringify({ subagents: { agentOverrides: {} } }));
1069
+ const sent: unknown[] = [];
1070
+ const commands = new Map<string, RegisteredSlashCommand>();
1071
+ const pi = {
1072
+ events: createEventBus(),
1073
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
1074
+ commands.set(name, spec);
1075
+ },
1076
+ registerShortcut() {},
1077
+ sendMessage(message: unknown) { sent.push(message); },
1078
+ };
1079
+ registerSlashCommands!(pi, createState(process.cwd()));
1080
+ await commands.get("subagents-profiles")!.handler("", createCommandContext());
1081
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /openai-codex\.quota/);
1082
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /openai-codex\.quality/);
1083
+ });
1084
+ });
1085
+
1086
+ it("loads a saved profile into user settings", async () => {
1087
+ await withIsolatedHome(async () => {
1088
+ const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
1089
+ fs.mkdirSync(profilesDir, { recursive: true });
1090
+ fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({
1091
+ subagents: { agentOverrides: {
1092
+ scout: { model: "openai-codex/gpt-5.3-codex-spark" },
1093
+ worker: { model: "openai-codex/gpt-5.4" },
1094
+ } },
1095
+ }, null, 2));
1096
+ const sent: unknown[] = [];
1097
+ const commands = new Map<string, RegisteredSlashCommand>();
1098
+ const pi = {
1099
+ events: createEventBus(),
1100
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1101
+ registerShortcut() {},
1102
+ sendMessage(message: unknown) { sent.push(message); },
1103
+ };
1104
+ registerSlashCommands!(pi, createState(process.cwd()));
1105
+ await commands.get("subagents-load-profile")!.handler("openai-codex.quota", createCommandContext());
1106
+ const settings = JSON.parse(fs.readFileSync(path.join(process.env.HOME!, ".selesai", "agent", "settings.json"), "utf-8"));
1107
+ assert.equal(settings.subagents.agentOverrides.scout.model, "openai-codex/gpt-5.3-codex-spark");
1108
+ assert.equal(settings.subagents.agentOverrides.worker.model, "openai-codex/gpt-5.4");
1109
+ assert.doesNotMatch(String((sent[0] as { content?: unknown }).content ?? ""), /run \/reload/);
1110
+ });
1111
+ });
1112
+
1113
+ it("can switch the current session model to the loaded profile worker model", async () => {
1114
+ await withIsolatedHome(async () => {
1115
+ const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
1116
+ fs.mkdirSync(profilesDir, { recursive: true });
1117
+ fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({
1118
+ subagents: { agentOverrides: { worker: { model: "gpt-5.4:high" } } },
1119
+ }, null, 2));
1120
+ const sent: unknown[] = [];
1121
+ const commands = new Map<string, RegisteredSlashCommand>();
1122
+ let setModelArg: unknown;
1123
+ const resolvedModel = { provider: "openai-codex", id: "gpt-5.4" };
1124
+ const pi = {
1125
+ events: createEventBus(),
1126
+ async setModel(model: unknown) { setModelArg = model; return true; },
1127
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1128
+ registerShortcut() {},
1129
+ sendMessage(message: unknown) { sent.push(message); },
1130
+ };
1131
+ registerSlashCommands!(pi as never, createState(process.cwd()));
1132
+ await commands.get("subagents-load-profile")!.handler("openai-codex.quota", createCommandContext({
1133
+ confirm: async () => true,
1134
+ modelRegistry: {
1135
+ getAvailable: () => [{ provider: "openai-codex", id: "gpt-5.4" }],
1136
+ find: (provider, id) => provider === "openai-codex" && id === "gpt-5.4" ? resolvedModel : undefined,
1137
+ },
1138
+ }) as never);
1139
+ assert.equal(setModelArg, resolvedModel);
1140
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Current session model switched to: openai-codex\/gpt-5.4/);
1141
+ });
1142
+ });
1143
+
1144
+ it("refreshes a provider model catalog", async () => {
1145
+ await withIsolatedHome(async () => {
1146
+ const sent: unknown[] = [];
1147
+ const commands = new Map<string, RegisteredSlashCommand>();
1148
+ const pi = {
1149
+ events: createEventBus(),
1150
+ exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
1151
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1152
+ registerShortcut() {},
1153
+ sendMessage(message: unknown) { sent.push(message); },
1154
+ };
1155
+ registerSlashCommands!(pi, createState(process.cwd()));
1156
+ await commands.get("subagents-refresh-provider-models")!.handler("openai-codex", createCommandContext({
1157
+ cwd: process.cwd(),
1158
+ modelRegistry: {
1159
+ getAvailable: () => [
1160
+ { provider: "openai-codex", id: "gpt-5.3-codex-spark", reasoning: true },
1161
+ { provider: "openai-codex", id: "gpt-5.4-mini", reasoning: true },
1162
+ ],
1163
+ },
1164
+ }) as never);
1165
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Provider: openai-codex/);
1166
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Warning: 2 models were classified with name heuristics fallback\./);
1167
+ assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "providers", "openai-codex.models.json")), true);
1168
+ });
1169
+ });
1170
+
1171
+ it("generates provider profiles", async () => {
1172
+ await withIsolatedHome(async () => {
1173
+ const sent: unknown[] = [];
1174
+ const commands = new Map<string, RegisteredSlashCommand>();
1175
+ const pi = {
1176
+ events: createEventBus(),
1177
+ exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
1178
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1179
+ registerShortcut() {},
1180
+ sendMessage(message: unknown) { sent.push(message); },
1181
+ };
1182
+ registerSlashCommands!(pi, createState(process.cwd()));
1183
+ await commands.get("subagents-generate-profiles")!.handler("openai-codex", createCommandContext({
1184
+ modelRegistry: {
1185
+ getAvailable: () => [
1186
+ { provider: "openai-codex", id: "gpt-5.3-codex-spark", reasoning: true },
1187
+ { provider: "openai-codex", id: "gpt-5.4-mini", reasoning: true },
1188
+ { provider: "openai-codex", id: "gpt-5.4", reasoning: true },
1189
+ { provider: "openai-codex", id: "gpt-5.5", reasoning: true },
1190
+ ],
1191
+ },
1192
+ }) as never);
1193
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Generated subagent profiles/);
1194
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Warning: generated profiles depend on heuristic-only classification for 4 selected models\./);
1195
+ assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "openai-codex.quota.json")), true);
1196
+ assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "openai-codex.quality.json")), true);
1197
+ });
1198
+ });
1199
+
1200
+ it("checks a profile", async () => {
1201
+ await withIsolatedHome(async () => {
1202
+ const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
1203
+ fs.mkdirSync(profilesDir, { recursive: true });
1204
+ fs.writeFileSync(path.join(profilesDir, "demo.json"), JSON.stringify({
1205
+ subagents: { agentOverrides: { scout: { model: "openai-codex/gpt-5.3-codex-spark" } } },
1206
+ }, null, 2));
1207
+ const sent: unknown[] = [];
1208
+ const commands = new Map<string, RegisteredSlashCommand>();
1209
+ const pi = {
1210
+ events: createEventBus(),
1211
+ exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
1212
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1213
+ registerShortcut() {},
1214
+ sendMessage(message: unknown) { sent.push(message); },
1215
+ };
1216
+ registerSlashCommands!(pi, createState(process.cwd()));
1217
+ await commands.get("subagents-check-profile")!.handler("demo", createCommandContext({
1218
+ modelRegistry: { getAvailable: () => [{ provider: "openai-codex", id: "gpt-5.3-codex-spark" }] },
1219
+ }) as never);
1220
+ assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /probe ok/);
1221
+ });
1222
+ });
1223
+
1224
+ it("suggests provider names for refresh and generate commands", async () => {
1225
+ await withIsolatedHome(async () => {
1226
+ const commands = new Map<string, RegisteredSlashCommand>();
1227
+ const pi = {
1228
+ events: createEventBus(),
1229
+ registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
1230
+ registerShortcut() {},
1231
+ sendMessage(_message: unknown) {},
1232
+ };
1233
+ const state = createState(process.cwd());
1234
+ state.lastUiContext = createCommandContext({
1235
+ modelRegistry: {
1236
+ getAvailable: () => [
1237
+ { provider: "openai-codex", id: "gpt-5.4" },
1238
+ { provider: "openai", id: "gpt-5" },
1239
+ { provider: "anthropic", id: "claude-sonnet-4" },
1240
+ ],
1241
+ },
1242
+ }) as never;
1243
+ registerSlashCommands!(pi, state);
1244
+ const refresh = commands.get("subagents-refresh-provider-models")!.getArgumentCompletions!("open") as Array<{ value: string; label: string }>;
1245
+ const generate = commands.get("subagents-generate-profiles")!.getArgumentCompletions!("an") as Array<{ value: string; label: string }>;
1246
+ assert.deepEqual(refresh.map((entry) => entry.value), ["openai", "openai-codex"]);
1247
+ assert.deepEqual(generate.map((entry) => entry.value), ["anthropic"]);
1248
+ });
1249
+ });
1250
+ });
1251
+
1252
+ describe("subagents-doctor slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
1253
+ beforeEach(() => {
1254
+ clearSlashSnapshots?.();
1255
+ });
1256
+
1257
+ it("routes to the doctor tool action", async () => {
1258
+ const { params } = await captureSlashCommandParams("subagents-doctor", "", process.cwd());
1259
+ assert.deepEqual(params, { action: "doctor" });
1260
+ });
1261
+
1262
+ it("routes fleet to the read-only status view", async () => {
1263
+ const { params } = await captureSlashCommandParams("subagents-fleet", "", process.cwd());
1264
+ assert.deepEqual(params, { action: "status", view: "fleet" });
1265
+ });
1266
+
1267
+ it("does not register the removed subagents-status overlay command", async () => {
1268
+ await withIsolatedHome(async () => {
1269
+ const commands = new Map<string, RegisteredSlashCommand>();
1270
+ const pi = {
1271
+ events: createEventBus(),
1272
+ registerCommand(name: string, spec: RegisteredSlashCommand) {
1273
+ commands.set(name, spec);
1274
+ },
1275
+ registerShortcut() {},
1276
+ sendMessage(_message: unknown) {},
1277
+ };
1278
+
1279
+ registerSlashCommands!(pi, createState(process.cwd()));
1280
+ assert.equal(commands.has("subagents-status"), false);
1281
+ });
1282
+ });
1283
+
1284
+ });