@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,217 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { ChainOutputValidationError, validateChainOutputBindings, validateChainOutputBindingsWithContext } from "../../src/runs/shared/chain-outputs.ts";
4
+ import {
5
+ DynamicFanoutError,
6
+ collectDynamicResults,
7
+ materializeDynamicParallelStep,
8
+ resolveJsonPointer,
9
+ validateDynamicCollection,
10
+ validateDynamicStepShape,
11
+ } from "../../src/runs/shared/dynamic-fanout.ts";
12
+ import type { ChainStep } from "../../src/shared/settings.ts";
13
+ import type { ChainOutputMap, SingleResult } from "../../src/shared/types.ts";
14
+
15
+ const outputs: ChainOutputMap = {
16
+ targets: {
17
+ text: "{\"items\":[{\"path\":\"src/a.ts\"},{\"path\":\"src/b.ts\"}]}",
18
+ structured: { items: [{ path: "src/a.ts" }, { path: "src/b.ts" }] },
19
+ agent: "scout",
20
+ stepIndex: 0,
21
+ },
22
+ };
23
+
24
+ describe("dynamic fanout helpers", () => {
25
+ it("resolves JSON Pointers and materializes item templates", () => {
26
+ assert.deepEqual(resolveJsonPointer({ items: [1, 2] }, "/items/1", "path"), 2);
27
+ const step: ChainStep = {
28
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
29
+ parallel: { agent: "reviewer", task: "Review {target.path}", label: "Review {target.path}" },
30
+ collect: { as: "reviews" },
31
+ };
32
+ const materialized = materializeDynamicParallelStep(step, outputs, 1);
33
+ assert.deepEqual(materialized.items.map((item) => item.key), ["src/a.ts", "src/b.ts"]);
34
+ assert.deepEqual(materialized.parallel.map((task) => task.task), ["Review src/a.ts", "Review src/b.ts"]);
35
+ assert.deepEqual(materialized.parallel.map((task) => task.label), ["Review src/a.ts", "Review src/b.ts"]);
36
+ });
37
+
38
+ it("rejects missing structured sources, over-limit arrays, duplicate keys, colliding ids, and bad templates", () => {
39
+ const base: ChainStep = {
40
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
41
+ parallel: { agent: "reviewer", task: "Review {target.path}" },
42
+ collect: { as: "reviews" },
43
+ };
44
+ assert.throws(
45
+ () => materializeDynamicParallelStep({ ...base, expand: { ...base.expand, maxItems: 1 } }, outputs, 1),
46
+ /exceeding maxItems/,
47
+ );
48
+ assert.throws(
49
+ () => materializeDynamicParallelStep(base, { targets: { text: "plain", agent: "scout", stepIndex: 0 } }, 1),
50
+ /requires structured output/,
51
+ );
52
+ assert.throws(
53
+ () => materializeDynamicParallelStep(base, { targets: { ...outputs.targets, structured: { items: [{ path: "x" }, { path: "x" }] } } }, 1),
54
+ /duplicate item key/,
55
+ );
56
+ assert.throws(
57
+ () => materializeDynamicParallelStep(base, { targets: { ...outputs.targets, structured: { items: [{ path: "a/b" }, { path: "a-b" }] } } }, 1),
58
+ /colliding item id/,
59
+ );
60
+ assert.throws(
61
+ () => materializeDynamicParallelStep({ ...base, parallel: { agent: "reviewer", task: "Review {other.path}" } }, outputs, 1),
62
+ /Unsupported template reference/,
63
+ );
64
+ assert.throws(
65
+ () => materializeDynamicParallelStep({ ...base, parallel: { agent: "reviewer", task: "Review {target[path]}" } }, outputs, 1),
66
+ /Invalid item reference/,
67
+ );
68
+ assert.throws(
69
+ () => materializeDynamicParallelStep({ ...base, parallel: { agent: "reviewer", task: "Review {target.path" } }, outputs, 1),
70
+ /Invalid item reference/,
71
+ );
72
+ });
73
+
74
+ it("allows config maxItems defaults and handles empty arrays deterministically", () => {
75
+ const base: ChainStep = {
76
+ expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path" },
77
+ parallel: { agent: "reviewer", task: "Review {target.path}" },
78
+ collect: { as: "reviews" },
79
+ };
80
+ const materialized = materializeDynamicParallelStep(base, outputs, 1, { maxItems: 4 });
81
+ assert.equal(materialized.parallel.length, 2);
82
+ assert.doesNotThrow(() => validateChainOutputBindings([
83
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
84
+ base,
85
+ ], { maxItems: 4 }));
86
+ const empty = materializeDynamicParallelStep(base, { targets: { ...outputs.targets, structured: { items: [] } } }, 1, { maxItems: 4 });
87
+ assert.equal(empty.parallel.length, 0);
88
+ assert.throws(
89
+ () => materializeDynamicParallelStep({ ...base, expand: { ...base.expand, onEmpty: "fail" } }, { targets: { ...outputs.targets, structured: { items: [] } } }, 1, { maxItems: 4 }),
90
+ /source array is empty/,
91
+ );
92
+ });
93
+
94
+ it("rejects malformed dynamic-like shapes before they can run as static parallel", () => {
95
+ const malformed = [
96
+ {
97
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
98
+ parallel: [{ agent: "reviewer", task: "Review" }],
99
+ collect: { as: "reviews" },
100
+ },
101
+ {
102
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
103
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
104
+ },
105
+ {
106
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
107
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
108
+ collect: { as: "reviews" },
109
+ when: "later",
110
+ },
111
+ {
112
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
113
+ parallel: { agent: "reviewer", task: "Review {item.path}", as: "child" },
114
+ collect: { as: "reviews" },
115
+ },
116
+ ] as ChainStep[];
117
+
118
+ for (const step of malformed) {
119
+ assert.throws(
120
+ () => validateChainOutputBindings([{ agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } }, step]),
121
+ ChainOutputValidationError,
122
+ );
123
+ }
124
+ });
125
+
126
+ it("accepts a runner-injected parentSessionId on the parallel template but keeps it out of user-facing validation", () => {
127
+ // Regression: the async runner threads parentSessionId onto the dynamic parallel
128
+ // template for permission-system forwarding. It must pass runner-internal validation
129
+ // (allowRunnerFields) without leaking into the user-facing dynamic field whitelist.
130
+ const runnerStep = {
131
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
132
+ parallel: { agent: "reviewer", task: "Review {item.path}", parentSessionId: "session-parent" },
133
+ collect: { as: "reviews" },
134
+ } as unknown as Parameters<typeof validateDynamicStepShape>[0];
135
+ assert.doesNotThrow(() => validateDynamicStepShape(runnerStep, 1, { allowRunnerFields: true }));
136
+ assert.throws(
137
+ () => validateDynamicStepShape(runnerStep, 1),
138
+ (error: unknown) => error instanceof DynamicFanoutError && /parentSessionId/.test(error.message),
139
+ );
140
+ });
141
+
142
+ it("accepts toolBudget on dynamic parallel templates", () => {
143
+ const step = {
144
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
145
+ parallel: { agent: "reviewer", task: "Review {item.path}", toolBudget: { hard: 3 } },
146
+ collect: { as: "reviews" },
147
+ } as unknown as Parameters<typeof validateDynamicStepShape>[0];
148
+
149
+ assert.doesNotThrow(() => validateDynamicStepShape(step, 1));
150
+ });
151
+
152
+ it("validates source ordering and collect name collisions", () => {
153
+ const chain: ChainStep[] = [
154
+ { agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
155
+ {
156
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
157
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
158
+ collect: { as: "targets" },
159
+ },
160
+ ];
161
+ assert.throws(() => validateChainOutputBindings(chain), ChainOutputValidationError);
162
+ assert.throws(
163
+ () => validateChainOutputBindings([chain[1]!]),
164
+ /unknown output 'targets'/,
165
+ );
166
+ });
167
+
168
+ it("validates appended steps against prior and reserved output names", () => {
169
+ assert.doesNotThrow(() => validateChainOutputBindingsWithContext([
170
+ { agent: "worker", task: "Use {outputs.targets}", as: "summary" },
171
+ ], {}, { priorOutputNames: ["targets"], startStepIndex: 2 }));
172
+ assert.throws(
173
+ () => validateChainOutputBindingsWithContext([
174
+ { agent: "worker", task: "Use {outputs.missing}" },
175
+ ], {}, { priorOutputNames: ["targets"], startStepIndex: 2 }),
176
+ /Unknown chain output reference '\{outputs\.missing\}' at step 3/,
177
+ );
178
+ assert.throws(
179
+ () => validateChainOutputBindingsWithContext([
180
+ { agent: "worker", task: "Use {previous}", as: "targets" },
181
+ ], {}, { priorOutputNames: ["targets"], startStepIndex: 2 }),
182
+ /Duplicate chain output name 'targets'/,
183
+ );
184
+ assert.doesNotThrow(() => validateChainOutputBindingsWithContext([
185
+ {
186
+ expand: { from: { output: "targets", path: "/items" }, maxItems: 4 },
187
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
188
+ collect: { as: "reviews" },
189
+ },
190
+ ], {}, { priorOutputNames: ["targets"], startStepIndex: 2 }));
191
+ });
192
+
193
+ it("collects ordered child result records and validates aggregate schema", () => {
194
+ const step: ChainStep = {
195
+ expand: { from: { output: "targets", path: "/items" }, key: "/path", maxItems: 4 },
196
+ parallel: { agent: "reviewer", task: "Review {item.path}" },
197
+ collect: { as: "reviews" },
198
+ };
199
+ const materialized = materializeDynamicParallelStep(step, outputs, 1);
200
+ const result = (agent: string, structuredOutput: unknown): SingleResult => ({
201
+ agent,
202
+ task: "t",
203
+ exitCode: 0,
204
+ messages: [],
205
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
206
+ finalOutput: "ok",
207
+ structuredOutput,
208
+ });
209
+ const timedOut = { ...result("reviewer", { ok: "b" }), exitCode: 1, error: "Subagent timed out after 300ms.", timedOut: true };
210
+ const collected = collectDynamicResults(step, materialized.items, [result("reviewer", { ok: "a" }), timedOut]);
211
+ assert.deepEqual(collected.map((item) => item.key), ["src/a.ts", "src/b.ts"]);
212
+ assert.deepEqual(collected.map((item) => item.structured), [{ ok: "a" }, { ok: "b" }]);
213
+ assert.equal(collected[1]?.timedOut, true);
214
+ assert.doesNotThrow(() => validateDynamicCollection({ type: "array", minItems: 2 }, collected));
215
+ assert.throws(() => validateDynamicCollection({ type: "object" }, collected), DynamicFanoutError);
216
+ });
217
+ });
@@ -0,0 +1,88 @@
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 { discoverAgents, discoverAgentsAll, EXTRA_AGENT_DIRS_ENV } from "../../src/agents/agents.ts";
7
+
8
+ let tempDir = "";
9
+ let agentDir = "";
10
+ let cwd = "";
11
+ const saved: Record<string, string | undefined> = {};
12
+ const MANAGED_ENV = ["SELESAI_CODING_AGENT_DIR", "HOME", "USERPROFILE", EXTRA_AGENT_DIRS_ENV];
13
+
14
+ function writeAgent(dir: string, name: string): string {
15
+ const filePath = path.join(dir, `${name}.md`);
16
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
17
+ fs.writeFileSync(filePath, `---\nname: ${name}\ndescription: ${name} agent\n---\n\nDo ${name} work.\n`, "utf-8");
18
+ return filePath;
19
+ }
20
+
21
+ describe("SELESAI_SUBAGENT_EXTRA_AGENT_DIRS discovery", () => {
22
+ beforeEach(() => {
23
+ for (const key of MANAGED_ENV) saved[key] = process.env[key];
24
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-extra-agent-dirs-"));
25
+ // Isolate from the developer's real user agent dirs so defaults are empty.
26
+ agentDir = path.join(tempDir, "agent");
27
+ const homeDir = path.join(tempDir, "home");
28
+ cwd = path.join(tempDir, "workspace");
29
+ fs.mkdirSync(cwd, { recursive: true });
30
+ fs.mkdirSync(homeDir, { recursive: true });
31
+ process.env.SELESAI_CODING_AGENT_DIR = agentDir;
32
+ process.env.HOME = homeDir;
33
+ process.env.USERPROFILE = homeDir;
34
+ delete process.env[EXTRA_AGENT_DIRS_ENV];
35
+ });
36
+
37
+ afterEach(() => {
38
+ for (const key of MANAGED_ENV) {
39
+ if (saved[key] === undefined) delete process.env[key];
40
+ else process.env[key] = saved[key];
41
+ }
42
+ fs.rmSync(tempDir, { recursive: true, force: true });
43
+ });
44
+
45
+ it("discovers agents from the env-provided dirs as a 'user' source", () => {
46
+ const bundledDir = path.join(tempDir, "store", "agents");
47
+ const bundledAgent = writeAgent(bundledDir, "bundled-reviewer");
48
+ process.env[EXTRA_AGENT_DIRS_ENV] = bundledDir;
49
+
50
+ const scoped = discoverAgents(cwd, "user");
51
+ const found = scoped.agents.find((agent) => agent.name === "bundled-reviewer");
52
+ assert.ok(found, "expected bundled agent under 'user' scope");
53
+ assert.equal(found?.filePath, bundledAgent);
54
+
55
+ const all = discoverAgentsAll(cwd);
56
+ assert.ok(all.user.find((agent) => agent.name === "bundled-reviewer" && agent.filePath === bundledAgent));
57
+ });
58
+
59
+ it("scans every directory listed (PATH-style delimiter)", () => {
60
+ const dirA = path.join(tempDir, "store-a");
61
+ const dirB = path.join(tempDir, "store-b");
62
+ writeAgent(dirA, "agent-a");
63
+ writeAgent(dirB, "agent-b");
64
+ process.env[EXTRA_AGENT_DIRS_ENV] = [dirA, dirB].join(path.delimiter);
65
+
66
+ const all = discoverAgentsAll(cwd);
67
+ assert.ok(all.user.find((agent) => agent.name === "agent-a"));
68
+ assert.ok(all.user.find((agent) => agent.name === "agent-b"));
69
+ });
70
+
71
+ it("lets a local user agent override a bundled one of the same name", () => {
72
+ const bundledDir = path.join(tempDir, "store", "agents");
73
+ writeAgent(bundledDir, "shared");
74
+ const localPath = writeAgent(path.join(agentDir, "agents"), "shared");
75
+ process.env[EXTRA_AGENT_DIRS_ENV] = bundledDir;
76
+
77
+ const scoped = discoverAgents(cwd, "user");
78
+ const matches = scoped.agents.filter((agent) => agent.name === "shared");
79
+ assert.equal(matches.length, 1, "name collisions must dedupe");
80
+ assert.equal(matches[0]?.filePath, localPath, "local user agent should win over bundled");
81
+ });
82
+
83
+ it("ignores the env var when unset or empty", () => {
84
+ process.env[EXTRA_AGENT_DIRS_ENV] = "";
85
+ const all = discoverAgentsAll(cwd);
86
+ assert.deepEqual(all.user, []);
87
+ });
88
+ });
@@ -0,0 +1,68 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createFileCoalescer } from "../../src/shared/file-coalescer.ts";
4
+
5
+ type TimerTask = { id: number; cb: () => void; delay: number };
6
+
7
+ function createFakeTimers() {
8
+ let nextId = 1;
9
+ const tasks = new Map<number, TimerTask>();
10
+ return {
11
+ timerApi: {
12
+ setTimeout(handler: () => void, delayMs: number): unknown {
13
+ const id = nextId++;
14
+ tasks.set(id, { id, cb: handler, delay: delayMs });
15
+ return id;
16
+ },
17
+ clearTimeout(handle: unknown): void {
18
+ if (typeof handle === "number") tasks.delete(handle);
19
+ },
20
+ },
21
+ runAll(): void {
22
+ const batch = Array.from(tasks.values()).sort((a, b) => a.id - b.id);
23
+ tasks.clear();
24
+ for (const task of batch) task.cb();
25
+ },
26
+ pendingCount(): number {
27
+ return tasks.size;
28
+ },
29
+ };
30
+ }
31
+
32
+ describe("createFileCoalescer", () => {
33
+ it("coalesces duplicate schedule calls per file", () => {
34
+ const events: string[] = [];
35
+ const timers = createFakeTimers();
36
+ const coalescer = createFileCoalescer((file) => events.push(file), 50, timers.timerApi);
37
+ assert.equal(coalescer.schedule("a.json"), true);
38
+ assert.equal(coalescer.schedule("a.json"), false);
39
+ assert.equal(timers.pendingCount(), 1);
40
+ timers.runAll();
41
+ assert.deepEqual(events, ["a.json"]);
42
+ assert.equal(coalescer.schedule("a.json"), true);
43
+ });
44
+
45
+ it("allows different files to schedule independently", () => {
46
+ const events: string[] = [];
47
+ const timers = createFakeTimers();
48
+ const coalescer = createFileCoalescer((file) => events.push(file), 50, timers.timerApi);
49
+ coalescer.schedule("a.json");
50
+ coalescer.schedule("b.json");
51
+ assert.equal(timers.pendingCount(), 2);
52
+ timers.runAll();
53
+ assert.deepEqual(events.sort(), ["a.json", "b.json"]);
54
+ });
55
+
56
+ it("clear cancels all pending handlers", () => {
57
+ const events: string[] = [];
58
+ const timers = createFakeTimers();
59
+ const coalescer = createFileCoalescer((file) => events.push(file), 50, timers.timerApi);
60
+ coalescer.schedule("a.json");
61
+ coalescer.schedule("b.json");
62
+ assert.equal(timers.pendingCount(), 2);
63
+ coalescer.clear();
64
+ assert.equal(timers.pendingCount(), 0);
65
+ timers.runAll();
66
+ assert.deepEqual(events, []);
67
+ });
68
+ });
@@ -0,0 +1,78 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { compactForegroundResult, extractToolArgsPreview } from "../../src/shared/utils.ts";
4
+ import { formatToolCall } from "../../src/shared/formatters.ts";
5
+
6
+ describe("foreground tool-call compaction", () => {
7
+ it("stores compact tool-call summaries instead of raw message payloads", () => {
8
+ const result = compactForegroundResult({
9
+ agent: "tester",
10
+ task: "run checks",
11
+ exitCode: 0,
12
+ messages: [{
13
+ role: "assistant",
14
+ content: [{
15
+ type: "toolCall",
16
+ name: "write",
17
+ arguments: {
18
+ path: "/tmp/report.md",
19
+ content: "x".repeat(50_000),
20
+ },
21
+ }],
22
+ }],
23
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
24
+ });
25
+
26
+ assert.equal(result.messages, undefined);
27
+ assert.deepEqual(result.toolCalls, [{
28
+ text: "write /tmp/report.md",
29
+ expandedText: "write /tmp/report.md",
30
+ }]);
31
+ });
32
+
33
+ it("keeps expanded generic tool-call previews bounded", () => {
34
+ const collapsed = formatToolCall("custom", { payload: "x".repeat(500) });
35
+ const expanded = formatToolCall("custom", { payload: "x".repeat(500) }, true);
36
+
37
+ assert.ok(expanded.length > collapsed.length);
38
+ assert.ok(expanded.length < 200);
39
+ });
40
+
41
+ it("does not keep an empty toolCalls array after compaction", () => {
42
+ const result = compactForegroundResult({
43
+ agent: "tester",
44
+ task: "run checks",
45
+ exitCode: 0,
46
+ messages: [],
47
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 },
48
+ });
49
+
50
+ assert.equal(result.toolCalls, undefined);
51
+ });
52
+
53
+ it("formats array-based web search previews clearly", () => {
54
+ assert.equal(
55
+ extractToolArgsPreview({
56
+ queries: ["Chrome native messaging manifest path macOS", "Chromium native messaging path macOS"],
57
+ workflow: "none",
58
+ }),
59
+ "Chrome native messaging manifest path macOS (+1 more)",
60
+ );
61
+ });
62
+
63
+ it("formats fetch_content urls clearly", () => {
64
+ assert.equal(
65
+ extractToolArgsPreview({
66
+ urls: ["https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging", "https://example.com/backup"],
67
+ }),
68
+ "https://developer.chrome.com/docs/extensions/develop/conc...",
69
+ );
70
+ });
71
+
72
+ it("falls back to generic array previews", () => {
73
+ assert.equal(
74
+ extractToolArgsPreview({ ids: ["run-a", "run-b", "run-c"] }),
75
+ "ids=run-a (+2 more)",
76
+ );
77
+ });
78
+ });