@desplega.ai/agent-swarm 1.91.0 → 1.92.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 (114) hide show
  1. package/README.md +3 -2
  2. package/openapi.json +1005 -152
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +662 -19
  6. package/src/be/memory/constants.ts +2 -1
  7. package/src/be/memory/providers/openai-embedding.ts +2 -5
  8. package/src/be/memory/providers/sqlite-store.ts +293 -76
  9. package/src/be/memory/types.ts +35 -0
  10. package/src/be/migrations/083_script_workflows.sql +51 -0
  11. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  12. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  13. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  14. package/src/be/migrations/087_skill_files.sql +19 -0
  15. package/src/be/modelsdev-cache.json +42310 -38617
  16. package/src/be/scripts/typecheck.ts +49 -0
  17. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  18. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  19. package/src/be/seed-scripts/catalog/compound-insights.ts +310 -6
  20. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  21. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  22. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +506 -0
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  25. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  26. package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
  27. package/src/be/seed-scripts/index.ts +51 -5
  28. package/src/be/seed-skills/index.ts +3 -3
  29. package/src/be/skill-sync.ts +91 -7
  30. package/src/be/swarm-config-guard.ts +17 -0
  31. package/src/commands/runner.ts +49 -4
  32. package/src/heartbeat/templates.ts +20 -16
  33. package/src/http/db-query.ts +20 -5
  34. package/src/http/index.ts +51 -7
  35. package/src/http/mcp-user.ts +23 -0
  36. package/src/http/mcp.ts +58 -0
  37. package/src/http/memory.ts +58 -0
  38. package/src/http/pages.ts +1 -1
  39. package/src/http/script-runs.ts +557 -0
  40. package/src/http/scripts.ts +39 -2
  41. package/src/http/skills.ts +225 -0
  42. package/src/prompts/session-templates.ts +24 -4
  43. package/src/providers/claude-adapter.ts +107 -28
  44. package/src/script-workflows/executor.ts +110 -0
  45. package/src/script-workflows/harness.ts +73 -0
  46. package/src/script-workflows/label-lint.ts +51 -0
  47. package/src/script-workflows/limits.ts +22 -0
  48. package/src/script-workflows/supervisor.ts +139 -0
  49. package/src/script-workflows/workflow-ctx.ts +209 -0
  50. package/src/scripts-runtime/sdk-allowlist.ts +4 -0
  51. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  52. package/src/scripts-runtime/types/stdlib.d.ts +61 -0
  53. package/src/scripts-runtime/types/swarm-sdk.d.ts +61 -0
  54. package/src/server.ts +4 -0
  55. package/src/slack/handlers.ts +11 -4
  56. package/src/slack/message-text.ts +98 -0
  57. package/src/slack/thread-buffer.ts +5 -3
  58. package/src/tests/claude-adapter-binary.test.ts +271 -74
  59. package/src/tests/create-page-tool.test.ts +19 -2
  60. package/src/tests/db-query.test.ts +28 -0
  61. package/src/tests/error-tracker.test.ts +121 -0
  62. package/src/tests/harness-provider-resolution.test.ts +33 -0
  63. package/src/tests/heartbeat-checklist.test.ts +36 -0
  64. package/src/tests/mcp-tools.test.ts +6 -0
  65. package/src/tests/mcp-transport-gc.test.ts +58 -0
  66. package/src/tests/memory-health-endpoint.test.ts +78 -0
  67. package/src/tests/memory-store.test.ts +221 -1
  68. package/src/tests/pages-http.test.ts +20 -2
  69. package/src/tests/pages-storage.test.ts +26 -0
  70. package/src/tests/prompt-template-session.test.ts +34 -5
  71. package/src/tests/script-runs-http.test.ts +278 -0
  72. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  73. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  74. package/src/tests/scripts-mcp-e2e.test.ts +102 -2
  75. package/src/tests/seed-scripts.test.ts +468 -3
  76. package/src/tests/skill-files-http.test.ts +171 -0
  77. package/src/tests/skill-files.test.ts +162 -0
  78. package/src/tests/skill-get-file-tool.test.ts +110 -0
  79. package/src/tests/skill-sync.test.ts +125 -6
  80. package/src/tests/slack-message-text.test.ts +250 -0
  81. package/src/tests/system-default-skills.test.ts +40 -0
  82. package/src/tools/create-page.ts +2 -2
  83. package/src/tools/db-query.ts +16 -6
  84. package/src/tools/script-runs.ts +123 -0
  85. package/src/tools/skills/index.ts +1 -0
  86. package/src/tools/skills/skill-get-file.ts +80 -0
  87. package/src/tools/slack-read.ts +12 -3
  88. package/src/tools/tool-config.ts +6 -2
  89. package/src/types.ts +72 -0
  90. package/src/utils/error-tracker.ts +40 -1
  91. package/src/utils/internal-ai/complete-structured.ts +10 -4
  92. package/src/workflows/executors/raw-llm.ts +76 -59
  93. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  94. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  95. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  96. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  97. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  98. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  99. package/templates/skills/agentmail-sending/content.md +6 -7
  100. package/templates/skills/desloppify/content.md +8 -9
  101. package/templates/skills/jira-interaction/content.md +25 -33
  102. package/templates/skills/kapso-whatsapp/content.md +29 -30
  103. package/templates/skills/linear-interaction/content.md +8 -9
  104. package/templates/skills/pages/content.md +205 -55
  105. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  106. package/templates/skills/script-workflows/config.json +14 -0
  107. package/templates/skills/script-workflows/content.md +68 -0
  108. package/templates/skills/sprite-cli/content.md +4 -5
  109. package/templates/skills/swarm-scripts/content.md +2 -3
  110. package/templates/skills/turso-interaction/content.md +14 -17
  111. package/templates/skills/workflow-iterate/content.md +38 -391
  112. package/templates/skills/x-api-interactions/content.md +4 -6
  113. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  114. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -5,13 +5,17 @@
