@desplega.ai/agent-swarm 1.166.0 → 1.168.0

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 (221) hide show
  1. package/dist/acp-adapter-zhejfecx.js +34 -0
  2. package/dist/{actions-qz3m692w.js → actions-7jg840nr.js} +25 -25
  3. package/dist/{amp-adapter-j1xbmsqz.js → amp-adapter-ye6rnetm.js} +4 -4
  4. package/dist/{anthropic-messages-hst62ws9.js → anthropic-messages-13yn502s.js} +4 -4
  5. package/dist/{app-43vrdmv8.js → app-ffes2kyf.js} +10 -9
  6. package/dist/{assistant-ertffmka.js → assistant-fhm5smyz.js} +26 -26
  7. package/dist/{azure-openai-responses-mh8tvf78.js → azure-openai-responses-9st34pfz.js} +5 -5
  8. package/dist/{boot-reembed-998jtpdg.js → boot-reembed-20q3nzvg.js} +9 -8
  9. package/dist/{boot-reembed-s7k7c6v2.js → boot-reembed-jvfadhsk.js} +9 -8
  10. package/dist/{boot-scrub-sweep-nepsk4t1.js → boot-scrub-sweep-jtfajzve.js} +10 -9
  11. package/dist/{boot-seeds-vvzs1zhk.js → boot-seeds-9qt0j8j2.js} +12 -11
  12. package/dist/{claude-adapter-2zvmekrp.js → claude-adapter-a8znhthz.js} +24 -22
  13. package/dist/{claude-managed-adapter-s49wbk8t.js → claude-managed-adapter-tzwxy803.js} +4 -4
  14. package/dist/{claude-managed-setup-cvhqq8qn.js → claude-managed-setup-nrhfx5b0.js} +3 -3
  15. package/dist/{claude-sdk-session-kkbxtw5q.js → claude-sdk-session-7x7chdrs.js} +7608 -7596
  16. package/dist/{cli-zsh093hk.js → cli-014vns2y.js} +63 -1
  17. package/dist/{cli-8kgvmkak.js → cli-0jngabtb.js} +1 -1
  18. package/dist/{cli-qvfk83fy.js → cli-13xwcqvj.js} +2 -2
  19. package/dist/{cli-0nhdwxd5.js → cli-43jg9xdw.js} +3 -3
  20. package/dist/{cli-cpfmqz3n.js → cli-47zfzshm.js} +5 -5
  21. package/dist/{cli-zy7vwsne.js → cli-5qkcdv8c.js} +16 -9
  22. package/dist/{cli-cwxhbf43.js → cli-5t5my0h8.js} +85 -2
  23. package/dist/{cli-y59h2pk9.js → cli-69egj654.js} +1 -1
  24. package/dist/{cli-68r1j4sg.js → cli-6bpvv8h4.js} +3 -3
  25. package/dist/{cli-yshaazs1.js → cli-74ez9b66.js} +63 -20
  26. package/dist/{cli-32bca7nr.js → cli-7avy4v8w.js} +1 -1
  27. package/dist/{cli-f2n17zhp.js → cli-7mhy3ths.js} +21 -18
  28. package/dist/{cli-q38n4bq0.js → cli-95zs84qw.js} +3 -3
  29. package/dist/{cli-fn697bnt.js → cli-9q0bh411.js} +21 -8
  30. package/dist/{cli-rggdq8wq.js → cli-9q24yyvs.js} +3 -3
  31. package/dist/cli-a35hzdve.js +33 -0
  32. package/dist/{cli-3qn4r334.js → cli-aj1e0h0d.js} +20 -14
  33. package/dist/{cli-rsy933yz.js → cli-bcspch6h.js} +30 -19
  34. package/dist/{cli-x3x0qwky.js → cli-bxqez1p0.js} +51 -23
  35. package/dist/{cli-ng97fjq7.js → cli-cmxkbzzs.js} +33 -7
  36. package/dist/{cli-df56tqdw.js → cli-ee0402n1.js} +198 -70
  37. package/dist/{cli-mwegs4af.js → cli-ew0c3a1j.js} +3 -3
  38. package/dist/{cli-ysnyhws9.js → cli-ewhwpyrb.js} +54 -51
  39. package/dist/{cli-j2jbr8af.js → cli-fhj4y16f.js} +1 -1
  40. package/dist/{cli-8t229cmr.js → cli-fqq06kkb.js} +30 -17
  41. package/dist/{cli-jvj6a0cv.js → cli-g6gx2dn1.js} +54 -19
  42. package/dist/{cli-nexwh8zg.js → cli-g7k7zbcz.js} +12 -5
  43. package/dist/{cli-ecknp7p1.js → cli-j05x7pnb.js} +10 -10
  44. package/dist/{cli-gt8azyet.js → cli-jb931ba2.js} +1 -1
  45. package/dist/{cli-nvvfd8bq.js → cli-jbzbr36n.js} +4091 -3793
  46. package/dist/{cli-3ddybbbx.js → cli-jvmtxqtc.js} +26 -14
  47. package/dist/{cli-pkke6geq.js → cli-km4x66j0.js} +83 -65
  48. package/dist/{cli-b0f70z24.js → cli-mn8qkd8v.js} +1 -1
  49. package/dist/{cli-0qjcsxgz.js → cli-nctfv2yy.js} +99 -9
  50. package/dist/{cli-4r1sjfc4.js → cli-ng1pmdx6.js} +10 -10
  51. package/dist/cli-nkqjj42q.js +104 -0
  52. package/dist/{cli-aqcey741.js → cli-p5jqk8fr.js} +1 -1
  53. package/dist/{cli-66zh26sp.js → cli-pgehz8e1.js} +57 -17
  54. package/dist/cli-q1vnz8wv.js +96 -0
  55. package/dist/{cli-fj86xr3c.js → cli-qd97v6z2.js} +2 -2
  56. package/dist/{cli-p4ztb80h.js → cli-r7eeddkq.js} +13 -8
  57. package/dist/{cli-8ssjez7e.js → cli-rhy7z1rp.js} +22 -11
  58. package/dist/{cli-vzc73wet.js → cli-rr5z1j4p.js} +87 -69
  59. package/dist/{cli-4mevd1hf.js → cli-s9y81s7j.js} +5 -5
  60. package/dist/{cli-dw2q9f7g.js → cli-svah6b6w.js} +4 -4
  61. package/dist/{cli-h3x613ze.js → cli-tc5zec7v.js} +6 -6
  62. package/dist/{cli-ttbsse3f.js → cli-tmhsxhb8.js} +1 -1
  63. package/dist/{cli-feh3dkah.js → cli-v1w7pbd8.js} +2 -1
  64. package/dist/{cli-10zmp1p7.js → cli-vr5avem2.js} +27 -32
  65. package/dist/{cli-bzr0ftht.js → cli-wnb9b2g3.js} +19 -0
  66. package/dist/{cli-krqm5f11.js → cli-xezn9nkh.js} +16 -13
  67. package/dist/{cli-8kt0bmgx.js → cli-xf713zz0.js} +2 -2
  68. package/dist/{cli-6zcfw0x7.js → cli-y195hv1a.js} +1 -1
  69. package/dist/{cli-x7kpr72a.js → cli-ytmmyj7f.js} +2 -2
  70. package/dist/{cli-v2a8x45d.js → cli-z48ggfyq.js} +11 -6
  71. package/dist/{cli-qbksc292.js → cli-zy39d433.js} +2 -0
  72. package/dist/cli.js +28 -28
  73. package/dist/{cloudflare-workers-ai-system-one-tt484t61.js → cloudflare-workers-ai-system-one-sta45jf9.js} +7 -5
  74. package/dist/{codex-adapter-dw1wxqt8.js → codex-adapter-n63k33d3.js} +18 -18
  75. package/dist/{codex-login-terbdsqq.js → codex-login-g1cyt5zm.js} +1 -1
  76. package/dist/{codex-session-runner-3cgb8y1g.js → codex-session-runner-xkxfd81a.js} +18 -18
  77. package/dist/{commands-mhxam6z2.js → commands-q4mfhn51.js} +9 -8
  78. package/dist/{cursor-adapter-2qda3st7.js → cursor-adapter-v8mm6bp5.js} +5 -5
  79. package/dist/{db-sjks418k.js → db-1t0bj3pc.js} +9 -8
  80. package/dist/{dispatcher-1es759rn.js → dispatcher-k1fhavcf.js} +24 -24
  81. package/dist/{dsh-adapter-bek4cyw9.js → dsh-adapter-5423p70m.js} +5 -5
  82. package/dist/{easter-egg-3d-mtpnw06s.js → easter-egg-3d-66zxervv.js} +2 -2
  83. package/dist/{execute-s4ccjq3n.js → execute-h4jgevyw.js} +61 -11
  84. package/dist/{google-generative-ai-d6nphg3q.js → google-generative-ai-b8zxrpax.js} +5 -5
  85. package/dist/{google-vertex-cpshkref.js → google-vertex-nrdd697t.js} +5 -5
  86. package/dist/{grok-adapter-ar9cw9jg.js → grok-adapter-phrndeb8.js} +18 -8
  87. package/dist/{handlers-1gkbhktr.js → handlers-59ywqwzh.js} +27 -27
  88. package/dist/{hook-vxa3tcsy.js → hook-atj0swc2.js} +14 -14
  89. package/dist/{hook-ds2zxq9v.js → hook-mew1chq9.js} +42 -31
  90. package/dist/{http-3h21z9md.js → http-ef2fnn9v.js} +953 -98
  91. package/dist/{index-n99fycq7.js → index-48pb9erw.js} +24 -24
  92. package/dist/{index-bn6cy8ps.js → index-9fc2h7ww.js} +24 -24
  93. package/dist/{index-9b65j1m5.js → index-gr69c43q.js} +9 -8
  94. package/dist/{index-f8xnqrye.js → index-h26wjcfy.js} +43 -24
  95. package/dist/{index-dkfxpkmm.js → index-w2yhye23.js} +25 -25
  96. package/dist/{keepalive-3xspa9jw.js → keepalive-qhcqz56v.js} +11 -10
  97. package/dist/{lead-6qphc124.js → lead-hpk5r3b1.js} +47 -47
  98. package/dist/{llama-cpp-classify-tgj35k8d.js → llama-cpp-classify-6erb7wwv.js} +3 -1
  99. package/dist/{maintenance-0teem1bb.js → maintenance-nvkyp2xe.js} +9 -8
  100. package/dist/{mistral-conversations-8445ddjn.js → mistral-conversations-059y51zt.js} +4 -4
  101. package/dist/{node-9sg7ajby.js → node-qm9eb6z0.js} +1 -1
  102. package/dist/{oauth-refresh-sweep-sh9q25xp.js → oauth-refresh-sweep-ngskthwh.js} +10 -9
  103. package/dist/{onboard-54qmnqxe.js → onboard-yfnkqhxc.js} +2 -2
  104. package/dist/{openai-codex-responses-9ynm88zy.js → openai-codex-responses-jzxkta9b.js} +8 -7
  105. package/dist/{openai-completions-nhqwjmaw.js → openai-completions-663yncfq.js} +4 -4
  106. package/dist/openai-decisions-vj9nkpf8.js +162 -0
  107. package/dist/{openai-responses-xx1c4kgm.js → openai-responses-m3g43rrt.js} +5 -5
  108. package/dist/{opencode-adapter-wjfa2wvd.js → opencode-adapter-yjyqynzf.js} +7 -7
  109. package/dist/{openrouter-images-8yx3487g.js → openrouter-images-kr60n6b9.js} +1 -1
  110. package/dist/{otel-impl-ycqzp2a1.js → otel-impl-hhm64r6m.js} +1 -1
  111. package/dist/{pi-messages-vxdt8v7j.js → pi-messages-xszk613n.js} +1 -1
  112. package/dist/{pi-mono-adapter-e77e9zst.js → pi-mono-adapter-0eg8kw2c.js} +68 -51
  113. package/dist/{pricing-refresh-atnkngqs.js → pricing-refresh-35f91nx2.js} +11 -10
  114. package/dist/{rbac-roles-j78wdksm.js → rbac-roles-97w370qj.js} +10 -9
  115. package/dist/{rbac-roles-9pjphh9q.js → rbac-roles-z6sje3gy.js} +9 -8
  116. package/dist/{render-v2-pxjs49fx.js → render-v2-n4v66bj3.js} +10 -9
  117. package/dist/{runtime-v2t1r79e.js → runtime-h9ha1mq6.js} +4 -4
  118. package/dist/{setup-ymej0xtx.js → setup-m3hhyv5f.js} +2 -2
  119. package/dist/{typesafe-system-one-datfr82p.js → typesafe-system-one-e988vty7.js} +7 -5
  120. package/dist/{virtual-modules-wm8mxdxx.js → virtual-modules-ftr9kwe8.js} +11 -11
  121. package/dist/{work-objects-3nh4ymzb.js → work-objects-mxf2c842.js} +27 -27
  122. package/dist/{worker-qa9fkz5s.js → worker-9t07z1gq.js} +47 -47
  123. package/openapi.json +72 -1
  124. package/package.json +10 -10
  125. package/src/be/memory/providers/openai-embedding.ts +7 -2
  126. package/src/be/memory/raters/llm-summarizer.ts +7 -2
  127. package/src/be/modelsdev-cache.json +61 -0
  128. package/src/be/script-connections.ts +40 -6
  129. package/src/be/seed-pricing.ts +84 -53
  130. package/src/be/swarm-config-guard.ts +14 -0
  131. package/src/commands/provider-credentials.ts +28 -5
  132. package/src/commands/runner.ts +7 -12
  133. package/src/extensions/catalog.generated.json +17 -0
  134. package/src/heartbeat/heartbeat.ts +91 -19
  135. package/src/hooks/hook.ts +72 -27
  136. package/src/http/mcp.ts +5 -0
  137. package/src/http/onboarding.ts +2 -0
  138. package/src/http/script-connections.ts +35 -0
  139. package/src/providers/acp-targets.ts +10 -5
  140. package/src/providers/amp-adapter.ts +1 -1
  141. package/src/providers/claude-adapter.ts +32 -0
  142. package/src/providers/claude-background-keepalive.ts +100 -0
  143. package/src/providers/claude-sdk-session.ts +6 -0
  144. package/src/providers/codex-adapter.ts +30 -3
  145. package/src/providers/codex-oauth/auth-json.ts +21 -4
  146. package/src/providers/codex-oauth/types.ts +2 -0
  147. package/src/providers/dsh-adapter.ts +11 -2
  148. package/src/providers/grok-adapter.ts +18 -2
  149. package/src/providers/opencode-adapter.ts +21 -5
  150. package/src/providers/pi-mono-extension.ts +56 -25
  151. package/src/providers/pricing-sources.md +2 -0
  152. package/src/scheduler/scheduler.ts +82 -13
  153. package/src/scripts-runtime/executors/registry.ts +1 -1
  154. package/src/server.ts +63 -3
  155. package/src/tests/acp-adapter.test.ts +152 -12
  156. package/src/tests/ci-unit-test-report.test.ts +73 -0
  157. package/src/tests/claude-background-keepalive-wiring.test.ts +220 -0
  158. package/src/tests/claude-background-keepalive.fixture.ts +162 -0
  159. package/src/tests/claude-background-keepalive.test.ts +139 -0
  160. package/src/tests/claude-default-route.test.ts +75 -0
  161. package/src/tests/claude-hook-output.test.ts +233 -0
  162. package/src/tests/codex-adapter-pool-auth-revalidate.test.ts +4 -2
  163. package/src/tests/codex-adapter.test.ts +166 -26
  164. package/src/tests/codex-oauth-auth-timestamps.test.ts +76 -0
  165. package/src/tests/codex-oauth.test.ts +4 -1
  166. package/src/tests/connection-embedded-auth.test.ts +51 -0
  167. package/src/tests/context-window.test.ts +2 -2
  168. package/src/tests/credential-check.test.ts +51 -1
  169. package/src/tests/docs-index-footprint.test.js +3 -3
  170. package/src/tests/dsh-adapter.test.ts +24 -2
  171. package/src/tests/enabled-tools.test.ts +269 -0
  172. package/src/tests/entrypoint-codex-oauth-seed.test.ts +12 -0
  173. package/src/tests/extensions-catalog.test.ts +1 -0
  174. package/src/tests/extensions-example-slack-reply-gate.test.ts +756 -0
  175. package/src/tests/grok-adapter.test.ts +13 -0
  176. package/src/tests/haiku-5-5-pricing.test.ts +38 -0
  177. package/src/tests/heartbeat-checklist.test.ts +158 -1
  178. package/src/tests/onboarding.test.ts +44 -0
  179. package/src/tests/opencode-adapter.test.ts +66 -3
  180. package/src/tests/opencode-plugin.test.ts +37 -0
  181. package/src/tests/openrouter-attribution-call-sites.test.ts +204 -0
  182. package/src/tests/openrouter-base-url.test.ts +30 -39
  183. package/src/tests/pi-mono-extension.test.ts +161 -2
  184. package/src/tests/pricing-refresh.test.ts +70 -1
  185. package/src/tests/reasoning-effort.test.ts +3 -1
  186. package/src/tests/schedule-target-type.test.ts +136 -0
  187. package/src/tests/script-connections-http.test.ts +45 -0
  188. package/src/tests/script-connections-mcp.test.ts +41 -0
  189. package/src/tests/script-executor-registry.test.ts +18 -0
  190. package/src/tests/send-task-self-send.test.ts +107 -0
  191. package/src/tests/telemetry-trigger.test.ts +12 -1
  192. package/src/tests/workflow-llm.test.ts +14 -0
  193. package/src/tests/workflow-system-one-decision-providers.test.ts +33 -0
  194. package/src/tools/request-human-input.ts +7 -2
  195. package/src/tools/script-connections/tool.ts +35 -3
  196. package/src/tools/send-task.ts +9 -1
  197. package/src/utils/enabled-tools.ts +27 -0
  198. package/src/utils/internal-ai/complete-structured.ts +11 -3
  199. package/src/utils/openrouter-base-url.ts +23 -0
  200. package/src/workflows/executors/raw-llm.ts +1 -0
  201. package/src/workflows/executors/system-one-decision.ts +2 -0
  202. package/src/workflows/executors/validate.ts +1 -0
  203. package/src/workflows/executors/workflow-llm.ts +9 -2
  204. package/templates/extensions/README.md +1 -0
  205. package/templates/extensions/slack-reply-gate/README.md +98 -0
  206. package/templates/extensions/slack-reply-gate/hooks.ts +582 -0
  207. package/templates/extensions/slack-reply-gate/manifest.json +8 -0
  208. package/templates/skills/agent-browser/SKILL.md +2 -0
  209. package/templates/skills/agent-browser/content.md +2 -0
  210. package/templates/skills/code-quality/content.md +1 -0
  211. package/templates/skills/heartbeat-runbook/content.md +1 -1
  212. package/templates/skills/swarm-scripts/SKILL.md +10 -0
  213. package/templates/skills/swarm-scripts/content.md +10 -0
  214. package/templates/skills/work-on-task/SKILL.md +4 -0
  215. package/templates/skills/work-on-task/content.md +4 -0
  216. package/templates/skills/workflow-iterate/content.md +9 -0
  217. package/dist/acp-adapter-yzfxn8vj.js +0 -23
  218. package/dist/cli-3dpj7atn.js +0 -11
  219. package/dist/cli-gvcgmx69.js +0 -183
  220. package/dist/{cli-jhm6hzjn.js → cli-mxvnnv5q.js} +7 -7
  221. package/dist/{codex-hook-d2rkvf39.js → codex-hook-15dgc4cb.js} +1 -1
