@desplega.ai/agent-swarm 1.90.0 → 1.92.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 (96) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +803 -150
  3. package/package.json +5 -5
  4. package/src/artifact-sdk/server.ts +2 -1
  5. package/src/be/db.ts +337 -1
  6. package/src/be/memory/providers/sqlite-store.ts +6 -1
  7. package/src/be/memory/types.ts +1 -0
  8. package/src/be/migrations/083_script_workflows.sql +51 -0
  9. package/src/be/modelsdev-cache.json +42352 -38595
  10. package/src/be/scripts/typecheck.ts +181 -1
  11. package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
  12. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  13. package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
  14. package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
  15. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  16. package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
  17. package/src/be/seed-scripts/index.ts +54 -0
  18. package/src/be/seed-skills/index.ts +7 -0
  19. package/src/be/swarm-config-guard.ts +17 -0
  20. package/src/commands/artifact.ts +3 -2
  21. package/src/commands/profile-sync.ts +310 -0
  22. package/src/commands/runner.ts +134 -3
  23. package/src/hooks/hook.ts +32 -9
  24. package/src/http/db-query.ts +20 -5
  25. package/src/http/index.ts +57 -0
  26. package/src/http/integrations.ts +6 -1
  27. package/src/http/mcp-bridge.ts +117 -0
  28. package/src/http/mcp-oauth.ts +97 -39
  29. package/src/http/memory.ts +5 -2
  30. package/src/http/openapi.ts +2 -2
  31. package/src/http/pages-public.ts +10 -11
  32. package/src/http/pages.ts +7 -11
  33. package/src/http/script-runs.ts +555 -0
  34. package/src/http/scripts.ts +24 -1
  35. package/src/http/utils.ts +11 -4
  36. package/src/jira/app.ts +2 -3
  37. package/src/jira/webhook-lifecycle.ts +2 -1
  38. package/src/linear/app.ts +2 -3
  39. package/src/prompts/session-templates.ts +24 -4
  40. package/src/providers/claude-adapter.ts +86 -13
  41. package/src/script-workflows/executor.ts +110 -0
  42. package/src/script-workflows/harness.ts +73 -0
  43. package/src/script-workflows/label-lint.ts +51 -0
  44. package/src/script-workflows/limits.ts +22 -0
  45. package/src/script-workflows/supervisor.ts +139 -0
  46. package/src/script-workflows/workflow-ctx.ts +205 -0
  47. package/src/scripts-runtime/executors/native.ts +1 -0
  48. package/src/scripts-runtime/sdk-allowlist.ts +124 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +198 -3
  50. package/src/scripts-runtime/types/stdlib.d.ts +287 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
  52. package/src/server.ts +2 -0
  53. package/src/slack/handlers.ts +11 -4
  54. package/src/slack/message-text.ts +98 -0
  55. package/src/slack/thread-buffer.ts +5 -3
  56. package/src/tests/claude-adapter-binary.test.ts +147 -4
  57. package/src/tests/claude-adapter-otel.test.ts +85 -1
  58. package/src/tests/db-query.test.ts +28 -0
  59. package/src/tests/error-tracker.test.ts +121 -0
  60. package/src/tests/harness-provider-resolution.test.ts +33 -0
  61. package/src/tests/hook-registration-nudge.test.ts +69 -0
  62. package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
  63. package/src/tests/mcp-tools.test.ts +6 -0
  64. package/src/tests/pages-public-html.test.ts +41 -0
  65. package/src/tests/pages-public-json-redirect.test.ts +37 -2
  66. package/src/tests/profile-sync.test.ts +282 -0
  67. package/src/tests/prompt-template-session.test.ts +34 -5
  68. package/src/tests/script-runs-http.test.ts +278 -0
  69. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  70. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  71. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  72. package/src/tests/scripts-runtime.test.ts +33 -0
  73. package/src/tests/seed-scripts.test.ts +347 -2
  74. package/src/tests/slack-message-text.test.ts +250 -0
  75. package/src/tests/system-default-skills.test.ts +40 -0
  76. package/src/tools/create-metric.ts +2 -3
  77. package/src/tools/create-page.ts +3 -6
  78. package/src/tools/db-query.ts +16 -6
  79. package/src/tools/memory-rate.ts +2 -1
  80. package/src/tools/memory-search.ts +1 -0
  81. package/src/tools/register-kapso-number.ts +2 -4
  82. package/src/tools/request-human-input.ts +2 -1
  83. package/src/tools/script-common.ts +2 -4
  84. package/src/tools/script-run.ts +7 -0
  85. package/src/tools/script-runs.ts +123 -0
  86. package/src/tools/slack-read.ts +12 -3
  87. package/src/tools/tool-config.ts +4 -1
  88. package/src/types.ts +52 -0
  89. package/src/utils/constants.ts +58 -8
  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/skills/pages/content.md +205 -55
  94. package/templates/skills/script-workflows/config.json +14 -0
  95. package/templates/skills/script-workflows/content.md +68 -0
  96. package/templates/skills/swarm-scripts/content.md +45 -7
