@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,271 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { describe, it } from "node:test";
6
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../../src/runs/shared/pi-args.ts";
7
+
8
+ const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+
10
+ function parentToolEnv(): NodeJS.ProcessEnv {
11
+ const env = { ...process.env };
12
+ delete env[SUBAGENT_CHILD_ENV];
13
+ delete env[SUBAGENT_FANOUT_CHILD_ENV];
14
+ return env;
15
+ }
16
+
17
+ describe("subagent extension child mode", () => {
18
+ it("collapses tool detail before direct subagent tool execution", () => {
19
+ const script = String.raw`
20
+ import registerSubagentExtension from "./src/extension/index.ts";
21
+ const events = { on() { return () => {}; }, emit() {} };
22
+ let registeredTool;
23
+ const fakePi = new Proxy({
24
+ events,
25
+ registerTool(tool) { if (tool.name === "subagent") registeredTool = tool; },
26
+ registerCommand() {},
27
+ registerShortcut() {},
28
+ registerMessageRenderer() {},
29
+ sendMessage() {},
30
+ getSessionName() { return undefined; },
31
+ }, {
32
+ get(target, prop) {
33
+ if (prop in target) return target[prop];
34
+ return () => undefined;
35
+ },
36
+ });
37
+ registerSubagentExtension(fakePi);
38
+ if (!registeredTool) throw new Error("tool not registered");
39
+ const calls = [];
40
+ const ctx = {
41
+ cwd: process.cwd(),
42
+ hasUI: true,
43
+ ui: {
44
+ setToolsExpanded(value) { calls.push(value); },
45
+ setWidget() {},
46
+ requestRender() {},
47
+ theme: { fg(_name, text) { return text; }, bg(_name, text) { return text; }, bold(text) { return text; } },
48
+ },
49
+ sessionManager: { getSessionId() { return "session-test"; }, getSessionFile() { return null; } },
50
+ modelRegistry: { getAvailable() { return []; } },
51
+ };
52
+ await registeredTool.execute("collapse-check", { action: "list" }, new AbortController().signal, undefined, ctx);
53
+ if (calls[0] !== false) throw new Error("expected setToolsExpanded(false), got " + JSON.stringify(calls));
54
+ `;
55
+
56
+ execFileSync(
57
+ process.execPath,
58
+ [
59
+ "--experimental-transform-types",
60
+ "--import",
61
+ "./test/support/register-loader.mjs",
62
+ "--input-type=module",
63
+ "--eval",
64
+ script,
65
+ ],
66
+ { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" },
67
+ );
68
+ });
69
+
70
+ it("does not show async badge for explicit foreground clarify chain calls", () => {
71
+ const script = String.raw`
72
+ import registerSubagentExtension from "./src/extension/index.ts";
73
+ const events = { on() { return () => {}; }, emit() {} };
74
+ let registeredTool;
75
+ const fakePi = new Proxy({
76
+ events,
77
+ registerTool(tool) { if (tool.name === "subagent") registeredTool = tool; },
78
+ registerCommand() {},
79
+ registerShortcut() {},
80
+ registerMessageRenderer() {},
81
+ sendMessage() {},
82
+ getSessionName() { return undefined; },
83
+ }, {
84
+ get(target, prop) {
85
+ if (prop in target) return target[prop];
86
+ return () => undefined;
87
+ },
88
+ });
89
+ registerSubagentExtension(fakePi);
90
+ if (!registeredTool) throw new Error("tool not registered");
91
+ const theme = { fg(_name, text) { return text; }, bold(text) { return text; } };
92
+ const asyncChain = registeredTool.renderCall({ chain: [{ agent: "worker" }, { agent: "reviewer" }], async: true }, theme).text;
93
+ const asyncParallel = registeredTool.renderCall({ tasks: [{ agent: "worker" }, { agent: "reviewer", count: 2 }], async: true }, theme).text;
94
+ const clarifyChain = registeredTool.renderCall({ chain: [{ agent: "worker" }, { agent: "reviewer" }], async: true, clarify: true }, theme).text;
95
+ if (!asyncChain.includes("[async]")) throw new Error("expected async chain badge, got " + asyncChain);
96
+ if (!asyncParallel.includes("parallel (3) [async]")) throw new Error("expected async parallel badge, got " + asyncParallel);
97
+ if (clarifyChain.includes("[async]")) throw new Error("unexpected clarify async badge: " + clarifyChain);
98
+ `;
99
+
100
+ execFileSync(
101
+ process.execPath,
102
+ [
103
+ "--experimental-transform-types",
104
+ "--import",
105
+ "./test/support/register-loader.mjs",
106
+ "--input-type=module",
107
+ "--eval",
108
+ script,
109
+ ],
110
+ { cwd: projectRoot, env: parentToolEnv(), stdio: "pipe" },
111
+ );
112
+ });
113
+
114
+ it("returns before registering anything for non-fanout children", () => {
115
+ const script = String.raw`
116
+ import registerSubagentExtension from "./src/extension/index.ts";
117
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "./src/runs/shared/pi-args.ts";
118
+ process.env[SUBAGENT_CHILD_ENV] = "1";
119
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "0";
120
+ const calls = [];
121
+ const fakePi = new Proxy({}, {
122
+ get(_target, prop) {
123
+ return (..._args) => {
124
+ calls.push(String(prop));
125
+ return undefined;
126
+ };
127
+ },
128
+ });
129
+ registerSubagentExtension(fakePi);
130
+ if (calls.length > 0) {
131
+ throw new Error("Unexpected child-mode registrations: " + calls.join(", "));
132
+ }
133
+ `;
134
+
135
+ execFileSync(
136
+ process.execPath,
137
+ [
138
+ "--experimental-transform-types",
139
+ "--import",
140
+ "./test/support/register-loader.mjs",
141
+ "--input-type=module",
142
+ "--eval",
143
+ script,
144
+ ],
145
+ { cwd: projectRoot, stdio: "pipe" },
146
+ );
147
+ });
148
+
149
+ it("returns before registering anything for fanout children", () => {
150
+ const script = String.raw`
151
+ import registerSubagentExtension from "./src/extension/index.ts";
152
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "./src/runs/shared/pi-args.ts";
153
+ process.env[SUBAGENT_CHILD_ENV] = "1";
154
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
155
+ const calls = [];
156
+ const fakePi = new Proxy({}, {
157
+ get(target, prop) {
158
+ if (prop in target) return target[prop];
159
+ return (..._args) => {
160
+ calls.push(String(prop));
161
+ return undefined;
162
+ };
163
+ },
164
+ });
165
+ registerSubagentExtension(fakePi);
166
+ if (calls.length > 0) {
167
+ throw new Error("Unexpected child-mode registrations: " + calls.join(", "));
168
+ }
169
+ `;
170
+
171
+ execFileSync(
172
+ process.execPath,
173
+ [
174
+ "--experimental-transform-types",
175
+ "--import",
176
+ "./test/support/register-loader.mjs",
177
+ "--input-type=module",
178
+ "--eval",
179
+ script,
180
+ ],
181
+ { cwd: projectRoot, stdio: "pipe" },
182
+ );
183
+ });
184
+
185
+ it("does not double-register the child-safe subagent tool when index and fanout-child both load", () => {
186
+ const script = String.raw`
187
+ import registerSubagentExtension from "./src/extension/index.ts";
188
+ import registerFanoutChildSubagentExtension from "./src/extension/fanout-child.ts";
189
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "./src/runs/shared/pi-args.ts";
190
+ process.env[SUBAGENT_CHILD_ENV] = "1";
191
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
192
+
193
+ const registeredNames = new Set();
194
+ const registrations = [];
195
+ function makePi(source) {
196
+ return {
197
+ events: { on() { return () => {}; }, emit() {} },
198
+ registerTool(tool) {
199
+ if (registeredNames.has(tool.name)) {
200
+ throw new Error("Tool " + tool.name + " conflicts with " + source);
201
+ }
202
+ registeredNames.add(tool.name);
203
+ registrations.push({ source, name: tool.name });
204
+ },
205
+ getSessionName() { return undefined; },
206
+ };
207
+ }
208
+
209
+ registerSubagentExtension(makePi("index.ts"));
210
+ registerFanoutChildSubagentExtension(makePi("fanout-child.ts"));
211
+ if (registrations.length !== 1 || registrations[0].name !== "subagent" || registrations[0].source !== "fanout-child.ts") {
212
+ throw new Error("expected only fanout-child.ts to register subagent, got " + JSON.stringify(registrations));
213
+ }
214
+ `;
215
+
216
+ execFileSync(
217
+ process.execPath,
218
+ [
219
+ "--experimental-transform-types",
220
+ "--import",
221
+ "./test/support/register-loader.mjs",
222
+ "--input-type=module",
223
+ "--eval",
224
+ script,
225
+ ],
226
+ { cwd: projectRoot, stdio: "pipe" },
227
+ );
228
+ });
229
+
230
+ it("lets fanout children call read-only list but blocks mutating management actions", () => {
231
+ const script = String.raw`
232
+ import registerFanoutChildSubagentExtension from "./src/extension/fanout-child.ts";
233
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "./src/runs/shared/pi-args.ts";
234
+ process.env[SUBAGENT_CHILD_ENV] = "1";
235
+ process.env[SUBAGENT_FANOUT_CHILD_ENV] = "1";
236
+ let registeredTool;
237
+ const fakePi = {
238
+ events: { on() { return () => {}; }, emit() {} },
239
+ registerTool(tool) { registeredTool = tool; },
240
+ getSessionName() { return undefined; },
241
+ };
242
+ registerFanoutChildSubagentExtension(fakePi);
243
+ if (!registeredTool) throw new Error("tool not registered");
244
+ const ctx = {
245
+ cwd: process.cwd(),
246
+ hasUI: false,
247
+ sessionManager: { getSessionId() { return "session-test"; }, getSessionFile() { return null; } },
248
+ modelRegistry: { getAvailable() { return []; } },
249
+ };
250
+ const list = await registeredTool.execute("list-check", { action: "list" }, new AbortController().signal, undefined, ctx);
251
+ if (list.isError) throw new Error("list should be allowed: " + JSON.stringify(list.content));
252
+ const create = await registeredTool.execute("create-check", { action: "create", config: { name: "x" } }, new AbortController().signal, undefined, ctx);
253
+ if (!create.isError) throw new Error("create should be blocked");
254
+ const text = create.content?.[0]?.text ?? "";
255
+ if (!text.includes("not available from child-safe subagent fanout mode")) throw new Error("unexpected create error: " + text);
256
+ `;
257
+
258
+ execFileSync(
259
+ process.execPath,
260
+ [
261
+ "--experimental-transform-types",
262
+ "--import",
263
+ "./test/support/register-loader.mjs",
264
+ "--input-type=module",
265
+ "--eval",
266
+ script,
267
+ ],
268
+ { cwd: projectRoot, stdio: "pipe" },
269
+ );
270
+ });
271
+ });
@@ -0,0 +1,173 @@
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 { describe, it } from "node:test";
6
+ import type { AgentConfig } from "../../src/agents/agents.ts";
7
+ import {
8
+ NATIVE_INTERCOM_EXTENSION_DIR,
9
+ applyIntercomBridgeToAgent,
10
+ diagnoseIntercomBridge,
11
+ resolveIntercomBridge,
12
+ resolveIntercomSessionTarget,
13
+ resolveSubagentIntercomTarget,
14
+ resolveIntercomBridgeMode,
15
+ type IntercomBridgeState,
16
+ } from "../../src/intercom/intercom-bridge.ts";
17
+
18
+ function makeAgent(overrides: Partial<AgentConfig> = {}): AgentConfig {
19
+ return {
20
+ name: "worker",
21
+ description: "Test worker",
22
+ systemPrompt: "Base prompt",
23
+ systemPromptMode: "replace",
24
+ inheritProjectContext: false,
25
+ inheritSkills: false,
26
+ source: "user",
27
+ filePath: "/tmp/worker.md",
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ describe("resolveIntercomBridgeMode", () => {
33
+ it("defaults unknown values to always", () => {
34
+ assert.equal(resolveIntercomBridgeMode(undefined), "always");
35
+ assert.equal(resolveIntercomBridgeMode("nope"), "always");
36
+ });
37
+
38
+ it("accepts explicit modes", () => {
39
+ assert.equal(resolveIntercomBridgeMode("off"), "off");
40
+ assert.equal(resolveIntercomBridgeMode("fork-only"), "fork-only");
41
+ assert.equal(resolveIntercomBridgeMode("always"), "always");
42
+ });
43
+ });
44
+
45
+ describe("resolveIntercomSessionTarget", () => {
46
+ it("prefers an explicit session name", () => {
47
+ assert.equal(resolveIntercomSessionTarget("planner", "session-12345678"), "planner");
48
+ });
49
+
50
+ it("uses a runtime-only subagent chat alias when unnamed", () => {
51
+ assert.equal(resolveIntercomSessionTarget(undefined, "session-12345678"), "subagent-chat-12345678");
52
+ });
53
+ });
54
+
55
+ describe("resolveSubagentIntercomTarget", () => {
56
+ it("builds stable child session targets from run metadata", () => {
57
+ assert.equal(resolveSubagentIntercomTarget("78f659a3", "worker"), "subagent-worker-78f659a3");
58
+ assert.equal(resolveSubagentIntercomTarget("78f659a3", "senior executor", 1), "subagent-senior-executor-78f659a3-2");
59
+ });
60
+ });
61
+
62
+ describe("diagnoseIntercomBridge", () => {
63
+ it("reports the native supervisor channel as available without external package discovery", () => {
64
+ const diagnostic = diagnoseIntercomBridge({
65
+ config: { mode: "always" },
66
+ context: "fresh",
67
+ orchestratorTarget: "main",
68
+ });
69
+
70
+ assert.equal(diagnostic.active, true);
71
+ assert.equal(diagnostic.wantsIntercom, true);
72
+ assert.equal(diagnostic.supervisorChannelAvailable, true);
73
+ assert.equal(diagnostic.extensionDir, NATIVE_INTERCOM_EXTENSION_DIR);
74
+ });
75
+
76
+ it("does not read external intercom config when bridge mode is off", () => {
77
+ const diagnostic = diagnoseIntercomBridge({
78
+ config: { mode: "off" },
79
+ context: "fresh",
80
+ orchestratorTarget: "main",
81
+ });
82
+
83
+ assert.equal(diagnostic.active, false);
84
+ assert.equal(diagnostic.reason, "bridge mode is off");
85
+ });
86
+ });
87
+
88
+ describe("resolveIntercomBridge", () => {
89
+ it("activates when mode/context permit and an orchestrator target exists", () => {
90
+ const bridge = resolveIntercomBridge({
91
+ config: { mode: "fork-only" },
92
+ context: "fork",
93
+ orchestratorTarget: "main",
94
+ });
95
+
96
+ assert.equal(bridge.active, true);
97
+ assert.equal(bridge.orchestratorTarget, "main");
98
+ assert.equal(bridge.extensionDir, NATIVE_INTERCOM_EXTENSION_DIR);
99
+ });
100
+
101
+ it("stays inactive for fresh context when mode is fork-only", () => {
102
+ const bridge = resolveIntercomBridge({
103
+ config: { mode: "fork-only" },
104
+ context: "fresh",
105
+ orchestratorTarget: "main",
106
+ });
107
+ assert.equal(bridge.active, false);
108
+ });
109
+
110
+ it("loads custom instructions from instructionFile", () => {
111
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-native-intercom-bridge-test-"));
112
+ const instructionFile = path.join(tempDir, "bridge.md");
113
+ fs.writeFileSync(instructionFile, "Custom bridge for {orchestratorTarget}\nUse ask then send.");
114
+ try {
115
+ const bridge = resolveIntercomBridge({
116
+ config: { mode: "always", instructionFile },
117
+ context: "fresh",
118
+ orchestratorTarget: "main",
119
+ });
120
+ assert.equal(bridge.active, true);
121
+ assert.match(bridge.instruction, /Custom bridge for main/);
122
+ } finally {
123
+ fs.rmSync(tempDir, { recursive: true, force: true });
124
+ }
125
+ });
126
+
127
+ it("uses stronger default instructions for fork-aware coordination", () => {
128
+ const bridge = resolveIntercomBridge({
129
+ config: { mode: "always" },
130
+ context: "fork",
131
+ orchestratorTarget: "main",
132
+ });
133
+ assert.equal(bridge.active, true);
134
+ assert.match(bridge.instruction, /reference-only/i);
135
+ assert.match(bridge.instruction, /normal assistant text/i);
136
+ assert.match(bridge.instruction, /contact_supervisor/);
137
+ assert.match(bridge.instruction, /need_decision/);
138
+ assert.match(bridge.instruction, /progress_update/);
139
+ assert.match(bridge.instruction, /focused task result/i);
140
+ });
141
+ });
142
+
143
+ describe("applyIntercomBridgeToAgent", () => {
144
+ const activeBridge: IntercomBridgeState = {
145
+ active: true,
146
+ mode: "always",
147
+ orchestratorTarget: "main",
148
+ extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
149
+ instruction: "Intercom orchestration channel:\n- Need a decision or blocked: contact_supervisor({ reason: \"need_decision\", message: \"<question>\" })\n- Blocked/update: contact_supervisor({ reason: \"progress_update\", message: \"UPDATE: <summary>\" })",
150
+ };
151
+
152
+ it("injects intercom tool and prompt instructions", () => {
153
+ const updated = applyIntercomBridgeToAgent(makeAgent({ tools: ["read", "bash"] }), activeBridge);
154
+ assert.deepEqual(updated.tools, ["read", "bash", "intercom", "contact_supervisor"]);
155
+ assert.match(updated.systemPrompt, /Intercom orchestration channel:/);
156
+ assert.match(updated.systemPrompt, /contact_supervisor/);
157
+ });
158
+
159
+ it("is idempotent", () => {
160
+ const first = applyIntercomBridgeToAgent(makeAgent({ tools: ["read"] }), activeBridge);
161
+ const second = applyIntercomBridgeToAgent(first, activeBridge);
162
+ assert.equal(second.tools?.filter((tool) => tool === "intercom").length, 1);
163
+ assert.equal(second.tools?.filter((tool) => tool === "contact_supervisor").length, 1);
164
+ assert.equal(second.systemPrompt, first.systemPrompt);
165
+ });
166
+
167
+ it("does not block native supervisor tools for agents with explicit extension allowlists", () => {
168
+ const agent = makeAgent({ tools: ["read"], extensions: ["/tmp/other-extension/index.ts"] });
169
+ const updated = applyIntercomBridgeToAgent(agent, activeBridge);
170
+ assert.deepEqual(updated.tools, ["read", "intercom", "contact_supervisor"]);
171
+ assert.match(updated.systemPrompt, /contact_supervisor/);
172
+ });
173
+ });
@@ -0,0 +1,119 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { createJsonlWriter, type DrainableSource, type JsonlWriteStream } from "../../src/shared/jsonl-writer.ts";
4
+
5
+ class MockSource implements DrainableSource {
6
+ paused = 0;
7
+ resumed = 0;
8
+ pause(): void {
9
+ this.paused++;
10
+ }
11
+ resume(): void {
12
+ this.resumed++;
13
+ }
14
+ }
15
+
16
+ class MockStream implements JsonlWriteStream {
17
+ writes: string[] = [];
18
+ ended = false;
19
+ private drainHandler?: () => void;
20
+ private readonly writeResults: boolean[];
21
+ constructor(writeResults: boolean[] = []) {
22
+ this.writeResults = writeResults;
23
+ }
24
+ write(chunk: string): boolean {
25
+ this.writes.push(chunk);
26
+ if (this.writeResults.length === 0) return true;
27
+ return this.writeResults.shift() ?? true;
28
+ }
29
+ once(event: "drain", listener: () => void): JsonlWriteStream {
30
+ if (event === "drain") this.drainHandler = listener;
31
+ return this;
32
+ }
33
+ end(callback?: () => void): void {
34
+ this.ended = true;
35
+ callback?.();
36
+ }
37
+ emitDrain(): void {
38
+ this.drainHandler?.();
39
+ }
40
+ }
41
+
42
+ describe("createJsonlWriter", () => {
43
+ it("writes lines with trailing newline", () => {
44
+ const source = new MockSource();
45
+ const stream = new MockStream();
46
+ const writer = createJsonlWriter("/tmp/out.jsonl", source, {
47
+ createWriteStream: () => stream,
48
+ });
49
+ writer.writeLine('{"type":"a"}');
50
+ writer.writeLine('{"type":"b"}');
51
+ assert.deepEqual(stream.writes, ['{"type":"a"}\n', '{"type":"b"}\n']);
52
+ });
53
+
54
+ it("pauses on backpressure and resumes on drain", () => {
55
+ const source = new MockSource();
56
+ const stream = new MockStream([false, true]);
57
+ const writer = createJsonlWriter("/tmp/out.jsonl", source, {
58
+ createWriteStream: () => stream,
59
+ });
60
+ writer.writeLine('{"type":"a"}');
61
+ assert.equal(source.paused, 1);
62
+ assert.equal(source.resumed, 0);
63
+ stream.emitDrain();
64
+ assert.equal(source.resumed, 1);
65
+ writer.writeLine('{"type":"b"}');
66
+ assert.deepEqual(stream.writes, ['{"type":"a"}\n', '{"type":"b"}\n']);
67
+ });
68
+
69
+ it("closes stream once", async () => {
70
+ const source = new MockSource();
71
+ const stream = new MockStream();
72
+ const writer = createJsonlWriter("/tmp/out.jsonl", source, {
73
+ createWriteStream: () => stream,
74
+ });
75
+ await writer.close();
76
+ assert.equal(stream.ended, true);
77
+ await writer.close();
78
+ assert.equal(stream.ended, true);
79
+ });
80
+
81
+ it("returns no-op writer when file path is undefined", async () => {
82
+ const source = new MockSource();
83
+ const writer = createJsonlWriter(undefined, source);
84
+ writer.writeLine('{"type":"a"}');
85
+ await writer.close();
86
+ assert.equal(source.paused, 0);
87
+ assert.equal(source.resumed, 0);
88
+ });
89
+
90
+ it("stops writing when maxBytes exceeded without pausing source", () => {
91
+ const source = new MockSource();
92
+ const stream = new MockStream();
93
+ const writer = createJsonlWriter("/tmp/out.jsonl", source, {
94
+ createWriteStream: () => stream,
95
+ maxBytes: 30,
96
+ });
97
+ writer.writeLine('{"type":"a"}');
98
+ writer.writeLine('{"type":"b"}');
99
+ writer.writeLine('{"type":"c"}');
100
+ assert.equal(stream.writes.length, 2);
101
+ assert.deepEqual(stream.writes, ['{"type":"a"}\n', '{"type":"b"}\n']);
102
+ assert.equal(source.paused, 0);
103
+ });
104
+
105
+ it("allows writes up to exactly maxBytes", () => {
106
+ const source = new MockSource();
107
+ const stream = new MockStream();
108
+ const line = '{"x":"a"}';
109
+ const lineBytes = Buffer.byteLength(`${line}\n`, "utf-8");
110
+ const writer = createJsonlWriter("/tmp/out.jsonl", source, {
111
+ createWriteStream: () => stream,
112
+ maxBytes: lineBytes * 2,
113
+ });
114
+ writer.writeLine(line);
115
+ writer.writeLine(line);
116
+ writer.writeLine(line);
117
+ assert.equal(stream.writes.length, 2);
118
+ });
119
+ });