@@ -0,0 +1,162 @@
1
+ // Fake Claude binary for the background-keepalive wiring tests. It speaks just
2
+ // enough of both transports: the SDK initialize handshake on stdin, and the
3
+ // stream-json stdout that the CLI and SDK sessions both parse. After one turn it
4
+ // goes silent, the way Claude does while it waits on a background job, until the
5
+ // test steps it through the control file (one command per line).
6
+ const transport = process.env.BG_FIXTURE_TRANSPORT;
7
+ const controlFile = process.env.BG_FIXTURE_CONTROL;
8
+ const withBackgroundTask = process.env.BG_FIXTURE_BACKGROUND === "1";
9
+ const sessionId = "22222222-2222-4222-8222-222222222222";
10
+
11
+ if (process.argv.includes("--version")) {
12
+ console.log("2.1.263 (Claude Code)");
13
+ process.exit(0);
14
+ }
15
+
16
+ if ((transport !== "cli" && transport !== "sdk") || !controlFile) {
17
+ console.error("BG_FIXTURE_TRANSPORT (cli|sdk) and BG_FIXTURE_CONTROL are required");
18
+ process.exit(2);
19
+ }
20
+
21
+ function writeMessage(message: Record<string, unknown>): void {
22
+ console.log(JSON.stringify(message));
23
+ }
24
+
25
+ function backgroundTasksChanged(tasks: Array<Record<string, unknown>>): void {
26
+ writeMessage({
27
+ type: "system",
28
+ subtype: "background_tasks_changed",
29
+ tasks,
30
+ uuid: crypto.randomUUID(),
31
+ session_id: sessionId,
32
+ });
33
+ }
34
+
35
+ const usage = {
36
+ input_tokens: 10,
37
+ output_tokens: 1,
38
+ cache_read_input_tokens: 0,
39
+ cache_creation_input_tokens: 0,
40
+ };
41
+
42
+ let started = false;
43
+
44
+ async function runTurn(): Promise<void> {
45
+ if (started) return;
46
+ started = true;
47
+ writeMessage({
48
+ type: "system",
49
+ subtype: "init",
50
+ session_id: sessionId,
51
+ model: "claude-haiku-4-5",
52
+ tools: [],
53
+ mcp_servers: [],
54
+ plugins: [],
55
+ permissionMode: "bypassPermissions",
56
+ slash_commands: [],
57
+ apiKeySource: "none",
58
+ cwd: process.cwd(),
59
+ claude_code_version: "2.1.263",
60
+ output_style: "default",
61
+ skills: [],
62
+ uuid: crypto.randomUUID(),
63
+ });
64
+ if (withBackgroundTask) {
65
+ backgroundTasksChanged([
66
+ { task_id: "bg-1", task_type: "local_bash", description: "Wait for TLC" },
67
+ ]);
68
+ }
69
+ writeMessage({
70
+ type: "assistant",
71
+ message: {
72
+ id: "message-1",
73
+ type: "message",
74
+ role: "assistant",
75
+ model: "claude-haiku-4-5",
76
+ content: [{ type: "text", text: "Waiting for the background job to finish." }],
77
+ stop_reason: "end_turn",
78
+ stop_sequence: null,
79
+ usage,
80
+ },
81
+ parent_tool_use_id: null,
82
+ uuid: crypto.randomUUID(),
83
+ session_id: sessionId,
84
+ });
85
+ writeMessage({
86
+ type: "result",
87
+ subtype: "success",
88
+ duration_ms: 1,
89
+ duration_api_ms: 1,
90
+ is_error: false,
91
+ num_turns: 1,
92
+ result: "Waiting for the background job to finish.",
93
+ stop_reason: "end_turn",
94
+ total_cost_usd: 0.001,
95
+ usage,
96
+ modelUsage: {},
97
+ permission_denials: [],
98
+ queued_turn_count: 0,
99
+ uuid: crypto.randomUUID(),
100
+ session_id: sessionId,
101
+ });
102
+
103
+ // Silent from here on, except for what the test asks for.
104
+ let handled = 0;
105
+ for (;;) {
106
+ const file = Bun.file(controlFile as string);
107
+ const commands = (await file.exists()) ? (await file.text()).split("\n").filter(Boolean) : [];
108
+ while (handled < commands.length) {
109
+ const command = commands[handled++];
110
+ if (command === "empty") backgroundTasksChanged([]);
111
+ if (command === "exit") process.exit(0);
112
+ if (command === "fail") {
113
+ console.error("Error: fixture process crashed");
114
+ process.exit(1);
115
+ }
116
+ }
117
+ await Bun.sleep(10);
118
+ }
119
+ }
120
+
121
+ async function readSdkInput(): Promise<void> {
122
+ const decoder = new TextDecoder();
123
+ let buffer = "";
124
+ for await (const chunk of Bun.stdin.stream()) {
125
+ buffer += decoder.decode(chunk, { stream: true });
126
+ let newline = buffer.indexOf("\n");
127
+ while (newline >= 0) {
128
+ const line = buffer.slice(0, newline).trim();
129
+ buffer = buffer.slice(newline + 1);
130
+ newline = buffer.indexOf("\n");
131
+ if (!line) continue;
132
+ const message = JSON.parse(line) as {
133
+ type?: string;
134
+ request_id?: string;
135
+ request?: { subtype?: string };
136
+ };
137
+ if (message.type === "control_request" && message.request?.subtype === "initialize") {
138
+ writeMessage({
139
+ type: "control_response",
140
+ response: {
141
+ subtype: "success",
142
+ request_id: message.request_id ?? "fixture-initialize",
143
+ response: {
144
+ commands: [],
145
+ agents: [],
146
+ output_style: "default",
147
+ available_output_styles: [],
148
+ models: [],
149
+ account: {},
150
+ },
151
+ },
152
+ });
153
+ } else if (message.type === "user") {
154
+ void runTurn();
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ // The CLI transport passes the prompt with -p; the SDK sends it on stdin.
161
+ if (transport === "sdk") void readSdkInput();
162
+ else await runTurn();
@@ -0,0 +1,139 @@
1
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { ClaudeBackgroundKeepalive } from "../providers/claude-background-keepalive";
3
+
4
+ // The keepalive's contract is the HTTP heartbeat it sends while Claude idles on
5
+ // background work. A local server records those requests; short intervals keep
6
+ // the timing real without fake clocks.
7
+ const INTERVAL_MS = 20;
8
+ // Real timers under CI's parallel load can slip a tick; retry instead of
9
+ // widening every window.
10
+ const TIMING = { retry: 2 };
11
+ const received: Array<{ method: string; path: string; auth: string | null; agent: string | null }> =
12
+ [];
13
+ const server = Bun.serve({
14
+ port: 0,
15
+ fetch(req) {
16
+ const url = new URL(req.url);
17
+ received.push({
18
+ method: req.method,
19
+ path: url.pathname,
20
+ auth: req.headers.get("authorization"),
21
+ agent: req.headers.get("x-agent-id"),
22
+ });
23
+ return Response.json({ updated: true });
24
+ },
25
+ });
26
+
27
+ afterAll(() => server.stop(true));
28
+ beforeEach(() => {
29
+ received.length = 0;
30
+ });
31
+
32
+ function keepalive(maxIdleMs?: number, apiUrl = `http://127.0.0.1:${server.port}`) {
33
+ return new ClaudeBackgroundKeepalive({
34
+ apiUrl,
35
+ apiKey: "key-123",
36
+ agentId: "agent-1",
37
+ taskId: "task-1",
38
+ intervalMs: INTERVAL_MS,
39
+ maxIdleMs,
40
+ });
41
+ }
42
+
43
+ const bgChanged = (tasks: Array<Record<string, unknown>>) => ({
44
+ type: "system",
45
+ subtype: "background_tasks_changed",
46
+ tasks,
47
+ });
48
+
49
+ describe("ClaudeBackgroundKeepalive", () => {
50
+ test(
51
+ "keeps heartbeating after the turn ends while a background Bash job is live",
52
+ async () => {
53
+ const k = keepalive();
54
+ // Message order from the stalled run: the backgrounded Bash joins the set,
55
+ // the assistant ends its turn, then the process goes silent.
56
+ k.observe(
57
+ bgChanged([{ task_id: "bscveebhn", task_type: "local_bash", description: "Wait for TLC" }]),
58
+ );
59
+ k.observe({ type: "assistant" });
60
+ k.observe({ type: "result", subtype: "success" });
61
+ await Bun.sleep(INTERVAL_MS * 6);
62
+ k.stop();
63
+
64
+ expect(received.length).toBeGreaterThanOrEqual(3);
65
+ expect(received[0]).toEqual({
66
+ method: "PUT",
67
+ path: "/api/active-sessions/heartbeat/task-1",
68
+ auth: "Bearer key-123",
69
+ agent: "agent-1",
70
+ });
71
+ },
72
+ TIMING,
73
+ );
74
+
75
+ test(
76
+ "a silent session with no background work sends nothing, so it still goes stale",
77
+ async () => {
78
+ const k = keepalive();
79
+ k.observe({ type: "assistant" });
80
+ k.observe({ type: "result", subtype: "success" });
81
+ // Ambient watchers are not activity either.
82
+ k.observe(bgChanged([{ task_id: "watcher", ambient: true }]));
83
+ await Bun.sleep(INTERVAL_MS * 5);
84
+ k.stop();
85
+ expect(received).toHaveLength(0);
86
+ },
87
+ TIMING,
88
+ );
89
+
90
+ test(
91
+ "stops when the background set empties",
92
+ async () => {
93
+ const k = keepalive();
94
+ k.observe(bgChanged([{ task_id: "a" }, { task_id: "b" }]));
95
+ await Bun.sleep(INTERVAL_MS * 3);
96
+ k.observe(bgChanged([]));
97
+ await Bun.sleep(INTERVAL_MS);
98
+ const settled = received.length;
99
+ expect(settled).toBeGreaterThan(0);
100
+ await Bun.sleep(INTERVAL_MS * 4);
101
+ k.stop();
102
+ expect(received).toHaveLength(settled);
103
+ },
104
+ TIMING,
105
+ );
106
+
107
+ test(
108
+ "a background job that never settles is covered only up to the idle cap",
109
+ async () => {
110
+ const k = keepalive(INTERVAL_MS * 3);
111
+ k.observe(bgChanged([{ task_id: "hung" }]));
112
+ await Bun.sleep(INTERVAL_MS * 6);
113
+ const capped = received.length;
114
+ expect(capped).toBeGreaterThan(0);
115
+ await Bun.sleep(INTERVAL_MS * 4);
116
+ expect(received).toHaveLength(capped);
117
+
118
+ // New output from the session restarts coverage while the job is live.
119
+ k.observe({ type: "assistant" });
120
+ await Bun.sleep(INTERVAL_MS * 2);
121
+ k.stop();
122
+ expect(received.length).toBeGreaterThan(capped);
123
+ },
124
+ TIMING,
125
+ );
126
+
127
+ test(
128
+ "stop() ends the keepalive and ignores later messages",
129
+ async () => {
130
+ const k = keepalive();
131
+ k.observe(bgChanged([{ task_id: "a" }]));
132
+ k.stop();
133
+ k.observe(bgChanged([{ task_id: "b" }]));
134
+ await Bun.sleep(INTERVAL_MS * 4);
135
+ expect(received).toHaveLength(0);
136
+ },
137
+ TIMING,
138
+ );
139
+ });
@@ -12,6 +12,7 @@ import {
12
12
  import {
13
13
  buildClaudeSessionEnvironment,
14
14
  checkClaudeCredentials,
15
+ claudeOpenRouterAttributionEnv,
15
16
  resolveClaudeBinaryArgv,
16
17
  withClaudeRouteEnv,
17
18
  } from "../providers/claude-adapter";
@@ -258,3 +259,77 @@ describe("internal-ai credential", () => {
258
259
  expect(cred?.kind).toBe("anthropic");
259
260
  });
260
261
  });
262
+
263
+ describe("claude OpenRouter attribution", () => {
264
+ const OPENROUTER = {
265
+ ANTHROPIC_BASE_URL: "https://openrouter.ai/api",
266
+ ANTHROPIC_AUTH_TOKEN: "sk-or",
267
+ };
268
+ const ATTRIBUTION = [
269
+ "HTTP-Referer: https://agent-swarm.dev",
270
+ "X-OpenRouter-Title: Agent Swarm",
271
+ "X-OpenRouter-Categories: personal-agent,cloud-agent",
272
+ ];
273
+ const spawnEnv = (env: Record<string, string>) =>
274
+ buildClaudeSessionEnvironment(
275
+ {
276
+ taskId: crypto.randomUUID(),
277
+ agentId: crypto.randomUUID(),
278
+ env,
279
+ apiUrl: "http://fixture.invalid",
280
+ apiKey: "example-fixture-swarm-key",
281
+ } as ProviderSessionConfig,
282
+ "sonnet",
283
+ "/tmp/fixture-task",
284
+ ).env;
285
+
286
+ test("an openrouter.ai base URL appends the attribution headers", () => {
287
+ expect(spawnEnv(OPENROUTER).ANTHROPIC_CUSTOM_HEADERS).toBe(ATTRIBUTION.join("\n"));
288
+ });
289
+
290
+ test("the operator's header lines are kept in order; ours replace same-name lines", () => {
291
+ const env = spawnEnv({
292
+ ...OPENROUTER,
293
+ ANTHROPIC_CUSTOM_HEADERS:
294
+ "X-Gateway-Key: secret\nhttp-referer: https://mine.example\nX-Team: a",
295
+ });
296
+ expect(env.ANTHROPIC_CUSTOM_HEADERS).toBe(
297
+ ["X-Gateway-Key: secret", "X-Team: a", ...ATTRIBUTION].join("\n"),
298
+ );
299
+ });
300
+
301
+ test("unset, Anthropic, gateway and lookalike base URLs leave ANTHROPIC_CUSTOM_HEADERS byte-identical", () => {
302
+ const operator = "X-Gateway-Key: secret\n\nX-Team: a ";
303
+ for (const base of [
304
+ {},
305
+ { ANTHROPIC_BASE_URL: "https://api.anthropic.com" },
306
+ { ANTHROPIC_BASE_URL: GATEWAY_URL },
307
+ { ANTHROPIC_BASE_URL: "https://openrouter.ai.evil.example/api" },
308
+ { ANTHROPIC_BASE_URL: "not a url" },
309
+ ]) {
310
+ expect(claudeOpenRouterAttributionEnv(base)).toEqual({});
311
+ expect(
312
+ spawnEnv({ ...base, ANTHROPIC_API_KEY: "sk-ant" }).ANTHROPIC_CUSTOM_HEADERS,
313
+ ).toBeUndefined();
314
+ expect(
315
+ spawnEnv({ ...base, ANTHROPIC_API_KEY: "sk-ant", ANTHROPIC_CUSTOM_HEADERS: operator })
316
+ .ANTHROPIC_CUSTOM_HEADERS,
317
+ ).toBe(operator);
318
+ }
319
+ });
320
+
321
+ test("OPENROUTER_APP_ATTRIBUTION=false leaves the operator's value untouched", () => {
322
+ for (const off of ["false", "0", "FALSE"]) {
323
+ expect(
324
+ spawnEnv({
325
+ ...OPENROUTER,
326
+ OPENROUTER_APP_ATTRIBUTION: off,
327
+ ANTHROPIC_CUSTOM_HEADERS: "X-A: b",
328
+ }).ANTHROPIC_CUSTOM_HEADERS,
329
+ ).toBe("X-A: b");
330
+ expect(
331
+ spawnEnv({ ...OPENROUTER, OPENROUTER_APP_ATTRIBUTION: off }).ANTHROPIC_CUSTOM_HEADERS,
332
+ ).toBe(undefined);
333
+ }
334
+ });
335
+ });
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Claude Code hook stdout contract.
3
+ *
4
+ * Claude Code parses hook stdout as JSON only when the whole stdout is one
5
+ * JSON object. A plain status line before a block made the block land as
6
+ * text, and the tool call ran anyway. Plain stdout on PreToolUse/PostToolUse
7
+ * never reaches the model, so nudges on those events must be JSON
8
+ * `additionalContext`.
9
+ *
10
+ * Each test runs the real hook as a subprocess against a fake API.
11
+ */
12
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
13
+ import { mkdtemp, rm } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import pkg from "../../package.json";
17
+ import { CHILD_PROCESS_TEST_BUDGET_MS, runChild } from "./test-proc";
18
+
19
+ const SERVER_NAME = pkg.config?.name ?? "agent-swarm";
20
+ const HOOK_PATH = join(import.meta.dir, "..", "hooks", "hook.ts");
21
+
22
+ type FakeState = {
23
+ isLead: boolean;
24
+ cancelledTaskId: string | null;
25
+ shouldBlockPolling: boolean;
26
+ };
27
+
28
+ const state: FakeState = { isLead: false, cancelledTaskId: null, shouldBlockPolling: false };
29
+ let server: ReturnType<typeof Bun.serve>;
30
+ let projectDir: string;
31
+
32
+ beforeAll(async () => {
33
+ server = Bun.serve({
34
+ port: 0,
35
+ fetch(req) {
36
+ const url = new URL(req.url);
37
+ if (url.pathname === "/me") {
38
+ return Response.json({
39
+ id: "00000000-0000-4000-8000-000000000001",
40
+ name: "probe",
41
+ isLead: state.isLead,
42
+ status: "busy",
43
+ shouldBlockPolling: state.shouldBlockPolling,
44
+ inbox: {
45
+ unreadCount: 2,
46
+ mentionsCount: 0,
47
+ offeredTasksCount: 1,
48
+ poolTasksCount: 0,
49
+ inProgressCount: 1,
50
+ recentMentions: [],
51
+ },
52
+ });
53
+ }
54
+ if (url.pathname === "/cancelled-tasks") {
55
+ const cancelled = state.cancelledTaskId
56
+ ? [{ id: state.cancelledTaskId, task: "t", failureReason: "stop now" }]
57
+ : [];
58
+ return Response.json({ cancelled });
59
+ }
60
+ return Response.json({ ok: true });
61
+ },
62
+ });
63
+ projectDir = await mkdtemp(join(tmpdir(), "claude-hook-output-"));
64
+ await Bun.write(
65
+ join(projectDir, ".mcp.json"),
66
+ JSON.stringify({
67
+ mcpServers: {
68
+ [SERVER_NAME]: {
69
+ url: `http://127.0.0.1:${server.port}/mcp`,
70
+ headers: {
71
+ Authorization: "Bearer test",
72
+ "X-Agent-ID": "00000000-0000-4000-8000-000000000001",
73
+ },
74
+ },
75
+ },
76
+ }),
77
+ );
78
+ });
79
+
80
+ afterAll(async () => {
81
+ server.stop(true);
82
+ await rm(projectDir, { recursive: true, force: true });
83
+ });
84
+
85
+ async function runHook(event: Record<string, unknown>, taskId: string) {
86
+ const taskFile = join(projectDir, `task-${taskId}.json`);
87
+ await Bun.write(
88
+ taskFile,
89
+ JSON.stringify({ taskId, agentId: "a", startedAt: new Date().toISOString() }),
90
+ );
91
+ const env: Record<string, string | undefined> = {
92
+ ...process.env,
93
+ CLAUDE_PROJECT_DIR: projectDir,
94
+ TASK_FILE: taskFile,
95
+ };
96
+ return runChild(["bun", HOOK_PATH], {
97
+ cwd: projectDir,
98
+ env,
99
+ stdin: JSON.stringify({ cwd: projectDir, ...event }),
100
+ });
101
+ }
102
+
103
+ /** Assert stdout is exactly one JSON object and return it. */
104
+ function parseSoleJson(stdout: string): Record<string, unknown> {
105
+ const trimmed = stdout.trim();
106
+ expect(trimmed.split("\n")).toHaveLength(1);
107
+ return JSON.parse(trimmed) as Record<string, unknown>;
108
+ }
109
+
110
+ const bashTouch = {
111
+ tool_name: "Bash",
112
+ tool_input: { command: "touch /tmp/probe" },
113
+ };
114
+
115
+ describe("claude hook stdout", () => {
116
+ test(
117
+ "blocked PreToolUse prints exactly one deny JSON object",
118
+ async () => {
119
+ const taskId = crypto.randomUUID();
120
+ Object.assign(state, { isLead: false, cancelledTaskId: taskId, shouldBlockPolling: false });
121
+ const result = await runHook({ hook_event_name: "PreToolUse", ...bashTouch }, taskId);
122
+ expect(result.exitCode).toBe(0);
123
+ const out = parseSoleJson(result.stdout) as {
124
+ hookSpecificOutput: Record<string, string>;
125
+ };
126
+ expect(out.hookSpecificOutput.hookEventName).toBe("PreToolUse");
127
+ expect(out.hookSpecificOutput.permissionDecision).toBe("deny");
128
+ expect(out.hookSpecificOutput.permissionDecisionReason).toContain("TASK CANCELLED");
129
+ },
130
+ CHILD_PROCESS_TEST_BUDGET_MS,
131
+ );
132
+
133
+ test(
134
+ "blocked UserPromptSubmit prints only the block JSON, no status line",
135
+ async () => {
136
+ const taskId = crypto.randomUUID();
137
+ Object.assign(state, { isLead: false, cancelledTaskId: taskId, shouldBlockPolling: false });
138
+ const result = await runHook({ hook_event_name: "UserPromptSubmit", prompt: "go" }, taskId);
139
+ expect(result.exitCode).toBe(0);
140
+ const out = parseSoleJson(result.stdout);
141
+ expect(out.decision).toBe("block");
142
+ expect(String(out.reason)).toContain("TASK CANCELLED");
143
+ },
144
+ CHILD_PROCESS_TEST_BUDGET_MS,
145
+ );
146
+
147
+ test(
148
+ "UserPromptSubmit without a block still prints the status line and tray",
149
+ async () => {
150
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
151
+ const result = await runHook(
152
+ { hook_event_name: "UserPromptSubmit", prompt: "go" },
153
+ crypto.randomUUID(),
154
+ );
155
+ expect(result.stdout).toContain('You are registered as worker agent "probe"');
156
+ expect(result.stdout).toContain("📬 2 unread");
157
+ },
158
+ CHILD_PROCESS_TEST_BUDGET_MS,
159
+ );
160
+
161
+ test(
162
+ "unblocked PreToolUse prints nothing on stdout",
163
+ async () => {
164
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
165
+ const result = await runHook(
166
+ { hook_event_name: "PreToolUse", ...bashTouch },
167
+ crypto.randomUUID(),
168
+ );
169
+ expect(result.exitCode).toBe(0);
170
+ expect(result.stdout.trim()).toBe("");
171
+ },
172
+ CHILD_PROCESS_TEST_BUDGET_MS,
173
+ );
174
+
175
+ test(
176
+ "polling limit blocks poll-task with one deny JSON object",
177
+ async () => {
178
+ Object.assign(state, { isLead: true, cancelledTaskId: null, shouldBlockPolling: true });
179
+ const result = await runHook(
180
+ { hook_event_name: "PreToolUse", tool_name: "mcp__agent-swarm__poll-task", tool_input: {} },
181
+ crypto.randomUUID(),
182
+ );
183
+ const out = parseSoleJson(result.stdout) as {
184
+ hookSpecificOutput: Record<string, string>;
185
+ };
186
+ expect(out.hookSpecificOutput.permissionDecision).toBe("deny");
187
+ expect(out.hookSpecificOutput.permissionDecisionReason).toContain("POLLING LIMIT");
188
+ },
189
+ CHILD_PROCESS_TEST_BUDGET_MS,
190
+ );
191
+
192
+ test(
193
+ "loop warning reaches the model as PreToolUse additionalContext",
194
+ async () => {
195
+ Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
196
+ const taskId = crypto.randomUUID();
197
+ let stdout = "";
198
+ try {
199
+ for (let i = 0; i < 14 && stdout.trim() === ""; i++) {
200
+ stdout = (await runHook({ hook_event_name: "PreToolUse", ...bashTouch }, taskId)).stdout;
201
+ }
202
+ } finally {
203
+ await rm(`/tmp/agent-swarm-tool-history/${taskId}.json`, { force: true });
204
+ }
205
+ const out = parseSoleJson(stdout) as { hookSpecificOutput: Record<string, string> };
206
+ expect(out.hookSpecificOutput.hookEventName).toBe("PreToolUse");
207
+ expect(out.hookSpecificOutput.additionalContext).toStartWith("Warning: ");
208
+ },
209
+ CHILD_PROCESS_TEST_BUDGET_MS * 3,
210
+ );
211
+
212
+ test(
213
+ "lead send-task reminder reaches the model as PostToolUse additionalContext",
214
+ async () => {
215
+ Object.assign(state, { isLead: true, cancelledTaskId: null, shouldBlockPolling: false });
216
+ const result = await runHook(
217
+ {
218
+ hook_event_name: "PostToolUse",
219
+ tool_name: "mcp__agent-swarm__send-task",
220
+ tool_input: {},
221
+ tool_response: { task: { id: "task-123" } },
222
+ },
223
+ crypto.randomUUID(),
224
+ );
225
+ const out = parseSoleJson(result.stdout) as {
226
+ hookSpecificOutput: Record<string, string>;
227
+ };
228
+ expect(out.hookSpecificOutput.hookEventName).toBe("PostToolUse");
229
+ expect(out.hookSpecificOutput.additionalContext).toContain("Task ID: task-123.");
230
+ },
231
+ CHILD_PROCESS_TEST_BUDGET_MS,
232
+ );
233
+ });
@@ -34,6 +34,7 @@
34
34
  import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
35
35
  import { unlink } from "node:fs/promises";
36
36
  import { createServer, type Server } from "node:http";
37
+ import { join } from "node:path";
37
38
  import { closeDb, initDb } from "../be/db";
38
39
  import { handleOAuthLocks } from "../http/oauth-locks";
39
40
  import { resolveCodexAuthMode } from "../providers/codex-adapter.js";
@@ -45,6 +46,7 @@ const TEST_DB_PATH = "./test-codex-adapter-pool-auth-revalidate.sqlite";
45
46
  const MOCK_API_URL = "http://localhost:3013";
46
47
  const MOCK_API_KEY = "example-test-api-key";
47
48
  const FAKE_HOME = "/home/fake-pool-worker";
49
+ const FAKE_AUTH_PATH = join(FAKE_HOME, ".codex", "auth.json");
48
50
 
49
51
  process.env.SECRETS_ENCRYPTION_KEY = Buffer.alloc(32, 4).toString("base64");
50
52
 
@@ -106,14 +108,14 @@ function fakeAuthJsonFs(initialContent: string) {
106
108
  let file: string | null = initialContent;
107
109
  const fs = {
108
110
  readFile: async (path: string): Promise<string> => {
109
- if (path !== `${FAKE_HOME}/.codex/auth.json` || file === null) {
111
+ if (path !== FAKE_AUTH_PATH || file === null) {
110
112
  throw new Error("ENOENT");
111
113
  }
112
114
  return file;
113
115
  },
114
116
  mkdir: async (): Promise<undefined> => undefined,
115
117
  writeFile: async (path: string, data: string): Promise<void> => {
116
- expect(path).toBe(`${FAKE_HOME}/.codex/auth.json`);
118
+ expect(path).toBe(FAKE_AUTH_PATH);
117
119
  file = data;
118
120
  },
119
121
  };