@@ -18,6 +18,7 @@ import type { SlackFile } from "./files";
18
18
  import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
19
19
  // Side-effect import: registers all Slack event templates in the in-memory registry
20
20
  import "./templates";
21
+ import { extractSlackMessageText } from "./message-text";
21
22
  import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
22
23
  import { registerTreeMessage } from "./watcher";
23
24
 
@@ -173,6 +174,8 @@ interface ThreadMessage {
173
174
  subtype?: string;
174
175
  text?: string;
175
176
  ts: string;
177
+ attachments?: Array<{ fallback?: string; text?: string; title?: string; pretext?: string }>;
178
+ blocks?: unknown[];
176
179
  }
177
180
 
178
181
  // Cache for bot's own user ID (avoids redundant auth.test calls)
@@ -286,8 +289,11 @@ async function getThreadContext(
286
289
  });
287
290
 
288
291
  const messages = (result.messages || []) as ThreadMessage[];
289
- // Filter out the current message only (keep bot messages for context)
290
- const previousMessages = messages.filter((m) => m.ts !== currentTs && m.text);
292
+ // Filter out the current message; include any message with extractable text
293
+ const previousMessages = messages.filter((m) => {
294
+ if (m.ts === currentTs) return false;
295
+ return extractSlackMessageText(m) !== "";
296
+ });
291
297
 
292
298
  if (previousMessages.length === 0) return "";
293
299
 
@@ -298,13 +304,14 @@ async function getThreadContext(
298
304
  const isBotMessage =
299
305
  m.user === botUserId || m.bot_id !== undefined || m.subtype === "bot_message";
300
306
 
307
+ const text = extractSlackMessageText(m);
301
308
  if (isBotMessage) {
302
309
  // Bot/agent message - truncate if too long
303
- const truncatedText = m.text && m.text.length > 500 ? `${m.text.slice(0, 500)}...` : m.text;
310
+ const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
304
311
  formattedMessages.push(`[Agent]: ${truncatedText}`);
305
312
  } else {
306
313
  const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
307
- formattedMessages.push(`${userName}: ${m.text}`);
314
+ formattedMessages.push(`${userName}: ${text}`);
308
315
  }
309
316
  }