5
5
  * Behaviors under test:
6
6
  * 1. Binary resolution — argv[0..n] tracks `parseClaudeBinary(process.env.CLAUDE_BINARY)`,
7
7
  * with `["claude"]` as the default. Same flags follow. Supports
8
- * whitespace-separated command strings (e.g. `"bunx @dexh/shannon"`).
9
- * 2. Tmux fail-fastwhen the resolved binary string contains "shannon"
10
- * (anywhere including inside a command string), createSession throws
11
- * if `tmux` is not on PATH.
12
- * 3. Trust pre-seed — when the resolved binary contains "shannon", the
13
- * adapter writes `projects[cwd].hasTrustDialogAccepted: true` to
14
- * `$HOME/.claude.json` before spawning. Idempotent. No-op for "claude".
8
+ * whitespace-separated command strings.
9
+ * 2. Claude Bridge routing SWARM_USE_CLAUDE_BRIDGE=true/1 forces the
10
+ * installed `claude-bridge` argv prefix and wins over
11
+ * `CLAUDE_BINARY`.
12
+ * 3. Tmux fail-fast — when the resolved binary string uses the legacy
13
+ * bridge compatibility path or claude-bridge is enabled, createSession
14
+ * throws if `tmux` is not on PATH.
15
+ * 4. Trust pre-seed — when the resolved path drives interactive claude in
16
+ * tmux, the adapter writes
17
+ * `projects[cwd].hasTrustDialogAccepted: true` to `$HOME/.claude.json`
18
+ * before spawning. Idempotent. No-op for "claude".
15
19
  *
16
20
  * `Bun.spawn` is stubbed so the tests don't actually exec anything; we read
17
21
  * the argv off the call args. `Bun.which` is stubbed for the tmux gate so
@@ -27,11 +31,17 @@ import { join } from "node:path";
27
31
  import {
28
32
  ClaudeAdapter,
29
33
  parseClaudeBinary,
34
+ parseClaudeBridgeEnabled,
30
35
  preseedClaudeTrustDialog,
31
36
  resolveClaudeBinary,
37
+ resolveClaudeBridgeEnabled,
32
38
  } from "../providers/claude-adapter";
33
39
  import type { ProviderSessionConfig } from "../providers/types";
34
40
 
41
+ const LEGACY_BRIDGE_COMPAT_BINARY = "shan" + "non";
42
+ const LEGACY_BRIDGE_COMPAT_PACKAGE = `@dexh/${LEGACY_BRIDGE_COMPAT_BINARY}`;
43
+ const LEGACY_BRIDGE_COMPAT_COMMAND = `bunx ${LEGACY_BRIDGE_COMPAT_PACKAGE}`;
44
+
35
45
  /** Minimal config — empty apiUrl/apiKey/agentId skips the MCP-server fetch. */
