@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,556 @@
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 { handleCreate, handleManagementAction, handleUpdate } from "../../src/agents/agent-management.ts";
7
+ import { clearSkillCache } from "../../src/agents/skills.ts";
8
+
9
+ let tempDir = "";
10
+ let oldAgentDir: string | undefined;
11
+
12
+ function readText(result: { content: Array<{ type: string; text?: string }> }): string {
13
+ const first = result.content[0];
14
+ assert.ok(first);
15
+ assert.equal(first.type, "text");
16
+ assert.equal(typeof first.text, "string");
17
+ return first.text;
18
+ }
19
+
20
+ describe("agent management config parsing", () => {
21
+ beforeEach(() => {
22
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-management-"));
23
+ oldAgentDir = process.env.SELESAI_CODING_AGENT_DIR;
24
+ process.env.SELESAI_CODING_AGENT_DIR = path.join(tempDir, "agent-home");
25
+ clearSkillCache();
26
+ });
27
+
28
+ afterEach(() => {
29
+ if (oldAgentDir === undefined) delete process.env.SELESAI_CODING_AGENT_DIR;
30
+ else process.env.SELESAI_CODING_AGENT_DIR = oldAgentDir;
31
+ clearSkillCache();
32
+ fs.rmSync(tempDir, { recursive: true, force: true });
33
+ });
34
+
35
+ it("surfaces JSON parse errors for create config strings", () => {
36
+ const result = handleCreate(
37
+ { config: '{"name":' },
38
+ { cwd: tempDir, modelRegistry: { getAvailable: () => [] } },
39
+ );
40
+
41
+ assert.equal(result.isError, true);
42
+ assert.match(readText(result), /config must be valid JSON:/);
43
+ });
44
+
45
+ it("surfaces JSON parse errors for update config strings", () => {
46
+ const result = handleUpdate(
47
+ { agent: "reviewer", config: '{"description":' },
48
+ { cwd: tempDir, modelRegistry: { getAvailable: () => [] } },
49
+ );
50
+
51
+ assert.equal(result.isError, true);
52
+ assert.match(readText(result), /config must be valid JSON:/);
53
+ });
54
+
55
+ it("creates, gets, updates, and deletes a packaged agent by runtime name", () => {
56
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
57
+ const created = handleCreate(
58
+ { config: { name: "Scout", package: "Code Analysis", description: "Fast recon", scope: "project", systemPrompt: "Inspect" } },
59
+ ctx,
60
+ );
61
+
62
+ assert.equal(created.isError, false);
63
+ assert.match(readText(created), /Created agent 'code-analysis.scout'/);
64
+ const filePath = path.join(tempDir, ".selesai", "agents", "code-analysis.scout.md");
65
+ let content = fs.readFileSync(filePath, "utf-8");
66
+ assert.match(content, /^name: scout$/m);
67
+ assert.match(content, /^package: code-analysis$/m);
68
+ assert.doesNotMatch(content, /^name: code-analysis\.scout$/m);
69
+
70
+ const got = handleManagementAction("get", { agent: "code-analysis.scout" }, ctx);
71
+ assert.equal(got.isError, false);
72
+ assert.match(readText(got), /Agent: code-analysis\.scout/);
73
+ assert.match(readText(got), /Local name: scout/);
74
+ assert.match(readText(got), /Package: code-analysis/);
75
+
76
+ const updated = handleUpdate(
77
+ { agent: "code-analysis.scout", config: { package: "documentation" } },
78
+ ctx,
79
+ );
80
+ assert.equal(updated.isError, false);
81
+ assert.match(readText(updated), /code-analysis\.scout' to 'documentation\.scout'/);
82
+ assert.equal(fs.existsSync(filePath), false);
83
+ const updatedPath = path.join(tempDir, ".selesai", "agents", "documentation.scout.md");
84
+ content = fs.readFileSync(updatedPath, "utf-8");
85
+ assert.match(content, /^name: scout$/m);
86
+ assert.match(content, /^package: documentation$/m);
87
+
88
+ const deleted = handleManagementAction("delete", { agent: "documentation.scout" }, ctx);
89
+ assert.equal(deleted.isError, false);
90
+ assert.equal(fs.existsSync(updatedPath), false);
91
+ });
92
+
93
+ it("rejects package values that cannot be normalized", () => {
94
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
95
+ const created = handleCreate(
96
+ { config: { name: "Scout", package: "!!!", description: "Fast recon", scope: "project" } },
97
+ ctx,
98
+ );
99
+
100
+ assert.equal(created.isError, true);
101
+ assert.match(readText(created), /config\.package is invalid/);
102
+ });
103
+
104
+ it("creates and updates packaged chains while preserving packaged step names", () => {
105
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
106
+ fs.mkdirSync(path.join(tempDir, ".selesai", "agents"), { recursive: true });
107
+ fs.writeFileSync(path.join(tempDir, ".selesai", "agents", "code-analysis.scout.md"), `---
108
+ name: scout
109
+ package: code-analysis
110
+ description: Fast recon
111
+ ---
112
+
113
+ Inspect
114
+ `, "utf-8");
115
+
116
+ const created = handleCreate(
117
+ { config: { name: "Review Flow", package: "Code Analysis", description: "Review flow", scope: "project", steps: [{ agent: "code-analysis.scout", task: "Inspect", toolBudget: { soft: 3, hard: 5, block: ["read"] } }] } },
118
+ ctx,
119
+ );
120
+ assert.equal(created.isError, false);
121
+ assert.match(readText(created), /Created chain 'code-analysis.review-flow'/);
122
+ const filePath = path.join(tempDir, ".selesai", "chains", "code-analysis.review-flow.chain.md");
123
+ let content = fs.readFileSync(filePath, "utf-8");
124
+ assert.match(content, /^name: review-flow$/m);
125
+ assert.match(content, /^package: code-analysis$/m);
126
+ assert.match(content, /^## code-analysis\.scout$/m);
127
+ assert.match(content, /^toolBudget: \{"soft":3,"hard":5,"block":\["read"\]\}$/m);
128
+
129
+ const updated = handleUpdate(
130
+ { chainName: "code-analysis.review-flow", config: { package: false } },
131
+ ctx,
132
+ );
133
+ assert.equal(updated.isError, false);
134
+ const updatedPath = path.join(tempDir, ".selesai", "chains", "review-flow.chain.md");
135
+ assert.equal(fs.existsSync(filePath), false);
136
+ content = fs.readFileSync(updatedPath, "utf-8");
137
+ assert.match(content, /^name: review-flow$/m);
138
+ assert.doesNotMatch(content, /^package:/m);
139
+ });
140
+
141
+ it("creates and updates agents with tool budgets", () => {
142
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
143
+ const result = handleCreate(
144
+ { config: { name: "budgeted-reviewer", description: "Review with a budget", scope: "project", toolBudget: { soft: 4, hard: 7, block: ["read", "grep"] } } },
145
+ ctx,
146
+ );
147
+
148
+ assert.equal(result.isError, false);
149
+ const filePath = path.join(tempDir, ".selesai", "agents", "budgeted-reviewer.md");
150
+ let content = fs.readFileSync(filePath, "utf-8");
151
+ assert.match(content, /^toolBudget: \{"soft":4,"hard":7,"block":\["read","grep"\]\}$/m);
152
+
153
+ const got = handleManagementAction("get", { agent: "budgeted-reviewer" }, ctx);
154
+ assert.equal(got.isError, false);
155
+ assert.match(readText(got), /Tool budget: \{"soft":4,"hard":7,"block":\["read","grep"\]\}/);
156
+
157
+ const updated = handleUpdate(
158
+ { agent: "budgeted-reviewer", config: { toolBudget: { hard: 3, block: "*" } } },
159
+ ctx,
160
+ );
161
+ assert.equal(updated.isError, false);
162
+ content = fs.readFileSync(filePath, "utf-8");
163
+ assert.match(content, /^toolBudget: \{"hard":3,"block":"\*"\}$/m);
164
+ });
165
+
166
+ it("rejects invalid tool budget management config", () => {
167
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
168
+ const agentResult = handleCreate(
169
+ { config: { name: "bad-budget", description: "Bad budget", scope: "project", toolBudget: { soft: 5, hard: 4 } } },
170
+ ctx,
171
+ );
172
+ assert.equal(agentResult.isError, true);
173
+ assert.match(readText(agentResult), /config\.toolBudget\.soft must be <= config\.toolBudget\.hard/);
174
+
175
+ const chainResult = handleCreate(
176
+ { config: { name: "bad-chain-budget", description: "Bad budget", scope: "project", steps: [{ agent: "reviewer", toolBudget: { hard: 2, block: [] } }] } },
177
+ ctx,
178
+ );
179
+ assert.equal(chainResult.isError, true);
180
+ assert.match(readText(chainResult), /config\.steps\[0\]\.toolBudget\.block must contain at least one tool name/);
181
+ });
182
+
183
+ it("creates agents with completion guard disabled", () => {
184
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
185
+ const result = handleCreate(
186
+ { config: { name: "test-runner", description: "Run tests", scope: "project", tools: "read, grep, bash, ls", completionGuard: false } },
187
+ ctx,
188
+ );
189
+
190
+ assert.equal(result.isError, false);
191
+ const filePath = path.join(tempDir, ".selesai", "agents", "test-runner.md");
192
+ const content = fs.readFileSync(filePath, "utf-8");
193
+ assert.match(content, /^completionGuard: false$/m);
194
+
195
+ const got = handleManagementAction("get", { agent: "test-runner" }, ctx);
196
+ assert.equal(got.isError, false);
197
+ assert.match(readText(got), /Completion guard: false/);
198
+ });
199
+
200
+ it("rejects non-boolean completion guard config", () => {
201
+ const result = handleCreate(
202
+ { config: { name: "test-runner", description: "Run tests", scope: "project", completionGuard: "false" } },
203
+ { cwd: tempDir, modelRegistry: { getAvailable: () => [] } },
204
+ );
205
+
206
+ assert.equal(result.isError, true);
207
+ assert.match(readText(result), /config\.completionGuard must be a boolean/);
208
+ });
209
+
210
+ it("creates agents with subagent-only extensions", () => {
211
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
212
+ const result = handleCreate(
213
+ { config: { name: "child-tool-user", description: "Uses child tools", scope: "project", subagentOnlyExtensions: "./tools/child-only.ts, /opt/pi/child.ts" } },
214
+ ctx,
215
+ );
216
+
217
+ assert.equal(result.isError, false);
218
+ const filePath = path.join(tempDir, ".selesai", "agents", "child-tool-user.md");
219
+ const content = fs.readFileSync(filePath, "utf-8");
220
+ assert.match(content, /^subagentOnlyExtensions: \.\/tools\/child-only\.ts, \/opt\/pi\/child\.ts$/m);
221
+
222
+ const got = handleManagementAction("get", { agent: "child-tool-user" }, ctx);
223
+ assert.equal(got.isError, false);
224
+ assert.match(readText(got), /Subagent-only extensions: \.\/tools\/child-only\.ts, \/opt\/pi\/child\.ts/);
225
+ });
226
+
227
+ it("does not serialize settings overrides into custom agent frontmatter during updates", () => {
228
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [{ provider: "anthropic", id: "claude-sonnet-4-6" }] } };
229
+ const settingsPath = path.join(tempDir, ".selesai", "settings.json");
230
+ const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
231
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
232
+ fs.writeFileSync(settingsPath, JSON.stringify({
233
+ subagents: {
234
+ agentOverrides: {
235
+ implementer: {
236
+ model: "anthropic/claude-sonnet-4-6",
237
+ systemPromptMode: "append",
238
+ inheritProjectContext: true,
239
+ inheritSkills: true,
240
+ },
241
+ },
242
+ },
243
+ }, null, 2), "utf-8");
244
+ fs.writeFileSync(agentPath, `---
245
+ name: implementer
246
+ description: TDD implementer
247
+ ---
248
+
249
+ Drive the failing test first.
250
+ `, "utf-8");
251
+
252
+ const got = handleManagementAction("get", { agent: "implementer" }, ctx);
253
+ assert.equal(got.isError, false);
254
+ const beforeText = readText(got);
255
+ assert.match(beforeText, /Model: anthropic\/claude-sonnet-4-6/);
256
+ assert.match(beforeText, /System prompt mode: append/);
257
+ assert.match(beforeText, /Inherit project context: true/);
258
+ assert.match(beforeText, /Inherit skills: true/);
259
+
260
+ const updated = handleUpdate(
261
+ { agent: "implementer", config: { description: "Updated implementer" } },
262
+ ctx,
263
+ );
264
+ assert.equal(updated.isError, false);
265
+
266
+ const content = fs.readFileSync(agentPath, "utf-8");
267
+ assert.match(content, /^description: Updated implementer$/m);
268
+ assert.doesNotMatch(content, /^model:/m);
269
+ assert.doesNotMatch(content, /^systemPromptMode:/m);
270
+ assert.doesNotMatch(content, /^inheritProjectContext:/m);
271
+ assert.doesNotMatch(content, /^inheritSkills:/m);
272
+
273
+ const gotAfter = handleManagementAction("get", { agent: "implementer" }, ctx);
274
+ assert.equal(gotAfter.isError, false);
275
+ const afterText = readText(gotAfter);
276
+ assert.match(afterText, /Model: anthropic\/claude-sonnet-4-6/);
277
+ assert.match(afterText, /System prompt mode: append/);
278
+ assert.match(afterText, /Inherit project context: true/);
279
+ assert.match(afterText, /Inherit skills: true/);
280
+ });
281
+
282
+ it("preserves explicit default-like frontmatter that blocks settings overrides during updates", () => {
283
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
284
+ const settingsPath = path.join(tempDir, ".selesai", "settings.json");
285
+ const agentPath = path.join(tempDir, ".selesai", "agents", "implementer.md");
286
+ fs.mkdirSync(path.dirname(agentPath), { recursive: true });
287
+ fs.writeFileSync(settingsPath, JSON.stringify({
288
+ subagents: {
289
+ agentOverrides: {
290
+ implementer: {
291
+ thinking: "high",
292
+ fallbackModels: ["openai/gpt-5-mini"],
293
+ tools: ["bash"],
294
+ skills: ["override-skill"],
295
+ defaultContext: "fork",
296
+ completionGuard: false,
297
+ toolBudget: { hard: 3 },
298
+ },
299
+ },
300
+ },
301
+ }, null, 2), "utf-8");
302
+ fs.writeFileSync(agentPath, `---
303
+ name: implementer
304
+ description: TDD implementer
305
+ fallbackModels:
306
+ thinking: off
307
+ tools:
308
+ skills:
309
+ defaultContext:
310
+ completionGuard: true
311
+ toolBudget:
312
+ ---
313
+
314
+ Drive the failing test first.
315
+ `, "utf-8");
316
+
317
+ const got = handleManagementAction("get", { agent: "implementer" }, ctx);
318
+ assert.equal(got.isError, false);
319
+ const beforeText = readText(got);
320
+ assert.match(beforeText, /Thinking: off/);
321
+ assert.doesNotMatch(beforeText, /Thinking: high/);
322
+
323
+ const updated = handleUpdate(
324
+ { agent: "implementer", config: { description: "Updated implementer" } },
325
+ ctx,
326
+ );
327
+ assert.equal(updated.isError, false);
328
+
329
+ const content = fs.readFileSync(agentPath, "utf-8");
330
+ assert.match(content, /^description: Updated implementer$/m);
331
+ assert.match(content, /^fallbackModels: ?$/m);
332
+ assert.match(content, /^thinking: off$/m);
333
+ assert.match(content, /^tools: ?$/m);
334
+ assert.match(content, /^skills: ?$/m);
335
+ assert.match(content, /^defaultContext: ?$/m);
336
+ assert.match(content, /^completionGuard: true$/m);
337
+ assert.match(content, /^toolBudget: ?$/m);
338
+
339
+ const gotAfter = handleManagementAction("get", { agent: "implementer" }, ctx);
340
+ assert.equal(gotAfter.isError, false);
341
+ const afterText = readText(gotAfter);
342
+ assert.match(afterText, /Thinking: off/);
343
+ assert.doesNotMatch(afterText, /Thinking: high/);
344
+ });
345
+
346
+ it("updates JSON chain descriptions without rewriting them as markdown", () => {
347
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
348
+ const chainPath = path.join(tempDir, ".selesai", "chains", "dynamic-review.chain.json");
349
+ fs.mkdirSync(path.dirname(chainPath), { recursive: true });
350
+ fs.writeFileSync(chainPath, JSON.stringify({
351
+ name: "dynamic-review",
352
+ description: "Review dynamic targets",
353
+ chain: [
354
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
355
+ {
356
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
357
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
358
+ collect: { as: "reviews" },
359
+ },
360
+ ],
361
+ }), "utf-8");
362
+
363
+ const updated = handleUpdate({ chainName: "dynamic-review", config: { description: "Updated dynamic review" } }, ctx);
364
+
365
+ assert.equal(updated.isError, false);
366
+ const content = fs.readFileSync(chainPath, "utf-8");
367
+ assert.doesNotMatch(content, /^---/);
368
+ const parsed = JSON.parse(content) as { description?: string; chain?: Array<{ collect?: { as?: string } }> };
369
+ assert.equal(parsed.description, "Updated dynamic review");
370
+ assert.equal(parsed.chain?.[1]?.collect?.as, "reviews");
371
+ });
372
+
373
+ it("renames and repackages JSON chains while preserving JSON format and extension", () => {
374
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
375
+ const chainPath = path.join(tempDir, ".selesai", "chains", "dynamic-review.chain.json");
376
+ fs.mkdirSync(path.dirname(chainPath), { recursive: true });
377
+ fs.writeFileSync(chainPath, JSON.stringify({
378
+ name: "dynamic-review",
379
+ description: "Review dynamic targets",
380
+ chain: [{ agent: "scout", task: "Return targets" }],
381
+ }), "utf-8");
382
+
383
+ const updated = handleUpdate({ chainName: "dynamic-review", config: { name: "Review Flow", package: "Code Analysis" } }, ctx);
384
+
385
+ assert.equal(updated.isError, false);
386
+ const updatedPath = path.join(tempDir, ".selesai", "chains", "code-analysis.review-flow.chain.json");
387
+ assert.equal(fs.existsSync(chainPath), false);
388
+ const content = fs.readFileSync(updatedPath, "utf-8");
389
+ assert.doesNotMatch(content, /^---/);
390
+ const parsed = JSON.parse(content) as { name?: string; package?: string; chain?: Array<{ agent?: string }> };
391
+ assert.equal(parsed.name, "review-flow");
392
+ assert.equal(parsed.package, "code-analysis");
393
+ assert.equal(parsed.chain?.[0]?.agent, "scout");
394
+ });
395
+
396
+ it("gets dynamic JSON chain details and lists invalid chain diagnostics", () => {
397
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
398
+ fs.mkdirSync(path.join(tempDir, ".selesai", "chains"), { recursive: true });
399
+ fs.writeFileSync(path.join(tempDir, ".selesai", "chains", "dynamic-review.chain.json"), JSON.stringify({
400
+ name: "dynamic-review",
401
+ description: "Review dynamic targets",
402
+ chain: [
403
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
404
+ {
405
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
406
+ parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
407
+ collect: { as: "reviews" },
408
+ },
409
+ ],
410
+ }), "utf-8");
411
+ fs.writeFileSync(path.join(tempDir, ".selesai", "chains", "broken.chain.json"), "{", "utf-8");
412
+
413
+ const got = handleManagementAction("get", { chainName: "dynamic-review" }, ctx);
414
+ assert.equal(got.isError, false);
415
+ assert.match(readText(got), /Dynamic fanout -> reviews/);
416
+ assert.match(readText(got), /Expand: targets\/items/);
417
+ assert.match(readText(got), /Agent: reviewer/);
418
+
419
+ const listed = handleManagementAction("list", {}, ctx);
420
+ assert.equal(listed.isError, false);
421
+ assert.match(readText(listed), /Chain diagnostics:/);
422
+ assert.match(readText(listed), /broken\.chain\.json/);
423
+ assert.match(readText(listed), /Invalid JSON chain/);
424
+ });
425
+
426
+ it("reports builtin runtime-loaded model mappings from current session state", () => {
427
+ const ctx = {
428
+ cwd: tempDir,
429
+ modelRegistry: {
430
+ getAvailable: () => [
431
+ { provider: "openai", id: "gpt-5-mini" },
432
+ { provider: "anthropic", id: "claude-sonnet-4" },
433
+ ],
434
+ },
435
+ model: { provider: "openai", id: "gpt-5-mini" },
436
+ };
437
+
438
+ const result = handleManagementAction("models", {}, ctx);
439
+ const text = readText(result);
440
+ assert.equal(result.isError, false);
441
+ assert.match(text, /^Builtin subagent models/m);
442
+ assert.match(text, /Current session model:\n openai\/gpt-5-mini/);
443
+ assert.match(text, /(?:^|\n)scout\n model:\n openai\/gpt-5-mini\n source: inherits current session model(?:\n|$)/);
444
+ });
445
+
446
+ it("reports override source and disabled builtin state in runtime model mappings", () => {
447
+ const projectSettingsPath = path.join(tempDir, ".selesai", "settings.json");
448
+ fs.mkdirSync(path.dirname(projectSettingsPath), { recursive: true });
449
+ fs.writeFileSync(projectSettingsPath, JSON.stringify({
450
+ subagents: {
451
+ agentOverrides: {
452
+ reviewer: { model: "claude-sonnet-4", disabled: true },
453
+ },
454
+ },
455
+ }, null, 2), "utf-8");
456
+
457
+ const ctx = {
458
+ cwd: tempDir,
459
+ modelRegistry: {
460
+ getAvailable: () => [
461
+ { provider: "openai", id: "gpt-5-mini" },
462
+ { provider: "anthropic", id: "claude-sonnet-4" },
463
+ ],
464
+ },
465
+ model: { provider: "openai", id: "gpt-5-mini" },
466
+ };
467
+
468
+ const result = handleManagementAction("models", { agent: "reviewer" }, ctx);
469
+ const text = readText(result);
470
+ assert.equal(result.isError, false);
471
+ assert.match(text, /^Builtin subagent model/m);
472
+ assert.match(text, /Agent: reviewer/);
473
+ assert.match(text, /Effective model:\n anthropic\/claude-sonnet-4/);
474
+ assert.match(text, /Source: project override/);
475
+ assert.match(text, /Requested model setting:\n claude-sonnet-4/);
476
+ assert.match(text, /Disabled: true/);
477
+ assert.match(text.replaceAll("\\", "/"), /Override file:\n .*\.pi\/settings\.json/);
478
+ });
479
+
480
+ it("rejects unknown builtin filters for runtime model mappings", () => {
481
+ const result = handleManagementAction("models", { agent: "not-a-builtin" }, {
482
+ cwd: tempDir,
483
+ modelRegistry: { getAvailable: () => [] },
484
+ });
485
+
486
+ assert.equal(result.isError, true);
487
+ assert.match(readText(result), /Builtin agent 'not-a-builtin' not found/);
488
+ });
489
+
490
+ it("creates delegate with its builtin prompt defaults", () => {
491
+ const result = handleCreate(
492
+ { config: { name: "delegate", description: "Delegate helper", scope: "project" } },
493
+ { cwd: tempDir, modelRegistry: { getAvailable: () => [] } },
494
+ );
495
+
496
+ assert.equal(result.isError, false);
497
+ const filePath = path.join(tempDir, ".selesai", "agents", "delegate.md");
498
+ const content = fs.readFileSync(filePath, "utf-8");
499
+ assert.match(content, /systemPromptMode: append/);
500
+ assert.match(content, /inheritProjectContext: true/);
501
+ assert.match(content, /inheritSkills: false/);
502
+ });
503
+
504
+ it("lists proactive skill subagent suggestions from repeated configured skill use", () => {
505
+ const ctx = { cwd: tempDir, modelRegistry: { getAvailable: () => [] } };
506
+ fs.mkdirSync(path.join(tempDir, ".selesai", "agents"), { recursive: true });
507
+ fs.mkdirSync(path.join(tempDir, ".selesai", "skills", "deslop"), { recursive: true });
508
+ fs.writeFileSync(path.join(tempDir, ".selesai", "skills", "deslop", "SKILL.md"), `---
509
+ description: Cleanup review.
510
+ ---
511
+
512
+ Review for cleanup.
513
+ `, "utf-8");
514
+ for (const name of ["cleanup-a", "cleanup-b"]) {
515
+ fs.writeFileSync(path.join(tempDir, ".selesai", "agents", `${name}.md`), `---
516
+ name: ${name}
517
+ description: Cleanup ${name}
518
+ skills: deslop
519
+ ---
520
+
521
+ Inspect cleanup.
522
+ `, "utf-8");
523
+ }
524
+
525
+ const listed = handleManagementAction("list", {}, ctx);
526
+ const text = readText(listed);
527
+ assert.match(text, /Proactive skill subagent suggestions:/);
528
+ assert.match(text, /- deslop via reviewer/);
529
+ assert.match(text, /Cleanup review\./);
530
+ });
531
+
532
+ it("can disable proactive skill subagent suggestions in config", () => {
533
+ const ctx = {
534
+ cwd: tempDir,
535
+ modelRegistry: { getAvailable: () => [] },
536
+ config: { proactiveSkillSubagents: false },
537
+ };
538
+ fs.mkdirSync(path.join(tempDir, ".selesai", "agents"), { recursive: true });
539
+ fs.mkdirSync(path.join(tempDir, ".selesai", "skills", "deslop"), { recursive: true });
540
+ fs.writeFileSync(path.join(tempDir, ".selesai", "skills", "deslop", "SKILL.md"), "Review for cleanup.\n", "utf-8");
541
+ for (const name of ["cleanup-a", "cleanup-b"]) {
542
+ fs.writeFileSync(path.join(tempDir, ".selesai", "agents", `${name}.md`), `---
543
+ name: ${name}
544
+ description: Cleanup ${name}
545
+ skills: deslop
546
+ ---
547
+
548
+ Inspect cleanup.
549
+ `, "utf-8");
550
+ }
551
+
552
+ const listed = handleManagementAction("list", {}, ctx);
553
+ assert.doesNotMatch(readText(listed), /Proactive skill subagent suggestions:/);
554
+ });
555
+
556
+ });