310
317
 
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared utility for extracting displayable text from a Slack message.
3
+ *
4
+ * Slack integration/alert apps (Datadog, PagerDuty, GitHub) often post with
5
+ * an empty top-level `text` field and put all content in legacy `attachments`
6
+ * or Block Kit `blocks`. This helper falls back through those layers so callers
7
+ * never silently drop those messages.
8
+ */
9
+
10
+ interface SlackAttachment {
11
+ fallback?: string;
12
+ text?: string;
13
+ title?: string;
14
+ pretext?: string;
15
+ }
16
+
17
+ // Internal shape used only inside the helper; callers pass `blocks?: unknown[]`
18
+ interface SlackBlockInternal {
19
+ type?: string;
20
+ text?: { type?: string; text?: string };
21
+ /** section blocks may use fields[] instead of (or alongside) a top-level text object */
22
+ fields?: Array<{ type?: string; text?: string }>;
23
+ elements?: unknown[];
24
+ }
25
+
26
+ export interface SlackMessageLike {
27
+ text?: string;
28
+ attachments?: SlackAttachment[];
29
+ /** Typed as unknown[] so any Slack SDK block variant is accepted without casting. */
30
+ blocks?: unknown[];
31
+ }
32
+
33
+ /**
34
+ * Recursively collect plain text from a Slack rich_text node tree.
35
+ *
36
+ * Handles text leaf nodes plus all container types that carry child elements:
37
+ * rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted.
38
+ */
39
+ function collectRichTextParts(node: unknown, parts: string[]): void {
40
+ if (node == null || typeof node !== "object") return;
41
+ const n = node as { type?: string; text?: string; elements?: unknown[] };
42
+ if (n.type === "text" && n.text) {
43
+ parts.push(n.text);
44
+ }
45
+ if (Array.isArray(n.elements)) {
46
+ for (const child of n.elements) {
47
+ collectRichTextParts(child, parts);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Return the best displayable text for a Slack message.
54
+ *
55
+ * Priority:
56
+ * 1. `msg.text` (non-empty)
57
+ * 2. `msg.attachments[]` — joins `fallback || text || title || pretext` for each
58
+ * 3. `msg.blocks[]` — extracts text from section (text + fields) and rich_text blocks
59
+ * 4. `""` if nothing found
60
+ */
61
+ export function extractSlackMessageText(msg: SlackMessageLike): string {
62
+ if (msg.text?.trim()) return msg.text;
63
+
64
+ // Legacy attachments (Datadog, PagerDuty, GitHub alert apps)
65
+ if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
66
+ const parts = msg.attachments
67
+ .filter((a): a is SlackAttachment => a != null && typeof a === "object")
68
+ .map((a) => a.fallback || a.text || a.title || a.pretext || "")
69
+ .filter(Boolean);
70
+ if (parts.length > 0) return parts.join("\n");
71
+ }
72
+
73
+ // Block Kit blocks
74
+ if (Array.isArray(msg.blocks) && msg.blocks.length > 0) {
75
+ const parts: string[] = [];
76
+ for (const rawBlock of msg.blocks) {
77
+ if (rawBlock == null || typeof rawBlock !== "object") continue;
78
+ const block = rawBlock as SlackBlockInternal;
79
+ if (block.type === "section") {
80
+ if (block.text?.text) parts.push(block.text.text);
81
+ if (Array.isArray(block.fields)) {
82
+ for (const field of block.fields) {
83
+ if (field != null && typeof field === "object" && field.text) {
84
+ parts.push(field.text);
85
+ }
86
+ }
87
+ }
88
+ } else if (block.type === "rich_text" && Array.isArray(block.elements)) {
89
+ for (const el of block.elements) {
90
+ collectRichTextParts(el, parts);
91
+ }
92
+ }
93
+ }
94
+ if (parts.length > 0) return parts.join("\n");
95
+ }
96
+
97
+ return "";
98
+ }
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
4
4
  import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
5
5
  import { getSlackApp } from "./app";
6
6
  import { buildBufferFlushBlocks } from "./blocks";
7
+ import { extractSlackMessageText } from "./message-text";
7
8
  import { registerTreeMessage } from "./watcher";
8
9
 
9
10
  interface BufferedMessage {
@@ -93,15 +94,16 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
93
94
  if (messages.length === 0) return "";
94
95
 
95
96
  const formatted = messages
96
- .filter((m) => m.text)
97
+ .filter((m) => extractSlackMessageText(m) !== "")
97
98
  .map((m) => {
98
99
  const msg = m as Record<string, unknown>;
99
100
  const isBotMessage = msg.bot_id !== undefined || msg.subtype === "bot_message";
101
+ const text = extractSlackMessageText(m);
100
102
  if (isBotMessage) {
101
- const truncated = m.text && m.text.length > 500 ? `${m.text.slice(0, 500)}...` : m.text;
103
+ const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
102
104
  return `[Agent]: ${truncated}`;
103
105
  }
104
- return `<@${m.user}>: ${m.text}`;
106
+ return `<@${m.user}>: ${text}`;
105
107
  })
106
108
  .join("\n");
107
109
 
@@ -6,10 +6,12 @@
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
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
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 contains "shannon"
13
+ * or claude-bridge is enabled, createSession throws if `tmux` is not on PATH.
14
+ * 4. Trust pre-seed — when the resolved binary contains "shannon", the
13
15
  * adapter writes `projects[cwd].hasTrustDialogAccepted: true` to
14
16
  * `$HOME/.claude.json` before spawning. Idempotent. No-op for "claude".
15
17
  *
@@ -27,8 +29,10 @@ import { join } from "node:path";
27
29
  import {
28
30
  ClaudeAdapter,
29
31
  parseClaudeBinary,
32
+ parseClaudeBridgeEnabled,
30
33
  preseedClaudeTrustDialog,
31
34
  resolveClaudeBinary,
35
+ resolveClaudeBridgeEnabled,
32
36
  } from "../providers/claude-adapter";
33
37
  import type { ProviderSessionConfig } from "../providers/types";
34
38
 
@@ -150,6 +154,45 @@ describe("resolveClaudeBinary precedence", () => {
150
154
  });
151
155
  });
152
156
 
157
+ describe("SWARM_USE_CLAUDE_BRIDGE boolean parsing", () => {
158
+ test("true/1 enable claude-bridge", () => {
159
+ expect(parseClaudeBridgeEnabled("true")).toBe(true);
160
+ expect(parseClaudeBridgeEnabled("TRUE")).toBe(true);
161
+ expect(parseClaudeBridgeEnabled(" 1 ")).toBe(true);
162
+ });
163
+
164
+ test("false/0/unset and invalid values are disabled", () => {
165
+ expect(parseClaudeBridgeEnabled("false")).toBe(false);
166
+ expect(parseClaudeBridgeEnabled("0")).toBe(false);
167
+ expect(parseClaudeBridgeEnabled(undefined)).toBe(false);
168
+ expect(parseClaudeBridgeEnabled("yes")).toBe(false);
169
+ });
170
+
171
+ test("resolvedEnv wins over fallbackEnv", () => {
172
+ expect(
173
+ resolveClaudeBridgeEnabled(
174
+ { SWARM_USE_CLAUDE_BRIDGE: "false" },
175
+ { SWARM_USE_CLAUDE_BRIDGE: "true" },
176
+ ),
177
+ ).toBe(false);
178
+ expect(
179
+ resolveClaudeBridgeEnabled(
180
+ { SWARM_USE_CLAUDE_BRIDGE: "1" },
181
+ { SWARM_USE_CLAUDE_BRIDGE: "0" },
182
+ ),
183
+ ).toBe(true);
184
+ });
185
+
186
+ test("empty resolvedEnv value falls through to fallbackEnv", () => {
187
+ expect(
188
+ resolveClaudeBridgeEnabled(
189
+ { SWARM_USE_CLAUDE_BRIDGE: " " },
190
+ { SWARM_USE_CLAUDE_BRIDGE: "true" },
191
+ ),
192
+ ).toBe(true);
193
+ });
194
+ });
195
+
153
196
  describe("preseedClaudeTrustDialog", () => {
154
197
  let homeDir: string;
155
198
 
@@ -241,6 +284,7 @@ describe("preseedClaudeTrustDialog", () => {
241
284
  describe("CLAUDE_BINARY env override", () => {
242
285
  // Cache the originals and restore after each test so the suite stays clean.
243
286
  let originalClaudeBinary: string | undefined;
287
+ let originalUseClaudeBridge: string | undefined;
244
288
  let originalOauthToken: string | undefined;
245
289
  let originalHome: string | undefined;
246
290
  let homeDir: string;
@@ -250,11 +294,13 @@ describe("CLAUDE_BINARY env override", () => {
250
294
 
251
295
  beforeEach(async () => {
252
296
  originalClaudeBinary = process.env.CLAUDE_BINARY;
297
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
253
298
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
254
299
  originalHome = process.env.HOME;
255
300
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
256
301
  process.env.HOME = homeDir;
257
302
  delete process.env.CLAUDE_BINARY;
303
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
258
304
  // Credential check runs before binary resolution; satisfy it.
259
305
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
260
306
 
@@ -285,6 +331,11 @@ describe("CLAUDE_BINARY env override", () => {
285
331
  } else {
286
332
  process.env.CLAUDE_BINARY = originalClaudeBinary;
287
333
  }
334
+ if (originalUseClaudeBridge === undefined) {
335
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
336
+ } else {
337
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
338
+ }
288
339
  if (originalOauthToken === undefined) {
289
340
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
290
341
  } else {
@@ -422,10 +473,65 @@ describe("CLAUDE_BINARY env override", () => {
422
473
 
423
474
  expect(spawnedArgs[0][0]).toBe("shannon");
424
475
  });
476
+
477
+ test("SWARM_USE_CLAUDE_BRIDGE=true routes through installed claude-bridge", async () => {
478
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
479
+
480
+ const adapter = new ClaudeAdapter();
481
+ await adapter.createSession(makeConfig());
482
+
483
+ const argv = spawnedArgs[0];
484
+ expect(argv[0]).toBe("claude-bridge");
485
+ expect(argv).toContain("--model");
486
+ expect(argv).toContain("-p");
487
+ });
488
+
489
+ test("SWARM_USE_CLAUDE_BRIDGE=1 wins over CLAUDE_BINARY=shannon", async () => {
490
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "1";
491
+ process.env.CLAUDE_BINARY = "shannon";
492
+
493
+ const adapter = new ClaudeAdapter();
494
+ await adapter.createSession(makeConfig());
495
+
496
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
497
+ });
498
+
499
+ test("config.env SWARM_USE_CLAUDE_BRIDGE=true is reloadable and wins over process.env false", async () => {
500
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "false";
501
+
502
+ const adapter = new ClaudeAdapter();
503
+ await adapter.createSession(
504
+ makeConfig({
505
+ env: {
506
+ SWARM_USE_CLAUDE_BRIDGE: "true",
507
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
508
+ } as Record<string, string>,
509
+ }),
510
+ );
511
+
512
+ expect(spawnedArgs[0][0]).toBe("claude-bridge");
513
+ });
514
+
515
+ test("config.env SWARM_USE_CLAUDE_BRIDGE=false disables process.env true", async () => {
516
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
517
+
518
+ const adapter = new ClaudeAdapter();
519
+ await adapter.createSession(
520
+ makeConfig({
521
+ env: {
522
+ SWARM_USE_CLAUDE_BRIDGE: "false",
523
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
524
+ } as Record<string, string>,
525
+ }),
526
+ );
527
+
528
+ expect(spawnedArgs[0][0]).toBe("claude");
529
+ });
425
530
  });
426
531
 
427
532
  describe("Shannon tmux fail-fast gate", () => {
428
533
  let originalClaudeBinary: string | undefined;
534
+ let originalUseClaudeBridge: string | undefined;
429
535
  let originalOauthToken: string | undefined;
430
536
  let originalHome: string | undefined;
431
537
  let homeDir: string;
@@ -434,11 +540,13 @@ describe("Shannon tmux fail-fast gate", () => {
434
540
 
435
541
  beforeEach(async () => {
436
542
  originalClaudeBinary = process.env.CLAUDE_BINARY;
543
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
437
544
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
438
545
  originalHome = process.env.HOME;
439
546
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
440
547
  process.env.HOME = homeDir;
441
548
  delete process.env.CLAUDE_BINARY;
549
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
442
550
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
443
551
  spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
444
552
  whichSpy = spyOn(Bun, "which");
@@ -458,6 +566,11 @@ describe("Shannon tmux fail-fast gate", () => {
458
566
  } else {
459
567
  process.env.CLAUDE_BINARY = originalClaudeBinary;
460
568
  }
569
+ if (originalUseClaudeBridge === undefined) {
570
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
571
+ } else {
572
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
573
+ }
461
574
  if (originalOauthToken === undefined) {
462
575
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
463
576
  } else {
@@ -520,10 +633,22 @@ describe("Shannon tmux fail-fast gate", () => {
520
633
  const adapter = new ClaudeAdapter();
521
634
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
522
635
  });
636
+
637
+ test("SWARM_USE_CLAUDE_BRIDGE=true triggers the tmux check", async () => {
638
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
639
+ whichSpy.mockImplementation((name: string) => {
640
+ if (name === "tmux") return null;
641
+ return null;
642
+ });
643
+
644
+ const adapter = new ClaudeAdapter();
645
+ await expect(adapter.createSession(makeConfig())).rejects.toThrow(/SWARM_USE_CLAUDE_BRIDGE/);
646
+ });
523
647
  });
524
648
 
525
649
  describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
526
650
  let originalClaudeBinary: string | undefined;
651
+ let originalUseClaudeBridge: string | undefined;
527
652
  let originalOauthToken: string | undefined;
528
653
  let originalHome: string | undefined;
529
654
  let homeDir: string;
@@ -532,11 +657,13 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
532
657
 
533
658
  beforeEach(async () => {
534
659
  originalClaudeBinary = process.env.CLAUDE_BINARY;
660
+ originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
535
661
  originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
536
662
  originalHome = process.env.HOME;
537
663
  homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-trust-test-"));
538
664
  process.env.HOME = homeDir;
539
665
  delete process.env.CLAUDE_BINARY;
666
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
540
667
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
541
668
  spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
542
669
  whichSpy = spyOn(Bun, "which").mockImplementation((name: string) => {
@@ -559,6 +686,11 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
559
686
  } else {
560
687
  process.env.CLAUDE_BINARY = originalClaudeBinary;
561
688
  }
689
+ if (originalUseClaudeBridge === undefined) {
690
+ delete process.env.SWARM_USE_CLAUDE_BRIDGE;
691
+ } else {
692
+ process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
693
+ }
562
694
  if (originalOauthToken === undefined) {
563
695
  delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
564
696
  } else {
@@ -625,4 +757,15 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
625
757
  const exists = await Bun.file(join(homeDir, ".claude.json")).exists();
626
758
  expect(exists).toBe(false);
627
759
  });
760
+
761
+ test("SWARM_USE_CLAUDE_BRIDGE=true does NOT use the legacy shannon trust pre-seed", async () => {
762
+ process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
763
+ const adapter = new ClaudeAdapter();
764
+ await adapter.createSession(makeConfig({ cwd: "/some/abs/cwd" }));
765
+
766
+ // claude-bridge owns its own pre-clear flow; the adapter should not write
767
+ // the legacy shannon pre-seed file before spawning.
768
+ const exists = await Bun.file(join(homeDir, ".claude.json")).exists();
769
+ expect(exists).toBe(false);
770
+ });
628
771
  });
@@ -15,7 +15,11 @@
15
15
 
16
16
  import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
17
17
  import { type Span, trace } from "@opentelemetry/api";
18
- import { buildClaudeCodeOtelEnv, ClaudeAdapter } from "../providers/claude-adapter";
18
+ import {
19
+ buildClaudeCodeOtelEnv,
20
+ buildClaudeCodeRuntimeEnv,
21
+ ClaudeAdapter,
22
+ } from "../providers/claude-adapter";
19
23
  import type { ProviderSessionConfig } from "../providers/types";
20
24
 
21
25
  // Example IDs from the task — trace af2c8371…, span adff4f24… (the orphaned
@@ -136,6 +140,51 @@ describe("buildClaudeCodeOtelEnv — gate on", () => {
136
140
  });
137
141
  });
138
142
 
143
+ describe("buildClaudeCodeRuntimeEnv", () => {
144
+ test("sets memory/privacy defaults for ephemeral Claude Code sessions", () => {
145
+ const env = buildClaudeCodeRuntimeEnv({});
146
+
147
+ expect(env.ENABLE_TOOL_SEARCH).toBe("true");
148
+ expect(env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING).toBe("1");
149
+ expect(env.CLAUDE_CODE_SKIP_PROMPT_HISTORY).toBe("1");
150
+ expect(env.CLAUDE_CODE_DISABLE_ATTACHMENTS).toBe("1");
151
+ expect(env.DISABLE_FEEDBACK_COMMAND).toBe("1");
152
+ expect(env.DISABLE_BUG_COMMAND).toBe("1");
153
+ });
154
+
155
+ test("always disables Claude Code Statsig/DNT telemetry", () => {
156
+ const env = buildClaudeCodeRuntimeEnv({});
157
+
158
+ expect(env.DISABLE_TELEMETRY).toBe("1");
159
+ expect(env.DO_NOT_TRACK).toBe("1");
160
+ });
161
+
162
+ test("keeps Claude Code Statsig/DNT opt-out separate from OTel config", () => {
163
+ for (const sourceEnv of [
164
+ { CLAUDE_CODE_ENABLE_TELEMETRY: "1" },
165
+ { CLAUDE_CODE_ENABLE_TELEMETRY: "true" },
166
+ { OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test" },
167
+ { OTEL_TRACES_EXPORTER: "otlp" },
168
+ { OTEL_METRICS_EXPORTER: "otlp" },
169
+ { OTEL_LOGS_EXPORTER: "otlp" },
170
+ ]) {
171
+ const env = buildClaudeCodeRuntimeEnv(sourceEnv);
172
+ expect(env.DISABLE_TELEMETRY).toBe("1");
173
+ expect(env.DO_NOT_TRACK).toBe("1");
174
+ }
175
+ });
176
+
177
+ test("ignores empty OTel env values for runtime defaults", () => {
178
+ const env = buildClaudeCodeRuntimeEnv({
179
+ OTEL_EXPORTER_OTLP_ENDPOINT: "",
180
+ CLAUDE_CODE_ENABLE_TELEMETRY: "0",
181
+ });
182
+
183
+ expect(env.DISABLE_TELEMETRY).toBe("1");
184
+ expect(env.DO_NOT_TRACK).toBe("1");
185
+ });
186
+ });
187
+
139
188
  // ─── Spawn integration through ClaudeAdapter.createSession ────────────────────
140
189
 
141
190
  /** Fake Bun.Subprocess — exits cleanly with no output. */
@@ -222,4 +271,39 @@ describe("ClaudeSession spawn env — SWARM_ENABLE_CLAUDE_CODE_OTEL", () => {
222
271
  expect(env?.ENABLE_PROMPT_CACHING_1H).toBe("1");
223
272
  expect(env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("test-token");
224
273
  });
274
+
275
+ test("spawn env carries Claude Code runtime guardrails", async () => {
276
+ const adapter = new ClaudeAdapter();
277
+ await adapter.createSession(makeConfig({ env: { CLAUDE_CODE_OAUTH_TOKEN: "test-token" } }));
278
+
279
+ expect(spawnedEnvs).toHaveLength(1);
280
+ const env = spawnedEnvs[0];
281
+ expect(env?.ENABLE_TOOL_SEARCH).toBe("true");
282
+ expect(env?.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING).toBe("1");
283
+ expect(env?.CLAUDE_CODE_SKIP_PROMPT_HISTORY).toBe("1");
284
+ expect(env?.CLAUDE_CODE_DISABLE_ATTACHMENTS).toBe("1");
285
+ expect(env?.DISABLE_FEEDBACK_COMMAND).toBe("1");
286
+ expect(env?.DISABLE_BUG_COMMAND).toBe("1");
287
+ expect(env?.DISABLE_TELEMETRY).toBe("1");
288
+ expect(env?.DO_NOT_TRACK).toBe("1");
289
+ expect(env?.CLAUDE_CODE_DISABLE_THINKING).toBeUndefined();
290
+ });
291
+
292
+ test("spawn env keeps Statsig/DNT opt-out even when OTel is configured", async () => {
293
+ const adapter = new ClaudeAdapter();
294
+ await adapter.createSession(
295
+ makeConfig({
296
+ env: {
297
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
298
+ CLAUDE_CODE_ENABLE_TELEMETRY: "1",
299
+ OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test",
300
+ },
301
+ }),
302
+ );
303
+
304
+ expect(spawnedEnvs).toHaveLength(1);
305
+ const env = spawnedEnvs[0];
306
+ expect(env?.DISABLE_TELEMETRY).toBe("1");
307
+ expect(env?.DO_NOT_TRACK).toBe("1");
308
+ });
225
309
  });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DbQueryInputSchema, resolveDbQuerySql } from "../http/db-query";
3
+
4
+ describe("db-query input compatibility", () => {
5
+ test("canonical sql input resolves to sql", () => {
6
+ const parsed = DbQueryInputSchema.parse({ sql: "SELECT 1", params: [] });
7
+
8
+ expect(resolveDbQuerySql(parsed)).toBe("SELECT 1");
9
+ });
10
+
11
+ test("legacy query input remains a runtime alias", () => {
12
+ const parsed = DbQueryInputSchema.parse({ query: "SELECT 2" });
13
+
14
+ expect(resolveDbQuerySql(parsed)).toBe("SELECT 2");
15
+ });
16
+
17
+ test("sql takes precedence when both sql and query are present", () => {
18
+ const parsed = DbQueryInputSchema.parse({ sql: "SELECT 3", query: "SELECT 4" });
19
+
20
+ expect(resolveDbQuerySql(parsed)).toBe("SELECT 3");
21
+ });
22
+
23
+ test("rejects input without sql or query", () => {
24
+ const parsed = DbQueryInputSchema.safeParse({});
25
+
26
+ expect(parsed.success).toBe(false);
27
+ });
28
+ });