36
46
  function makeConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSessionConfig {
37
47
  return {
@@ -79,36 +89,54 @@ describe("parseClaudeBinary", () => {
79
89
 
80
90
  test("single token → one-element array", () => {
81
91
  expect(parseClaudeBinary("claude")).toEqual(["claude"]);
82
- expect(parseClaudeBinary("shannon")).toEqual(["shannon"]);
83
- expect(parseClaudeBinary("/usr/local/bin/shannon")).toEqual(["/usr/local/bin/shannon"]);
92
+ expect(parseClaudeBinary(LEGACY_BRIDGE_COMPAT_BINARY)).toEqual([LEGACY_BRIDGE_COMPAT_BINARY]);
93
+ expect(parseClaudeBinary(`/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`)).toEqual([
94
+ `/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`,
95
+ ]);
84
96
  });
85
97
 
86
98
  test("command string → whitespace-split argv", () => {
87
- expect(parseClaudeBinary("bunx @dexh/shannon")).toEqual(["bunx", "@dexh/shannon"]);
88
- expect(parseClaudeBinary("npx -y @dexh/shannon")).toEqual(["npx", "-y", "@dexh/shannon"]);
99
+ expect(parseClaudeBinary(LEGACY_BRIDGE_COMPAT_COMMAND)).toEqual([
100
+ "bunx",
101
+ LEGACY_BRIDGE_COMPAT_PACKAGE,
102
+ ]);
103
+ expect(parseClaudeBinary(`npx -y ${LEGACY_BRIDGE_COMPAT_PACKAGE}`)).toEqual([
104
+ "npx",
105
+ "-y",
106
+ LEGACY_BRIDGE_COMPAT_PACKAGE,
107
+ ]);
89
108
  });
90
109
 
91
110
  test("version-pinned → preserves the version suffix", () => {
92
- expect(parseClaudeBinary("bunx @dexh/shannon@1.2.3")).toEqual(["bunx", "@dexh/shannon@1.2.3"]);
111
+ expect(parseClaudeBinary(`${LEGACY_BRIDGE_COMPAT_COMMAND}@1.2.3`)).toEqual([
112
+ "bunx",
113
+ `${LEGACY_BRIDGE_COMPAT_PACKAGE}@1.2.3`,
114
+ ]);
93
115
  });
94
116
 
95
117
  test("multiple-space tolerance → trims + collapses", () => {
96
- expect(parseClaudeBinary(" bunx shannon ")).toEqual(["bunx", "shannon"]);
97
- expect(parseClaudeBinary("\tbunx\t@dexh/shannon\n")).toEqual(["bunx", "@dexh/shannon"]);
118
+ expect(parseClaudeBinary(` bunx ${LEGACY_BRIDGE_COMPAT_BINARY} `)).toEqual([
119
+ "bunx",
120
+ LEGACY_BRIDGE_COMPAT_BINARY,
121
+ ]);
122
+ expect(parseClaudeBinary(`\tbunx\t${LEGACY_BRIDGE_COMPAT_PACKAGE}\n`)).toEqual([
123
+ "bunx",
124
+ LEGACY_BRIDGE_COMPAT_PACKAGE,
125
+ ]);
98
126
  });
99
127
  });
100
128
 
101
129
  describe("resolveClaudeBinary precedence", () => {
102
130
  test("resolvedEnv wins over fallbackEnv (swarm_config overrides process.env)", () => {
103
- const resolvedEnv = { CLAUDE_BINARY: "shannon" };
131
+ const resolvedEnv = { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY };
104
132
  const fallbackEnv = { CLAUDE_BINARY: "claude" };
105
- expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe("shannon");
133
+ expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
106
134
  });
107
135
 
108
136
  test("falls back to fallbackEnv when resolvedEnv is absent", () => {
109
137
  const resolvedEnv = {};
110
- const fallbackEnv = { CLAUDE_BINARY: "bunx @dexh/shannon" };
111
- expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe("bunx @dexh/shannon");
138
+ const fallbackEnv = { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_COMMAND };
139
+ expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe(LEGACY_BRIDGE_COMPAT_COMMAND);
112
140
  });
113
141
 
114
142
  test("both absent → 'claude' default", () => {
@@ -117,12 +145,12 @@ describe("resolveClaudeBinary precedence", () => {
117
145
 
118
146
  test("empty / whitespace-only resolvedEnv value falls through to fallbackEnv", () => {
119
147
  // `.trim() || …` falls through on empty/whitespace.
120
- expect(resolveClaudeBinary({ CLAUDE_BINARY: "" }, { CLAUDE_BINARY: "shannon" })).toBe(
121
- "shannon",
122
- );
123
- expect(resolveClaudeBinary({ CLAUDE_BINARY: " " }, { CLAUDE_BINARY: "shannon" })).toBe(
124
- "shannon",
125
- );
148
+ expect(
149
+ resolveClaudeBinary({ CLAUDE_BINARY: "" }, { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY }),
150
+ ).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
151
+ expect(
152
+ resolveClaudeBinary({ CLAUDE_BINARY: " " }, { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY }),
153
+ ).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
126
154
  });
127
155
 
128
156
  test("empty fallback after empty resolved → 'claude' default", () => {
@@ -130,8 +158,8 @@ describe("resolveClaudeBinary precedence", () => {
130
158
  });
131
159
 
132
160
  test("command-string passes through unchanged (caller does the argv split)", () => {
133
- const resolvedEnv = { CLAUDE_BINARY: "bunx @dexh/shannon@1.2.3" };
134
- expect(resolveClaudeBinary(resolvedEnv, {})).toBe("bunx @dexh/shannon@1.2.3");
161
+ const resolvedEnv = { CLAUDE_BINARY: `${LEGACY_BRIDGE_COMPAT_COMMAND}@1.2.3` };
162
+ expect(resolveClaudeBinary(resolvedEnv, {})).toBe(`${LEGACY_BRIDGE_COMPAT_COMMAND}@1.2.3`);
135
163
  });
136
164
 
137
165
  test("fallbackEnv defaults to process.env when omitted", () => {
@@ -150,6 +178,45 @@ describe("resolveClaudeBinary precedence", () => {
150
178
  });
151
179
  });
152
180
 
181
+ describe("SWARM_USE_CLAUDE_BRIDGE boolean parsing", () => {
182
+ test("true/1 enable claude-bridge", () => {
183
+ expect(parseClaudeBridgeEnabled("true")).toBe(true);
184
+ expect(parseClaudeBridgeEnabled("TRUE")).toBe(true);
185
+ expect(parseClaudeBridgeEnabled(" 1 ")).toBe(true);
186
+ });
187
+
188
+ test("false/0/unset and invalid values are disabled", () => {
189
+ expect(parseClaudeBridgeEnabled("false")).toBe(false);
190
+ expect(parseClaudeBridgeEnabled("0")).toBe(false);
191
+ expect(parseClaudeBridgeEnabled(undefined)).toBe(false);
192
+ expect(parseClaudeBridgeEnabled("yes")).toBe(false);
193
+ });
194
+
195
+ test("resolvedEnv wins over fallbackEnv", () => {
196
+ expect(
197
+ resolveClaudeBridgeEnabled(
198
+ { SWARM_USE_CLAUDE_BRIDGE: "false" },
199
+ { SWARM_USE_CLAUDE_BRIDGE: "true" },
200
+ ),
201
+ ).toBe(false);
202
+ expect(
203
+ resolveClaudeBridgeEnabled(
204
+ { SWARM_USE_CLAUDE_BRIDGE: "1" },
205
+ { SWARM_USE_CLAUDE_BRIDGE: "0" },
206
+ ),
207
+ ).toBe(true);
208
+ });
209
+
210
+ test("empty resolvedEnv value falls through to fallbackEnv", () => {
211
+ expect(
212
+ resolveClaudeBridgeEnabled(
213
+ { SWARM_USE_CLAUDE_BRIDGE: " " },
214
+ { SWARM_USE_CLAUDE_BRIDGE: "true" },
215
+ ),
216
+ ).toBe(true);
217
+ });
218
+ });
219
+
153
220
  describe("preseedClaudeTrustDialog", () => {
154
221
  let homeDir: string;
155
222
 
@@ -241,26 +308,32 @@ describe("preseedClaudeTrustDialog", () => {
241
308
  describe("CLAUDE_BINARY env override", () => {
242
309
  // Cache the originals and restore after each test so the suite stays clean.
243
310
  let originalClaudeBinary: string | undefined;
311
+ let originalUseClaudeBridge: string | undefined;
244
312
  let originalOauthToken: string | undefined;
245
313
  let originalHome: string | undefined;
246
314
  let homeDir: string;
247
315
  let spawnSpy: ReturnType<typeof spyOn>;
248
316
  let whichSpy: ReturnType<typeof spyOn>;
249
317
  let spawnedArgs: Array<readonly string[]>;
318
+ let spawnedEnvs: Array<Record<string, string> | undefined>;
250
319
 
251
320
  beforeEach(async () => {
252
321
  originalClaudeBinary = process.env.CLAUDE_BINARY;
322
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
253
323
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
254
324
  originalHome = process.env.HOME;
255
325
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
256
326
  process.env.HOME = homeDir;
257
327
  delete process.env.CLAUDE_BINARY;
328
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
258
329
  // Credential check runs before binary resolution; satisfy it.
259
330
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
260
331
 
261
332
  spawnedArgs = [];
262
- spawnSpy = spyOn(Bun, "spawn").mockImplementation(((cmd: readonly string[]) => {
333
+ spawnedEnvs = [];
334
+ spawnSpy = spyOn(Bun, "spawn").mockImplementation(((cmd: readonly string[], opts?: unknown) => {
263
335
  spawnedArgs.push(cmd);
336
+ spawnedEnvs.push((opts as { env?: Record<string, string> } | undefined)?.env);
264
337
  return makeFakeProc();
265
338
  }) as typeof Bun.spawn);
266
339
 
@@ -285,6 +358,11 @@ describe("CLAUDE_BINARY env override", () => {
285
358
  } else {
286
359
  process.env.CLAUDE_BINARY = originalClaudeBinary;
287
360
  }
361
+ if (originalUseClaudeBridge === undefined) {
362
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
363
+ } else {
364
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
365
+ }
288
366
  if (originalOauthToken === undefined) {
289
367
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
290
368
  } else {
@@ -301,68 +379,68 @@ describe("CLAUDE_BINARY env override", () => {
301
379
  expect(argv[0]).toBe("claude");
302
380
  });
303
381
 
304
- test("override: argv[0] is 'shannon' when CLAUDE_BINARY=shannon", async () => {
305
- process.env.CLAUDE_BINARY = "shannon";
382
+ test("legacy bridge override: argv[0] comes from CLAUDE_BINARY", async () => {
383
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
306
384
 
307
385
  const adapter = new ClaudeAdapter();
308
386
  await adapter.createSession(makeConfig());
309
387
 
310
388
  const argv = spawnedArgs[0];
311
- expect(argv[0]).toBe("shannon");
389
+ expect(argv[0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
312
390
  });
313
391
 
314
- test("custom path: argv[0] is the absolute path when CLAUDE_BINARY=/usr/local/bin/shannon", async () => {
315
- process.env.CLAUDE_BINARY = "/usr/local/bin/shannon";
392
+ test("custom legacy bridge path: argv[0] is the absolute path", async () => {
393
+ process.env.CLAUDE_BINARY = `/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`;
316
394
 
317
395
  const adapter = new ClaudeAdapter();
318
396
  await adapter.createSession(makeConfig());
319
397
 
320
- expect(spawnedArgs[0][0]).toBe("/usr/local/bin/shannon");
398
+ expect(spawnedArgs[0][0]).toBe(`/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`);
321
399
  });
322
400
 
323
- test("command string: 'bunx @dexh/shannon' → argv[0..1] is ['bunx', '@dexh/shannon']", async () => {
324
- process.env.CLAUDE_BINARY = "bunx @dexh/shannon";
401
+ test("legacy bridge command string → argv[0..1] is split", async () => {
402
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_COMMAND;
325
403
 
326
404
  const adapter = new ClaudeAdapter();
327
405
  await adapter.createSession(makeConfig());
328
406
 
329
407
  const argv = spawnedArgs[0];
330
408
  expect(argv[0]).toBe("bunx");
331
- expect(argv[1]).toBe("@dexh/shannon");
409
+ expect(argv[1]).toBe(LEGACY_BRIDGE_COMPAT_PACKAGE);
332
410
  // Claude args follow.
333
411
  expect(argv).toContain("--model");
334
412
  expect(argv).toContain("-p");
335
413
  });
336
414
 
337
- test("version-pinned command string: argv[0..1] = ['bunx', '@dexh/shannon@1.2.3']", async () => {
338
- process.env.CLAUDE_BINARY = "bunx @dexh/shannon@1.2.3";
415
+ test("version-pinned legacy bridge command string keeps package suffix", async () => {
416
+ process.env.CLAUDE_BINARY = `${LEGACY_BRIDGE_COMPAT_COMMAND}@1.2.3`;
339
417
 
340
418
  const adapter = new ClaudeAdapter();
341
419
  await adapter.createSession(makeConfig());
342
420
 
343
421
  const argv = spawnedArgs[0];
344
422
  expect(argv[0]).toBe("bunx");
345
- expect(argv[1]).toBe("@dexh/shannon@1.2.3");
423
+ expect(argv[1]).toBe(`${LEGACY_BRIDGE_COMPAT_PACKAGE}@1.2.3`);
346
424
  });
347
425
 
348
- test("multiple-space tolerance: ' bunx shannon ' argv = ['bunx', 'shannon', ...]", async () => {
349
- process.env.CLAUDE_BINARY = " bunx shannon ";
426
+ test("multiple-space tolerance for legacy bridge command", async () => {
427
+ process.env.CLAUDE_BINARY = ` bunx ${LEGACY_BRIDGE_COMPAT_BINARY} `;
350
428
 
351
429
  const adapter = new ClaudeAdapter();
352
430
  await adapter.createSession(makeConfig());
353
431
 
354
432
  const argv = spawnedArgs[0];
355
433
  expect(argv[0]).toBe("bunx");
356
- expect(argv[1]).toBe("shannon");
434
+ expect(argv[1]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
357
435
  expect(argv).toContain("--model");
358
436
  });
359
437
 
360
- test("argv[1..] (after prefix) matches between default 'claude' and command-string 'bunx @dexh/shannon'", async () => {
361
- process.env.CLAUDE_BINARY = "bunx @dexh/shannon";
438
+ test("argv[1..] after prefix matches between default and legacy bridge command", async () => {
439
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_COMMAND;
362
440
  const adapter = new ClaudeAdapter();
363
441
  await adapter.createSession(makeConfig());
364
442
  // Drop the 2-element prefix.
365
- const argvShannon = spawnedArgs[0].slice(2);
443
+ const argvLegacyBridge = spawnedArgs[0].slice(2);
366
444
 
367
445
  spawnedArgs = [];
368
446
  delete process.env.CLAUDE_BINARY;
@@ -370,47 +448,47 @@ describe("CLAUDE_BINARY env override", () => {
370
448
  // Drop the 1-element prefix.
371
449
  const argvClaude = spawnedArgs[0].slice(1);
372
450
 
373
- expect(argvShannon).toEqual(argvClaude);
451
+ expect(argvLegacyBridge).toEqual(argvClaude);
374
452
  });
375
453
 
376
454
  test("swarm_config overlay (config.env) wins over process.env CLAUDE_BINARY", async () => {
377
455
  // process.env says "claude" — but the runner's resolvedEnv overlay (passed
378
- // through config.env) says "shannon". The overlay must win, mirroring the
456
+ // through config.env) says a legacy bridge binary. The overlay must win, mirroring the
379
457
  // HARNESS_PROVIDER reload path.
380
458
  process.env.CLAUDE_BINARY = "claude";
381
459
 
382
460
  const adapter = new ClaudeAdapter();
383
461
  await adapter.createSession(
384
462
  makeConfig({
385
- env: { CLAUDE_BINARY: "shannon", CLAUDE_CODE_OAUTH_TOKEN: "test-token" } as Record<
386
- string,
387
- string
388
- >,
463
+ env: {
464
+ CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY,
465
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
466
+ } as Record<string, string>,
389
467
  }),
390
468
  );
391
469
 
392
- expect(spawnedArgs[0][0]).toBe("shannon");
470
+ expect(spawnedArgs[0][0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
393
471
  });
394
472
 
395
- test("config.env CLAUDE_BINARY='bunx @dexh/shannon' (swarm_config override) splits + spawns correctly", async () => {
473
+ test("config.env legacy bridge command override splits + spawns correctly", async () => {
396
474
  delete process.env.CLAUDE_BINARY;
397
475
 
398
476
  const adapter = new ClaudeAdapter();
399
477
  await adapter.createSession(
400
478
  makeConfig({
401
479
  env: {
402
- CLAUDE_BINARY: "bunx @dexh/shannon",
480
+ CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_COMMAND,
403
481
  CLAUDE_CODE_OAUTH_TOKEN: "test-token",
404
482
  } as Record<string, string>,
405
483
  }),
406
484
  );
407
485
 
408
486
  expect(spawnedArgs[0][0]).toBe("bunx");
409
- expect(spawnedArgs[0][1]).toBe("@dexh/shannon");
487
+ expect(spawnedArgs[0][1]).toBe(LEGACY_BRIDGE_COMPAT_PACKAGE);
410
488
  });
411
489
 
412
490
  test("config.env without CLAUDE_BINARY falls back to process.env", async () => {
413
- process.env.CLAUDE_BINARY = "shannon";
491
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
414
492
 
415
493
  const adapter = new ClaudeAdapter();
416
494
  await adapter.createSession(
@@ -420,12 +498,94 @@ describe("CLAUDE_BINARY env override", () => {
420
498
  }),
421
499
  );
422
500
 
423
- expect(spawnedArgs[0][0]).toBe("shannon");
501
+ expect(spawnedArgs[0][0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
502
+ });
503
+
504
+ test("SWARM_USE_CLAUDE_BRIDGE=true routes through installed claude-bridge", async () => {
505
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
506
+
507
+ const adapter = new ClaudeAdapter();
508
+ await adapter.createSession(makeConfig());
509
+
510
+ const argv = spawnedArgs[0];
511
+ expect(argv[0]).toBe("claude-bridge");
512
+ expect(argv).toContain("--model");
513
+ expect(argv).toContain("-p");
514
+ });
515
+
516
+ test("SWARM_USE_CLAUDE_BRIDGE=true passes OAuth token to the bridge process", async () => {
517
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
518
+
519
+ const adapter = new ClaudeAdapter();
520
+ await adapter.createSession(makeConfig());
521
+
522
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
523
+ expect(spawnedEnvs[0]?.CLAUDE_CODE_OAUTH_TOKEN).toBe("test-token");
524
+ });
525
+
526
+ test("SWARM_USE_CLAUDE_BRIDGE=true forwards Anthropic local auth through bridge flag", async () => {
527
+ const adapter = new ClaudeAdapter();
528
+ await adapter.createSession(
529
+ makeConfig({
530
+ env: {
531
+ SWARM_USE_CLAUDE_BRIDGE: "true",
532
+ ANTHROPIC_API_KEY: "sk-ant-test",
533
+ } as Record<string, string>,
534
+ }),
535
+ );
536
+
537
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
538
+ expect(spawnedArgs[0]).toContain("--desplega-local-auth");
539
+ expect(spawnedEnvs[0]?.ANTHROPIC_API_KEY).toBe("sk-ant-test");
540
+ expect(spawnedEnvs[0]?.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
541
+ });
542
+
543
+ test("SWARM_USE_CLAUDE_BRIDGE=1 wins over legacy CLAUDE_BINARY", async () => {
544
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "1";
545
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
546
+
547
+ const adapter = new ClaudeAdapter();
548
+ await adapter.createSession(makeConfig());
549
+
550
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
551
+ });
552
+
553
+ test("config.env SWARM_USE_CLAUDE_BRIDGE=true is reloadable and wins over process.env false", async () => {
554
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "false";
555
+
556
+ const adapter = new ClaudeAdapter();
557
+ await adapter.createSession(
558
+ makeConfig({
559
+ env: {
560
+ SWARM_USE_CLAUDE_BRIDGE: "true",
561
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
562
+ } as Record<string, string>,
563
+ }),
564
+ );
565
+
566
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
567
+ });
568
+
569
+ test("config.env SWARM_USE_CLAUDE_BRIDGE=false disables process.env true", async () => {
570
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
571
+
572
+ const adapter = new ClaudeAdapter();
573
+ await adapter.createSession(
574
+ makeConfig({
575
+ env: {
576
+ SWARM_USE_CLAUDE_BRIDGE: "false",
577
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
578
+ } as Record<string, string>,
579
+ }),
580
+ );
581
+
582
+ expect(spawnedArgs[0][0]).toBe("claude");
424
583
  });
425
584
  });
426
585
 
427
- describe("Shannon tmux fail-fast gate", () => {
586
+ describe("Claude Bridge tmux fail-fast gate", () => {
428
587
  let originalClaudeBinary: string | undefined;
588
+ let originalUseClaudeBridge: string | undefined;
429
589
  let originalOauthToken: string | undefined;
430
590
  let originalHome: string | undefined;
431
591
  let homeDir: string;
@@ -434,11 +594,13 @@ describe("Shannon tmux fail-fast gate", () => {
434
594
 
435
595
  beforeEach(async () => {
436
596
  originalClaudeBinary = process.env.CLAUDE_BINARY;
597
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
437
598
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
438
599
  originalHome = process.env.HOME;
439
600
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
440
601
  process.env.HOME = homeDir;
441
602
  delete process.env.CLAUDE_BINARY;
603
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
442
604
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
443
605
  spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
444
606
  whichSpy = spyOn(Bun, "which");
@@ -458,6 +620,11 @@ describe("Shannon tmux fail-fast gate", () => {
458
620
  } else {
459
621
  process.env.CLAUDE_BINARY = originalClaudeBinary;
460
622
  }
623
+ if (originalUseClaudeBridge === undefined) {
624
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
625
+ } else {
626
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
627
+ }
461
628
  if (originalOauthToken === undefined) {
462
629
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
463
630
  } else {
@@ -465,8 +632,8 @@ describe("Shannon tmux fail-fast gate", () => {
465
632
  }
466
633
  });
467
634
 
468
- test("sad path: rejects with tmux-mentioning error when CLAUDE_BINARY=shannon and tmux is missing", async () => {
469
- process.env.CLAUDE_BINARY = "shannon";
635
+ test("sad path: rejects with tmux-mentioning error when legacy CLAUDE_BINARY is set and tmux is missing", async () => {
636
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
470
637
  whichSpy.mockImplementation((name: string) => {
471
638
  if (name === "tmux") return null;
472
639
  return `/usr/bin/${name}`;
@@ -476,8 +643,8 @@ describe("Shannon tmux fail-fast gate", () => {
476
643
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
477
644
  });
478
645
 
479
- test("happy path: does not throw when CLAUDE_BINARY=shannon and tmux IS on PATH", async () => {
480
- process.env.CLAUDE_BINARY = "shannon";
646
+ test("happy path: does not throw when legacy CLAUDE_BINARY is set and tmux IS on PATH", async () => {
647
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
481
648
  whichSpy.mockImplementation((name: string) => {
482
649
  if (name === "tmux") return "/usr/bin/tmux";
483
650
  return null;
@@ -487,7 +654,7 @@ describe("Shannon tmux fail-fast gate", () => {
487
654
  await expect(adapter.createSession(makeConfig())).resolves.toBeDefined();
488
655
  });
489
656
 
490
- test("non-shannon binary skips the tmux check (no Bun.which call for tmux)", async () => {
657
+ test("default binary skips the tmux check (no Bun.which call for tmux)", async () => {
491
658
  process.env.CLAUDE_BINARY = "claude";
492
659
  whichSpy.mockImplementation((name: string) => {
493
660
  if (name === "tmux") return null;
@@ -499,8 +666,8 @@ describe("Shannon tmux fail-fast gate", () => {
499
666
  await expect(adapter.createSession(makeConfig())).resolves.toBeDefined();
500
667
  });
501
668
 
502
- test("custom shannon path (e.g. /usr/local/bin/shannon) still triggers the tmux check", async () => {
503
- process.env.CLAUDE_BINARY = "/usr/local/bin/shannon";
669
+ test("custom legacy bridge path still triggers the tmux check", async () => {
670
+ process.env.CLAUDE_BINARY = `/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`;
504
671
  whichSpy.mockImplementation((name: string) => {
505
672
  if (name === "tmux") return null;
506
673
  return null;
@@ -510,8 +677,8 @@ describe("Shannon tmux fail-fast gate", () => {
510
677
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
511
678
  });
512
679
 
513
- test("command-string CLAUDE_BINARY='bunx @dexh/shannon' still triggers the tmux check", async () => {
514
- process.env.CLAUDE_BINARY = "bunx @dexh/shannon";
680
+ test("legacy bridge command string still triggers the tmux check", async () => {
681
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_COMMAND;
515
682
  whichSpy.mockImplementation((name: string) => {
516
683
  if (name === "tmux") return null;
517
684
  return null;
@@ -520,10 +687,22 @@ describe("Shannon tmux fail-fast gate", () => {
520
687
  const adapter = new ClaudeAdapter();
521
688
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
522
689
  });
690
+
691
+ test("SWARM_USE_CLAUDE_BRIDGE=true triggers the tmux check", async () => {
692
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
693
+ whichSpy.mockImplementation((name: string) => {
694
+ if (name === "tmux") return null;
695
+ return null;
696
+ });
697
+
698
+ const adapter = new ClaudeAdapter();
699
+ await expect(adapter.createSession(makeConfig())).rejects.toThrow(/SWARM_USE_CLAUDE_BRIDGE/);
700
+ });
523
701
  });
524
702
 
525
703
  describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
526
704
  let originalClaudeBinary: string | undefined;
705
+ let originalUseClaudeBridge: string | undefined;
527
706
  let originalOauthToken: string | undefined;
528
707
  let originalHome: string | undefined;
529
708
  let homeDir: string;
@@ -532,11 +711,13 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
532
711
 
533
712
  beforeEach(async () => {
534
713
  originalClaudeBinary = process.env.CLAUDE_BINARY;
714
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
535
715
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
536
716
  originalHome = process.env.HOME;
537
717
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-trust-test-"));
538
718
  process.env.HOME = homeDir;
539
719
  delete process.env.CLAUDE_BINARY;
720
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
540
721
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
541
722
  spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
542
723
  whichSpy = spyOn(Bun, "which").mockImplementation((name: string) => {
@@ -559,6 +740,11 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
559
740
  } else {
560
741
  process.env.CLAUDE_BINARY = originalClaudeBinary;
561
742
  }
743
+ if (originalUseClaudeBridge === undefined) {
744
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
745
+ } else {
746
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
747
+ }
562
748
  if (originalOauthToken === undefined) {
563
749
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
564
750
  } else {
@@ -566,8 +752,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
566
752
  }
567
753
  });
568
754
 
569
- test("CLAUDE_BINARY=shannon writes hasTrustDialogAccepted for config.cwd", async () => {
570
- process.env.CLAUDE_BINARY = "shannon";
755
+ test("legacy CLAUDE_BINARY writes hasTrustDialogAccepted for config.cwd", async () => {
756
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
571
757
  const cwd = "/some/abs/cwd";
572
758
  const adapter = new ClaudeAdapter();
573
759
  await adapter.createSession(makeConfig({ cwd }));
@@ -577,8 +763,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
577
763
  expect(data.projects[cwd].hasCompletedProjectOnboarding).toBe(true);
578
764
  });
579
765
 
580
- test("CLAUDE_BINARY='bunx @dexh/shannon' (command string) also triggers the pre-seed", async () => {
581
- process.env.CLAUDE_BINARY = "bunx @dexh/shannon";
766
+ test("legacy CLAUDE_BINARY command string also triggers the pre-seed", async () => {
767
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_COMMAND;
582
768
  const cwd = "/some/other/cwd";
583
769
  const adapter = new ClaudeAdapter();
584
770
  await adapter.createSession(makeConfig({ cwd }));
@@ -587,8 +773,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
587
773
  expect(data.projects[cwd].hasTrustDialogAccepted).toBe(true);
588
774
  });
589
775
 
590
- test("idempotent: re-creating session with shannon does not rewrite the file", async () => {
591
- process.env.CLAUDE_BINARY = "shannon";
776
+ test("idempotent: re-creating legacy bridge session does not rewrite the file", async () => {
777
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
592
778
  const cwd = "/some/abs/cwd";
593
779
  const adapter = new ClaudeAdapter();
594
780
  await adapter.createSession(makeConfig({ cwd }));
@@ -607,7 +793,7 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
607
793
  },
608
794
  }),
609
795
  );
610
- process.env.CLAUDE_BINARY = "shannon";
796
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
611
797
  const adapter = new ClaudeAdapter();
612
798
  await adapter.createSession(makeConfig({ cwd: "/new/cwd" }));
613
799
 
@@ -625,4 +811,15 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
625
811
  const exists = await Bun.file(join(homeDir, ".claude.json")).exists();
626
812
  expect(exists).toBe(false);
627
813
  });
814
+
815
+ test("SWARM_USE_CLAUDE_BRIDGE=true writes hasTrustDialogAccepted for config.cwd", async () => {
816
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
817
+ const cwd = "/some/bridge/cwd";
818
+ const adapter = new ClaudeAdapter();
819
+ await adapter.createSession(makeConfig({ cwd }));
820
+
821
+ const data = JSON.parse(await readFile(join(homeDir, ".claude.json"), "utf-8"));
822
+ expect(data.projects[cwd].hasTrustDialogAccepted).toBe(true);
823
+ expect(data.projects[cwd].hasCompletedProjectOnboarding).toBe(true);
824
+ });
628
825
  });