@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,579 @@
1
+ import assert from "node:assert/strict";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { afterEach, beforeEach, describe, it } from "node:test";
6
+ import { buildBuiltinOverrideConfig, discoverAgents, discoverAgentsAll, removeBuiltinAgentOverride } from "../../src/agents/agents.ts";
7
+
8
+ let tempHome = "";
9
+ let tempProject = "";
10
+ const originalHome = process.env.HOME;
11
+ const originalUserProfile = process.env.USERPROFILE;
12
+ const originalPiCodingAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
13
+ const originalExtraAgentDirs = process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
14
+
15
+ function writeJson(filePath: string, value: unknown): void {
16
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
17
+ fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
18
+ }
19
+
20
+ function writeProjectAgent(cwd: string, name: string, body: string): void {
21
+ const filePath = path.join(cwd, ".selesai", "agents", `${name}.md`);
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, body, "utf-8");
24
+ }
25
+
26
+ function writeUserAgent(home: string, name: string, body: string): void {
27
+ const filePath = path.join(home, ".selesai", "agent", "agents", `${name}.md`);
28
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
29
+ fs.writeFileSync(filePath, body, "utf-8");
30
+ }
31
+
32
+ describe("builtin agent overrides", () => {
33
+ beforeEach(() => {
34
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-home-"));
35
+ tempProject = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-project-"));
36
+ process.env.HOME = tempHome;
37
+ process.env.USERPROFILE = tempHome;
38
+ delete process.env.SELESAI_CODING_AGENT_DIR;
39
+ delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
40
+ });
41
+
42
+ afterEach(() => {
43
+ if (originalHome === undefined) delete process.env.HOME;
44
+ else process.env.HOME = originalHome;
45
+ if (originalUserProfile === undefined) delete process.env.USERPROFILE;
46
+ else process.env.USERPROFILE = originalUserProfile;
47
+ if (originalPiCodingAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
48
+ else process.env.SELESAI_CODING_AGENT_DIR = originalPiCodingAgentDir;
49
+ if (originalExtraAgentDirs === undefined) delete process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS;
50
+ else process.env.SELESAI_SUBAGENT_EXTRA_AGENT_DIRS = originalExtraAgentDirs;
51
+ fs.rmSync(tempHome, { recursive: true, force: true });
52
+ fs.rmSync(tempProject, { recursive: true, force: true });
53
+ });
54
+
55
+ it("bundled builtin agents inherit the default model", () => {
56
+ const builtins = discoverAgentsAll(tempProject).builtin;
57
+ assert.ok(builtins.length > 0);
58
+ assert.deepEqual(
59
+ builtins
60
+ .filter((agent) => agent.model !== undefined || agent.fallbackModels !== undefined)
61
+ .map((agent) => agent.name),
62
+ [],
63
+ );
64
+ });
65
+
66
+ it("applies subagents.defaultModel to builtin agents with explicit overrides winning", () => {
67
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
68
+ subagents: {
69
+ defaultModel: "deepseek-v4-flash",
70
+ agentOverrides: {
71
+ oracle: { model: "deepseek-v4-pro" },
72
+ reviewer: { model: false },
73
+ },
74
+ },
75
+ });
76
+
77
+ const builtins = discoverAgentsAll(tempProject).builtin;
78
+ const scout = builtins.find((agent) => agent.name === "scout");
79
+ assert.equal(scout?.model, "deepseek-v4-flash");
80
+ assert.equal(scout?.modelSource?.type, "subagents.defaultModel");
81
+ assert.equal(scout?.modelSource?.scope, "user");
82
+ assert.equal(builtins.find((agent) => agent.name === "worker")?.model, "deepseek-v4-flash");
83
+ assert.equal(builtins.find((agent) => agent.name === "oracle")?.model, "deepseek-v4-pro");
84
+ assert.equal(builtins.find((agent) => agent.name === "reviewer")?.model, undefined);
85
+ });
86
+
87
+ it("prefers project subagents.defaultModel over user defaultModel", () => {
88
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
89
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
90
+ subagents: { defaultModel: "deepseek-v4-flash" },
91
+ });
92
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
93
+ subagents: { defaultModel: "deepseek-v4-pro" },
94
+ });
95
+
96
+ const worker = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "worker");
97
+ assert.ok(worker);
98
+ assert.equal(worker.model, "deepseek-v4-pro");
99
+ });
100
+
101
+ it("applies subagents.defaultModel to custom agents without a frontmatter model", () => {
102
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
103
+ subagents: {
104
+ defaultModel: "deepseek-v4-flash",
105
+ agentOverrides: {
106
+ implementer: { model: "deepseek-v4-pro" },
107
+ },
108
+ },
109
+ });
110
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
111
+ writeProjectAgent(tempProject, "auditor", `---\nname: auditor\ndescription: Audit code\nmodel: google/gemini-3-pro\n---\n\nAudit the code.\n`);
112
+ writeProjectAgent(tempProject, "scout-copy", `---\nname: scout-copy\ndescription: Scout code\n---\n\nScout the code.\n`);
113
+
114
+ const agents = discoverAgents(tempProject, "both").agents;
115
+ assert.equal(agents.find((agent) => agent.name === "implementer")?.model, "deepseek-v4-pro");
116
+ assert.equal(agents.find((agent) => agent.name === "auditor")?.model, "google/gemini-3-pro");
117
+ assert.equal(agents.find((agent) => agent.name === "scout-copy")?.model, "deepseek-v4-flash");
118
+ });
119
+
120
+ it("applies user settings overrides to builtin agents", () => {
121
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
122
+ subagents: {
123
+ agentOverrides: {
124
+ reviewer: {
125
+ model: "openai/gpt-5.4",
126
+ thinking: "xhigh",
127
+ systemPromptMode: "replace",
128
+ inheritProjectContext: true,
129
+ inheritSkills: true,
130
+ subagentOnlyExtensions: ["./tools/child-review.ts"],
131
+ completionGuard: false,
132
+ },
133
+ },
134
+ },
135
+ });
136
+
137
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
138
+ assert.ok(reviewer);
139
+ assert.equal(reviewer.source, "builtin");
140
+ assert.equal(reviewer.model, "openai/gpt-5.4");
141
+ assert.equal(reviewer.thinking, "xhigh");
142
+ assert.equal(reviewer.systemPromptMode, "replace");
143
+ assert.equal(reviewer.inheritProjectContext, true);
144
+ assert.equal(reviewer.inheritSkills, true);
145
+ assert.deepEqual(reviewer.subagentOnlyExtensions, ["./tools/child-review.ts"]);
146
+ assert.equal(reviewer.completionGuard, false);
147
+ assert.equal(reviewer.override?.scope, "user");
148
+ assert.equal(reviewer.override?.path, path.join(tempHome, ".selesai", "agent", "settings.json"));
149
+ });
150
+
151
+ it("globally disables builtin thinking suffix defaults from user settings", () => {
152
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
153
+ subagents: {
154
+ disableThinking: true,
155
+ },
156
+ });
157
+
158
+ const builtins = discoverAgentsAll(tempProject).builtin;
159
+ assert.ok(builtins.some((agent) => agent.name === "reviewer"));
160
+ assert.deepEqual(
161
+ builtins
162
+ .filter((agent) => agent.thinking !== undefined)
163
+ .map((agent) => agent.name),
164
+ [],
165
+ );
166
+ assert.equal(
167
+ builtins.find((agent) => agent.name === "reviewer")?.override?.path,
168
+ path.join(tempHome, ".selesai", "agent", "settings.json"),
169
+ );
170
+ });
171
+
172
+ it("lets an explicit same-scope thinking override opt back in when global thinking is disabled", () => {
173
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
174
+ subagents: {
175
+ disableThinking: true,
176
+ agentOverrides: {
177
+ reviewer: {
178
+ thinking: "high",
179
+ },
180
+ },
181
+ },
182
+ });
183
+
184
+ const agents = discoverAgents(tempProject, "both").agents;
185
+ const reviewer = agents.find((agent) => agent.name === "reviewer");
186
+ const worker = agents.find((agent) => agent.name === "worker");
187
+ assert.ok(reviewer);
188
+ assert.ok(worker);
189
+ assert.equal(reviewer.thinking, "high");
190
+ assert.equal(worker.thinking, undefined);
191
+ });
192
+
193
+ it("lets project settings disable builtin thinking even when user overrides request it", () => {
194
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
195
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
196
+ subagents: {
197
+ agentOverrides: {
198
+ reviewer: {
199
+ thinking: "xhigh",
200
+ },
201
+ },
202
+ },
203
+ });
204
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
205
+ subagents: {
206
+ disableThinking: true,
207
+ },
208
+ });
209
+
210
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
211
+ assert.ok(reviewer);
212
+ assert.equal(reviewer.thinking, undefined);
213
+ });
214
+
215
+ it("surfaces malformed subagent default model settings", () => {
216
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
217
+ writeJson(settingsPath, {
218
+ subagents: {
219
+ defaultModel: "",
220
+ },
221
+ });
222
+
223
+ assert.throws(
224
+ () => discoverAgents(tempProject, "both"),
225
+ (error: unknown) => error instanceof Error
226
+ && error.message.includes(settingsPath)
227
+ && error.message.includes("defaultModel"),
228
+ );
229
+ });
230
+
231
+ it("surfaces malformed global thinking settings", () => {
232
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
233
+ writeJson(settingsPath, {
234
+ subagents: {
235
+ disableThinking: "yes",
236
+ },
237
+ });
238
+
239
+ assert.throws(
240
+ () => discoverAgents(tempProject, "both"),
241
+ (error: unknown) => error instanceof Error
242
+ && error.message.includes(settingsPath)
243
+ && error.message.includes("disableThinking"),
244
+ );
245
+ });
246
+
247
+ it("prefers project settings overrides over user settings overrides", () => {
248
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
249
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
250
+ subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
251
+ });
252
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
253
+ subagents: { agentOverrides: { reviewer: { model: "openai-codex/gpt-5.4-mini", thinking: "high" } } },
254
+ });
255
+
256
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
257
+ assert.ok(reviewer);
258
+ assert.equal(reviewer.model, "openai-codex/gpt-5.4-mini");
259
+ assert.equal(reviewer.thinking, "high");
260
+ assert.equal(reviewer.override?.scope, "project");
261
+ assert.equal(reviewer.override?.path, path.join(tempProject, ".selesai", "settings.json"));
262
+ });
263
+
264
+ it("does not apply project settings overrides when scope is user", () => {
265
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
266
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
267
+ subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
268
+ });
269
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
270
+ subagents: { agentOverrides: { reviewer: { model: "openai-codex/gpt-5.4-mini" } } },
271
+ });
272
+
273
+ const reviewer = discoverAgents(tempProject, "user").agents.find((agent) => agent.name === "reviewer");
274
+ assert.ok(reviewer);
275
+ assert.equal(reviewer.model, "openai/gpt-5.4");
276
+ assert.equal(reviewer.override?.scope, "user");
277
+ });
278
+
279
+ it("does not apply user settings overrides when scope is project", () => {
280
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
281
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
282
+ subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
283
+ });
284
+
285
+ const reviewer = discoverAgents(tempProject, "project").agents.find((agent) => agent.name === "reviewer");
286
+ assert.ok(reviewer);
287
+ assert.notEqual(reviewer.model, "openai/gpt-5.4");
288
+ assert.equal(reviewer.override, undefined);
289
+ });
290
+
291
+ it("does not read malformed out-of-scope settings files", () => {
292
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
293
+ fs.mkdirSync(path.join(tempHome, ".selesai", "agent"), { recursive: true });
294
+ fs.writeFileSync(path.join(tempHome, ".selesai", "agent", "settings.json"), '{"subagents":', "utf-8");
295
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
296
+ subagents: { agentOverrides: { reviewer: { model: "openai-codex/gpt-5.4-mini" } } },
297
+ });
298
+
299
+ const reviewer = discoverAgents(tempProject, "project").agents.find((agent) => agent.name === "reviewer");
300
+ assert.ok(reviewer);
301
+ assert.equal(reviewer.model, "openai-codex/gpt-5.4-mini");
302
+ assert.equal(reviewer.override?.scope, "project");
303
+ });
304
+
305
+ it("frontmatter wins per-field over agentOverrides for a shadowing project agent", () => {
306
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
307
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
308
+ subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
309
+ });
310
+ writeProjectAgent(tempProject, "reviewer", `---\nname: reviewer\ndescription: Project reviewer\nmodel: google/gemini-3-pro\n---\n\nUse the project reviewer.\n`);
311
+
312
+ const reviewer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "reviewer");
313
+ assert.ok(reviewer);
314
+ assert.equal(reviewer.source, "project");
315
+ assert.equal(reviewer.model, "google/gemini-3-pro");
316
+ assert.equal(reviewer.override, undefined);
317
+ });
318
+
319
+ it("fills in unset fields on a custom project agent from project agentOverrides", () => {
320
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
321
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
322
+ subagents: {
323
+ agentOverrides: {
324
+ implementer: {
325
+ model: "anthropic/claude-sonnet-4-6",
326
+ fallbackModels: ["openai/gpt-5-mini"],
327
+ thinking: "high",
328
+ systemPromptMode: "append",
329
+ inheritProjectContext: true,
330
+ inheritSkills: true,
331
+ defaultContext: "fork",
332
+ tools: ["bash", "mcp:xcodebuild_list_sims"],
333
+ skills: ["tdd"],
334
+ subagentOnlyExtensions: ["./tools/child-review.ts"],
335
+ completionGuard: false,
336
+ },
337
+ },
338
+ },
339
+ });
340
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
341
+
342
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
343
+ assert.ok(implementer);
344
+ assert.equal(implementer.source, "project");
345
+ assert.equal(implementer.model, "anthropic/claude-sonnet-4-6");
346
+ assert.deepEqual(implementer.fallbackModels, ["openai/gpt-5-mini"]);
347
+ assert.equal(implementer.thinking, "high");
348
+ assert.equal(implementer.systemPromptMode, "append");
349
+ assert.equal(implementer.inheritProjectContext, true);
350
+ assert.equal(implementer.inheritSkills, true);
351
+ assert.equal(implementer.defaultContext, "fork");
352
+ assert.deepEqual(implementer.tools, ["bash"]);
353
+ assert.deepEqual(implementer.mcpDirectTools, ["xcodebuild_list_sims"]);
354
+ assert.deepEqual(implementer.skills, ["tdd"]);
355
+ assert.deepEqual(implementer.subagentOnlyExtensions, ["./tools/child-review.ts"]);
356
+ assert.equal(implementer.completionGuard, false);
357
+ assert.equal(implementer.override?.scope, "project");
358
+ assert.equal(implementer.override?.path, path.join(tempProject, ".selesai", "settings.json"));
359
+ });
360
+
361
+ it("fills in unset fields on a custom user agent from user agentOverrides", () => {
362
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
363
+ subagents: { agentOverrides: { implementer: { model: "anthropic/claude-sonnet-4-6" } } },
364
+ });
365
+ writeUserAgent(tempHome, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
366
+
367
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
368
+ assert.ok(implementer);
369
+ assert.equal(implementer.source, "user");
370
+ assert.equal(implementer.model, "anthropic/claude-sonnet-4-6");
371
+ assert.equal(implementer.override?.scope, "user");
372
+ });
373
+
374
+ it("applies user agentOverrides to a custom project agent when project settings have no entry", () => {
375
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
376
+ subagents: { agentOverrides: { implementer: { model: "anthropic/claude-sonnet-4-6" } } },
377
+ });
378
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
379
+
380
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
381
+ assert.ok(implementer);
382
+ assert.equal(implementer.source, "project");
383
+ assert.equal(implementer.model, "anthropic/claude-sonnet-4-6");
384
+ assert.equal(implementer.override?.scope, "user");
385
+ });
386
+
387
+ it("prefers project agentOverrides over user agentOverrides on a custom project agent", () => {
388
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
389
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
390
+ subagents: { agentOverrides: { implementer: { model: "anthropic/claude-sonnet-4-6" } } },
391
+ });
392
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
393
+ subagents: { agentOverrides: { implementer: { model: "openai/gpt-5.4" } } },
394
+ });
395
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
396
+
397
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
398
+ assert.ok(implementer);
399
+ assert.equal(implementer.model, "openai/gpt-5.4");
400
+ assert.equal(implementer.override?.scope, "project");
401
+ });
402
+
403
+ it("keeps explicit custom frontmatter fields over matching agentOverrides", () => {
404
+ fs.mkdirSync(path.join(tempProject, ".selesai"), { recursive: true });
405
+ writeJson(path.join(tempProject, ".selesai", "settings.json"), {
406
+ subagents: {
407
+ agentOverrides: {
408
+ implementer: {
409
+ model: "anthropic/claude-sonnet-4-6",
410
+ thinking: "high",
411
+ tools: ["bash"],
412
+ skills: ["override-skill"],
413
+ inheritProjectContext: true,
414
+ defaultContext: "fork",
415
+ completionGuard: true,
416
+ },
417
+ },
418
+ },
419
+ });
420
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\nmodel: google/gemini-3-pro\nthinking: medium\ntools: read, mcp:local_tool\nskills: agent-skill\ninheritProjectContext: false\ndefaultContext: fresh\ncompletionGuard: false\n---\n\nDrive the failing test first.\n`);
421
+
422
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
423
+ assert.ok(implementer);
424
+ assert.equal(implementer.model, "google/gemini-3-pro");
425
+ assert.equal(implementer.thinking, "medium");
426
+ assert.deepEqual(implementer.tools, ["read"]);
427
+ assert.deepEqual(implementer.mcpDirectTools, ["local_tool"]);
428
+ assert.deepEqual(implementer.skills, ["agent-skill"]);
429
+ assert.equal(implementer.inheritProjectContext, false);
430
+ assert.equal(implementer.defaultContext, "fresh");
431
+ assert.equal(implementer.completionGuard, false);
432
+ assert.equal(implementer.override, undefined);
433
+ });
434
+
435
+ it("leaves a custom agent untouched when no agentOverrides entry matches its name", () => {
436
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
437
+ subagents: { agentOverrides: { reviewer: { model: "openai/gpt-5.4" } } },
438
+ });
439
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
440
+
441
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
442
+ assert.ok(implementer);
443
+ assert.equal(implementer.model, undefined);
444
+ assert.equal(implementer.override, undefined);
445
+ });
446
+
447
+ it("disableBuiltins does not disable custom agents", () => {
448
+ writeJson(path.join(tempHome, ".selesai", "agent", "settings.json"), {
449
+ subagents: { disableBuiltins: true },
450
+ });
451
+ writeProjectAgent(tempProject, "implementer", `---\nname: implementer\ndescription: TDD implementer\n---\n\nDrive the failing test first.\n`);
452
+
453
+ const implementer = discoverAgents(tempProject, "both").agents.find((agent) => agent.name === "implementer");
454
+ assert.ok(implementer);
455
+ assert.notEqual(implementer.disabled, true);
456
+ });
457
+
458
+ it("does not create a settings file when removing a non-existent override", () => {
459
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
460
+ assert.equal(fs.existsSync(settingsPath), false);
461
+ removeBuiltinAgentOverride(tempProject, "reviewer", "user");
462
+ assert.equal(fs.existsSync(settingsPath), false);
463
+ });
464
+
465
+ it("surfaces malformed settings files instead of silently ignoring them", () => {
466
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
467
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
468
+ fs.writeFileSync(settingsPath, '{"subagents":', "utf-8");
469
+
470
+ assert.throws(
471
+ () => discoverAgents(tempProject, "both"),
472
+ (error: unknown) => error instanceof Error
473
+ && error.message.includes(settingsPath)
474
+ && error.message.includes("Failed to parse settings file"),
475
+ );
476
+ });
477
+
478
+ it("surfaces settings read failures without mislabeling them as parse errors", () => {
479
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
480
+ fs.mkdirSync(settingsPath, { recursive: true });
481
+
482
+ assert.throws(
483
+ () => discoverAgents(tempProject, "both"),
484
+ (error: unknown) => error instanceof Error
485
+ && error.message.includes(settingsPath)
486
+ && error.message.includes("Failed to read settings file"),
487
+ );
488
+ });
489
+
490
+ it("surfaces malformed builtin override entries instead of silently ignoring them", () => {
491
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
492
+ writeJson(settingsPath, {
493
+ subagents: {
494
+ agentOverrides: {
495
+ reviewer: {
496
+ inheritProjectContext: "true",
497
+ },
498
+ },
499
+ },
500
+ });
501
+
502
+ assert.throws(
503
+ () => discoverAgents(tempProject, "both"),
504
+ (error: unknown) => error instanceof Error
505
+ && error.message.includes(settingsPath)
506
+ && error.message.includes("reviewer")
507
+ && error.message.includes("inheritProjectContext"),
508
+ );
509
+ });
510
+
511
+ it("surfaces malformed completion guard override values", () => {
512
+ const settingsPath = path.join(tempHome, ".selesai", "agent", "settings.json");
513
+ writeJson(settingsPath, {
514
+ subagents: {
515
+ agentOverrides: {
516
+ reviewer: {
517
+ completionGuard: "false",
518
+ },
519
+ },
520
+ },
521
+ });
522
+
523
+ assert.throws(
524
+ () => discoverAgents(tempProject, "both"),
525
+ (error: unknown) => error instanceof Error
526
+ && error.message.includes(settingsPath)
527
+ && error.message.includes("reviewer")
528
+ && error.message.includes("completionGuard"),
529
+ );
530
+ });
531
+
532
+ it("builds false sentinels when an override clears builtin fields", () => {
533
+ const override = buildBuiltinOverrideConfig(
534
+ {
535
+ model: "openai-codex/gpt-5.4-mini",
536
+ fallbackModels: ["openai/gpt-5-mini"],
537
+ thinking: "high",
538
+ systemPromptMode: "append",
539
+ inheritProjectContext: true,
540
+ inheritSkills: false,
541
+ defaultContext: "fork",
542
+ systemPrompt: "Base prompt",
543
+ skills: ["safe-bash"],
544
+ tools: ["bash"],
545
+ mcpDirectTools: ["xcodebuild_list_sims"],
546
+ subagentOnlyExtensions: ["./tools/base-child.ts"],
547
+ completionGuard: false,
548
+ },
549
+ {
550
+ model: undefined,
551
+ fallbackModels: undefined,
552
+ thinking: undefined,
553
+ systemPromptMode: "replace",
554
+ inheritProjectContext: false,
555
+ inheritSkills: false,
556
+ defaultContext: undefined,
557
+ systemPrompt: "Base prompt",
558
+ skills: undefined,
559
+ tools: undefined,
560
+ mcpDirectTools: undefined,
561
+ subagentOnlyExtensions: undefined,
562
+ completionGuard: true,
563
+ },
564
+ );
565
+
566
+ assert.deepEqual(override, {
567
+ model: false,
568
+ fallbackModels: false,
569
+ thinking: false,
570
+ systemPromptMode: "replace",
571
+ inheritProjectContext: false,
572
+ defaultContext: false,
573
+ skills: false,
574
+ tools: false,
575
+ subagentOnlyExtensions: false,
576
+ completionGuard: true,
577
+ });
578
+ });
579
+ });
@@ -0,0 +1,20 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { resolveExecutionAgentScope } from "../../src/agents/agent-scope.ts";
4
+
5
+ describe("resolveExecutionAgentScope", () => {
6
+ it("defaults to both when scope is omitted", () => {
7
+ assert.equal(resolveExecutionAgentScope(undefined), "both");
8
+ });
9
+
10
+ it("passes through explicit scopes", () => {
11
+ assert.equal(resolveExecutionAgentScope("user"), "user");
12
+ assert.equal(resolveExecutionAgentScope("project"), "project");
13
+ assert.equal(resolveExecutionAgentScope("both"), "both");
14
+ });
15
+
16
+ it("falls back to both for invalid scopes", () => {
17
+ assert.equal(resolveExecutionAgentScope("invalid"), "both");
18
+ assert.equal(resolveExecutionAgentScope(""), "both");
19
+ });
20
+ });
@@ -0,0 +1,88 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { mergeAgentsForScope } from "../../src/agents/agent-selection.ts";
4
+ import type { AgentConfig } from "../../src/agents/agents.ts";
5
+
6
+ function makeAgent(name: string, source: "builtin" | "package" | "user" | "project", systemPrompt: string): AgentConfig {
7
+ return {
8
+ name,
9
+ description: `${name} agent`,
10
+ systemPromptMode: "replace",
11
+ inheritProjectContext: false,
12
+ inheritSkills: false,
13
+ systemPrompt,
14
+ source,
15
+ filePath: `/${source}/${name}.md`,
16
+ };
17
+ }
18
+
19
+ describe("mergeAgentsForScope", () => {
20
+ it("returns project agents when scope is project", () => {
21
+ const userAgents = [makeAgent("shared", "user", "user prompt")];
22
+ const projectAgents = [makeAgent("shared", "project", "project prompt")];
23
+ const result = mergeAgentsForScope("project", userAgents, projectAgents);
24
+ assert.equal(result.length, 1);
25
+ assert.equal(result[0]?.source, "project");
26
+ });
27
+
28
+ it("returns user agents when scope is user", () => {
29
+ const userAgents = [makeAgent("shared", "user", "user prompt")];
30
+ const projectAgents = [makeAgent("shared", "project", "project prompt")];
31
+ const result = mergeAgentsForScope("user", userAgents, projectAgents);
32
+ assert.equal(result.length, 1);
33
+ assert.equal(result[0]?.source, "user");
34
+ });
35
+
36
+ it("prefers project agents on name collisions when scope is both", () => {
37
+ const userAgents = [makeAgent("shared", "user", "user prompt")];
38
+ const projectAgents = [makeAgent("shared", "project", "project prompt")];
39
+ const result = mergeAgentsForScope("both", userAgents, projectAgents);
40
+ assert.equal(result.length, 1);
41
+ assert.equal(result[0]?.source, "project");
42
+ assert.equal(result[0]?.systemPrompt, "project prompt");
43
+ });
44
+
45
+ it("keeps agents from both scopes when names are distinct", () => {
46
+ const userAgents = [makeAgent("user-only", "user", "user prompt")];
47
+ const projectAgents = [makeAgent("project-only", "project", "project prompt")];
48
+ const result = mergeAgentsForScope("both", userAgents, projectAgents);
49
+ assert.equal(result.length, 2);
50
+ assert.ok(result.find((a) => a.name === "user-only" && a.source === "user"));
51
+ assert.ok(result.find((a) => a.name === "project-only" && a.source === "project"));
52
+ });
53
+
54
+ it("includes builtin agents when no user or project override exists", () => {
55
+ const builtinAgents = [makeAgent("scout", "builtin", "builtin prompt")];
56
+ const result = mergeAgentsForScope("both", [], [], builtinAgents);
57
+ assert.equal(result.length, 1);
58
+ assert.equal(result[0]?.source, "builtin");
59
+ });
60
+
61
+ it("user agents override builtins with the same name", () => {
62
+ const builtinAgents = [makeAgent("scout", "builtin", "builtin prompt")];
63
+ const userAgents = [makeAgent("scout", "user", "custom prompt")];
64
+ const result = mergeAgentsForScope("both", userAgents, [], builtinAgents);
65
+ assert.equal(result.length, 1);
66
+ assert.equal(result[0]?.source, "user");
67
+ assert.equal(result[0]?.systemPrompt, "custom prompt");
68
+ });
69
+
70
+ it("package agents override builtins but not user or project agents", () => {
71
+ const builtinAgents = [makeAgent("scout", "builtin", "builtin prompt")];
72
+ const packageAgents = [makeAgent("scout", "package", "package prompt")];
73
+ const userAgents = [makeAgent("scout", "user", "user prompt")];
74
+ const projectAgents = [makeAgent("scout", "project", "project prompt")];
75
+
76
+ assert.equal(mergeAgentsForScope("both", [], [], builtinAgents, packageAgents)[0]?.source, "package");
77
+ assert.equal(mergeAgentsForScope("user", userAgents, [], builtinAgents, packageAgents)[0]?.source, "user");
78
+ assert.equal(mergeAgentsForScope("project", [], projectAgents, builtinAgents, packageAgents)[0]?.source, "project");
79
+ });
80
+
81
+ it("project agents override builtins with the same name", () => {
82
+ const builtinAgents = [makeAgent("scout", "builtin", "builtin prompt")];
83
+ const projectAgents = [makeAgent("scout", "project", "project prompt")];
84
+ const result = mergeAgentsForScope("both", [], projectAgents, builtinAgents);
85
+ assert.equal(result.length, 1);
86
+ assert.equal(result[0]?.source, "project");
87
+ });
88
+ });