@desplega.ai/agent-swarm 1.92.0 → 1.92.2

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 (90) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +416 -20
  6. package/src/be/memory/boot-reembed.ts +85 -0
  7. package/src/be/memory/constants.ts +44 -2
  8. package/src/be/memory/providers/openai-embedding.ts +15 -5
  9. package/src/be/memory/providers/sqlite-store.ts +325 -76
  10. package/src/be/memory/reranker.ts +35 -17
  11. package/src/be/memory/types.ts +43 -0
  12. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  13. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  14. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  15. package/src/be/migrations/087_skill_files.sql +19 -0
  16. package/src/be/modelsdev-cache.json +5622 -2543
  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 +465 -0
  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 +34 -439
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  25. package/src/be/seed-scripts/index.ts +32 -4
  26. package/src/be/seed-skills/index.ts +0 -7
  27. package/src/be/skill-sync.ts +91 -7
  28. package/src/commands/runner.ts +6 -2
  29. package/src/heartbeat/templates.ts +20 -16
  30. package/src/http/index.ts +50 -7
  31. package/src/http/mcp-user.ts +23 -0
  32. package/src/http/mcp.ts +58 -0
  33. package/src/http/memory.ts +62 -0
  34. package/src/http/pages.ts +1 -1
  35. package/src/http/script-runs.ts +2 -0
  36. package/src/http/scripts.ts +39 -2
  37. package/src/http/skills.ts +225 -0
  38. package/src/providers/claude-adapter.ts +56 -24
  39. package/src/script-workflows/workflow-ctx.ts +7 -3
  40. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  41. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  42. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  43. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  44. package/src/server.ts +2 -0
  45. package/src/tasks/worker-follow-up.ts +12 -0
  46. package/src/tests/claude-adapter-binary.test.ts +135 -81
  47. package/src/tests/create-page-tool.test.ts +19 -2
  48. package/src/tests/heartbeat-checklist.test.ts +36 -0
  49. package/src/tests/mcp-transport-gc.test.ts +58 -0
  50. package/src/tests/memory-e2e.test.ts +6 -6
  51. package/src/tests/memory-health-endpoint.test.ts +78 -0
  52. package/src/tests/memory-rater-e2e.test.ts +4 -5
  53. package/src/tests/memory-reranker.test.ts +135 -124
  54. package/src/tests/memory-store.test.ts +221 -1
  55. package/src/tests/memory.test.ts +13 -12
  56. package/src/tests/pages-http.test.ts +20 -2
  57. package/src/tests/pages-storage.test.ts +26 -0
  58. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  59. package/src/tests/seed-scripts.test.ts +328 -3
  60. package/src/tests/skill-files-http.test.ts +171 -0
  61. package/src/tests/skill-files.test.ts +162 -0
  62. package/src/tests/skill-get-file-tool.test.ts +110 -0
  63. package/src/tests/skill-sync.test.ts +125 -6
  64. package/src/tests/task-cascade-fail.test.ts +304 -0
  65. package/src/tools/create-page.ts +2 -2
  66. package/src/tools/skills/index.ts +1 -0
  67. package/src/tools/skills/skill-get-file.ts +80 -0
  68. package/src/tools/tool-config.ts +2 -1
  69. package/src/types.ts +20 -0
  70. package/src/utils/internal-ai/complete-structured.ts +2 -2
  71. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  72. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  73. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  74. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  75. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  76. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  77. package/templates/skills/agentmail-sending/content.md +6 -7
  78. package/templates/skills/desloppify/content.md +8 -9
  79. package/templates/skills/jira-interaction/content.md +25 -33
  80. package/templates/skills/kapso-whatsapp/content.md +29 -30
  81. package/templates/skills/linear-interaction/content.md +8 -9
  82. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  83. package/templates/skills/sprite-cli/content.md +4 -5
  84. package/templates/skills/turso-interaction/content.md +14 -17
  85. package/templates/skills/workflow-iterate/content.md +38 -391
  86. package/templates/skills/x-api-interactions/content.md +4 -6
  87. package/templates/workflows/llm-safe-release-context/config.json +13 -0
  88. package/templates/workflows/llm-safe-release-context/content.md +69 -0
  89. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  90. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -5,15 +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"`).
8
+ * whitespace-separated command strings.
9
9
  * 2. Claude Bridge routing — SWARM_USE_CLAUDE_BRIDGE=true/1 forces the
10
10
  * installed `claude-bridge` argv prefix and wins over
11
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
15
- * adapter writes `projects[cwd].hasTrustDialogAccepted: true` to
16
- * `$HOME/.claude.json` before spawning. Idempotent. No-op for "claude".
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".
17
19
  *
18
20
  * `Bun.spawn` is stubbed so the tests don't actually exec anything; we read
19
21
  * the argv off the call args. `Bun.which` is stubbed for the tmux gate so
@@ -36,6 +38,10 @@ import {
36
38
  } from "../providers/claude-adapter";
37
39
  import type { ProviderSessionConfig } from "../providers/types";
38
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
+
39
45
  /** Minimal config — empty apiUrl/apiKey/agentId skips the MCP-server fetch. */
40
46
  function makeConfig(overrides: Partial<ProviderSessionConfig> = {}): ProviderSessionConfig {
41
47
  return {
@@ -83,36 +89,54 @@ describe("parseClaudeBinary", () => {
83
89
 
84
90
  test("single token → one-element array", () => {
85
91
  expect(parseClaudeBinary("claude")).toEqual(["claude"]);
86
- expect(parseClaudeBinary("shannon")).toEqual(["shannon"]);
87
- 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
+ ]);
88
96
  });
89
97
 
90
98
  test("command string → whitespace-split argv", () => {
91
- expect(parseClaudeBinary("bunx @dexh/shannon")).toEqual(["bunx", "@dexh/shannon"]);
92
- 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
+ ]);
93
108
  });
94
109
 
95
110
  test("version-pinned → preserves the version suffix", () => {
96
- 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
+ ]);
97
115
  });
98
116
 
99
117
  test("multiple-space tolerance → trims + collapses", () => {
100
- expect(parseClaudeBinary(" bunx shannon ")).toEqual(["bunx", "shannon"]);
101
- 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
+ ]);
102
126
  });
103
127
  });
104
128
 
105
129
  describe("resolveClaudeBinary precedence", () => {
106
130
  test("resolvedEnv wins over fallbackEnv (swarm_config overrides process.env)", () => {
107
- const resolvedEnv = { CLAUDE_BINARY: "shannon" };
131
+ const resolvedEnv = { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY };
108
132
  const fallbackEnv = { CLAUDE_BINARY: "claude" };
109
- expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe("shannon");
133
+ expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
110
134
  });
111
135
 
112
136
  test("falls back to fallbackEnv when resolvedEnv is absent", () => {
113
137
  const resolvedEnv = {};
114
- const fallbackEnv = { CLAUDE_BINARY: "bunx @dexh/shannon" };
115
- 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);
116
140
  });
117
141
 
118
142
  test("both absent → 'claude' default", () => {
@@ -121,12 +145,12 @@ describe("resolveClaudeBinary precedence", () => {
121
145
 
122
146
  test("empty / whitespace-only resolvedEnv value falls through to fallbackEnv", () => {
123
147
  // `.trim() || …` falls through on empty/whitespace.
124
- expect(resolveClaudeBinary({ CLAUDE_BINARY: "" }, { CLAUDE_BINARY: "shannon" })).toBe(
125
- "shannon",
126
- );
127
- expect(resolveClaudeBinary({ CLAUDE_BINARY: " " }, { CLAUDE_BINARY: "shannon" })).toBe(
128
- "shannon",
129
- );
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);
130
154
  });
131
155
 
132
156
  test("empty fallback after empty resolved → 'claude' default", () => {
@@ -134,8 +158,8 @@ describe("resolveClaudeBinary precedence", () => {
134
158
  });
135
159
 
136
160
  test("command-string passes through unchanged (caller does the argv split)", () => {
137
- const resolvedEnv = { CLAUDE_BINARY: "bunx @dexh/shannon@1.2.3" };
138
- 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`);
139
163
  });
140
164
 
141
165
  test("fallbackEnv defaults to process.env when omitted", () => {
@@ -291,6 +315,7 @@ describe("CLAUDE_BINARY env override", () => {
291
315
  let spawnSpy: ReturnType<typeof spyOn>;
292
316
  let whichSpy: ReturnType<typeof spyOn>;
293
317
  let spawnedArgs: Array<readonly string[]>;
318
+ let spawnedEnvs: Array<Record<string, string> | undefined>;
294
319
 
295
320
  beforeEach(async () => {
296
321
  originalClaudeBinary = process.env.CLAUDE_BINARY;
@@ -305,8 +330,10 @@ describe("CLAUDE_BINARY env override", () => {
305
330
  process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
306
331
 
307
332
  spawnedArgs = [];
308
- spawnSpy = spyOn(Bun, "spawn").mockImplementation(((cmd: readonly string[]) => {
333
+ spawnedEnvs = [];
334
+ spawnSpy = spyOn(Bun, "spawn").mockImplementation(((cmd: readonly string[], opts?: unknown) => {
309
335
  spawnedArgs.push(cmd);
336
+ spawnedEnvs.push((opts as { env?: Record<string, string> } | undefined)?.env);
310
337
  return makeFakeProc();
311
338
  }) as typeof Bun.spawn);
312
339
 
@@ -352,68 +379,68 @@ describe("CLAUDE_BINARY env override", () => {
352
379
  expect(argv[0]).toBe("claude");
353
380
  });
354
381
 
355
- test("override: argv[0] is 'shannon' when CLAUDE_BINARY=shannon", async () => {
356
- 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;
357
384
 
358
385
  const adapter = new ClaudeAdapter();
359
386
  await adapter.createSession(makeConfig());
360
387
 
361
388
  const argv = spawnedArgs[0];
362
- expect(argv[0]).toBe("shannon");
389
+ expect(argv[0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
363
390
  });
364
391
 
365
- test("custom path: argv[0] is the absolute path when CLAUDE_BINARY=/usr/local/bin/shannon", async () => {
366
- 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}`;
367
394
 
368
395
  const adapter = new ClaudeAdapter();
369
396
  await adapter.createSession(makeConfig());
370
397
 
371
- expect(spawnedArgs[0][0]).toBe("/usr/local/bin/shannon");
398
+ expect(spawnedArgs[0][0]).toBe(`/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`);
372
399
  });
373
400
 
374
- test("command string: 'bunx @dexh/shannon' → argv[0..1] is ['bunx', '@dexh/shannon']", async () => {
375
- 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;
376
403
 
377
404
  const adapter = new ClaudeAdapter();
378
405
  await adapter.createSession(makeConfig());
379
406
 
380
407
  const argv = spawnedArgs[0];
381
408
  expect(argv[0]).toBe("bunx");
382
- expect(argv[1]).toBe("@dexh/shannon");
409
+ expect(argv[1]).toBe(LEGACY_BRIDGE_COMPAT_PACKAGE);
383
410
  // Claude args follow.
384
411
  expect(argv).toContain("--model");
385
412
  expect(argv).toContain("-p");
386
413
  });
387
414
 
388
- test("version-pinned command string: argv[0..1] = ['bunx', '@dexh/shannon@1.2.3']", async () => {
389
- 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`;
390
417
 
391
418
  const adapter = new ClaudeAdapter();
392
419
  await adapter.createSession(makeConfig());
393
420
 
394
421
  const argv = spawnedArgs[0];
395
422
  expect(argv[0]).toBe("bunx");
396
- expect(argv[1]).toBe("@dexh/shannon@1.2.3");
423
+ expect(argv[1]).toBe(`${LEGACY_BRIDGE_COMPAT_PACKAGE}@1.2.3`);
397
424
  });
398
425
 
399
- test("multiple-space tolerance: ' bunx shannon ' argv = ['bunx', 'shannon', ...]", async () => {
400
- 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} `;
401
428
 
402
429
  const adapter = new ClaudeAdapter();
403
430
  await adapter.createSession(makeConfig());
404
431
 
405
432
  const argv = spawnedArgs[0];
406
433
  expect(argv[0]).toBe("bunx");
407
- expect(argv[1]).toBe("shannon");
434
+ expect(argv[1]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
408
435
  expect(argv).toContain("--model");
409
436
  });
410
437
 
411
- test("argv[1..] (after prefix) matches between default 'claude' and command-string 'bunx @dexh/shannon'", async () => {
412
- 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;
413
440
  const adapter = new ClaudeAdapter();
414
441
  await adapter.createSession(makeConfig());
415
442
  // Drop the 2-element prefix.
416
- const argvShannon = spawnedArgs[0].slice(2);
443
+ const argvLegacyBridge = spawnedArgs[0].slice(2);
417
444
 
418
445
  spawnedArgs = [];
419
446
  delete process.env.CLAUDE_BINARY;
@@ -421,47 +448,47 @@ describe("CLAUDE_BINARY env override", () => {
421
448
  // Drop the 1-element prefix.
422
449
  const argvClaude = spawnedArgs[0].slice(1);
423
450
 
424
- expect(argvShannon).toEqual(argvClaude);
451
+ expect(argvLegacyBridge).toEqual(argvClaude);
425
452
  });
426
453
 
427
454
  test("swarm_config overlay (config.env) wins over process.env CLAUDE_BINARY", async () => {
428
455
  // process.env says "claude" — but the runner's resolvedEnv overlay (passed
429
- // 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
430
457
  // HARNESS_PROVIDER reload path.
431
458
  process.env.CLAUDE_BINARY = "claude";
432
459
 
433
460
  const adapter = new ClaudeAdapter();
434
461
  await adapter.createSession(
435
462
  makeConfig({
436
- env: { CLAUDE_BINARY: "shannon", CLAUDE_CODE_OAUTH_TOKEN: "test-token" } as Record<
437
- string,
438
- string
439
- >,
463
+ env: {
464
+ CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY,
465
+ CLAUDE_CODE_OAUTH_TOKEN: "test-token",
466
+ } as Record<string, string>,
440
467
  }),
441
468
  );
442
469
 
443
- expect(spawnedArgs[0][0]).toBe("shannon");
470
+ expect(spawnedArgs[0][0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
444
471
  });
445
472
 
446
- 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 () => {
447
474
  delete process.env.CLAUDE_BINARY;
448
475
 
449
476
  const adapter = new ClaudeAdapter();
450
477
  await adapter.createSession(
451
478
  makeConfig({
452
479
  env: {
453
- CLAUDE_BINARY: "bunx @dexh/shannon",
480
+ CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_COMMAND,
454
481
  CLAUDE_CODE_OAUTH_TOKEN: "test-token",
455
482
  } as Record<string, string>,
456
483
  }),
457
484
  );
458
485
 
459
486
  expect(spawnedArgs[0][0]).toBe("bunx");
460
- expect(spawnedArgs[0][1]).toBe("@dexh/shannon");
487
+ expect(spawnedArgs[0][1]).toBe(LEGACY_BRIDGE_COMPAT_PACKAGE);
461
488
  });
462
489
 
463
490
  test("config.env without CLAUDE_BINARY falls back to process.env", async () => {
464
- process.env.CLAUDE_BINARY = "shannon";
491
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
465
492
 
466
493
  const adapter = new ClaudeAdapter();
467
494
  await adapter.createSession(
@@ -471,7 +498,7 @@ describe("CLAUDE_BINARY env override", () => {
471
498
  }),
472
499
  );
473
500
 
474
- expect(spawnedArgs[0][0]).toBe("shannon");
501
+ expect(spawnedArgs[0][0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
475
502
  });
476
503
 
477
504
  test("SWARM_USE_CLAUDE_BRIDGE=true routes through installed claude-bridge", async () => {
@@ -486,9 +513,36 @@ describe("CLAUDE_BINARY env override", () => {
486
513
  expect(argv).toContain("-p");
487
514
  });
488
515
 
489
- test("SWARM_USE_CLAUDE_BRIDGE=1 wins over CLAUDE_BINARY=shannon", async () => {
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 () => {
490
544
  process.env.SWARM_USE_CLAUDE_BRIDGE = "1";
491
- process.env.CLAUDE_BINARY = "shannon";
545
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
492
546
 
493
547
  const adapter = new ClaudeAdapter();
494
548
  await adapter.createSession(makeConfig());
@@ -529,7 +583,7 @@ describe("CLAUDE_BINARY env override", () => {
529
583
  });
530
584
  });
531
585
 
532
- describe("Shannon tmux fail-fast gate", () => {
586
+ describe("Claude Bridge tmux fail-fast gate", () => {
533
587
  let originalClaudeBinary: string | undefined;
534
588
  let originalUseClaudeBridge: string | undefined;
535
589
  let originalOauthToken: string | undefined;
@@ -578,8 +632,8 @@ describe("Shannon tmux fail-fast gate", () => {
578
632
  }
579
633
  });
580
634
 
581
- test("sad path: rejects with tmux-mentioning error when CLAUDE_BINARY=shannon and tmux is missing", async () => {
582
- 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;
583
637
  whichSpy.mockImplementation((name: string) => {
584
638
  if (name === "tmux") return null;
585
639
  return `/usr/bin/${name}`;
@@ -589,8 +643,8 @@ describe("Shannon tmux fail-fast gate", () => {
589
643
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
590
644
  });
591
645
 
592
- test("happy path: does not throw when CLAUDE_BINARY=shannon and tmux IS on PATH", async () => {
593
- 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;
594
648
  whichSpy.mockImplementation((name: string) => {
595
649
  if (name === "tmux") return "/usr/bin/tmux";
596
650
  return null;
@@ -600,7 +654,7 @@ describe("Shannon tmux fail-fast gate", () => {
600
654
  await expect(adapter.createSession(makeConfig())).resolves.toBeDefined();
601
655
  });
602
656
 
603
- 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 () => {
604
658
  process.env.CLAUDE_BINARY = "claude";
605
659
  whichSpy.mockImplementation((name: string) => {
606
660
  if (name === "tmux") return null;
@@ -612,8 +666,8 @@ describe("Shannon tmux fail-fast gate", () => {
612
666
  await expect(adapter.createSession(makeConfig())).resolves.toBeDefined();
613
667
  });
614
668
 
615
- test("custom shannon path (e.g. /usr/local/bin/shannon) still triggers the tmux check", async () => {
616
- 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}`;
617
671
  whichSpy.mockImplementation((name: string) => {
618
672
  if (name === "tmux") return null;
619
673
  return null;
@@ -623,8 +677,8 @@ describe("Shannon tmux fail-fast gate", () => {
623
677
  await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
624
678
  });
625
679
 
626
- test("command-string CLAUDE_BINARY='bunx @dexh/shannon' still triggers the tmux check", async () => {
627
- 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;
628
682
  whichSpy.mockImplementation((name: string) => {
629
683
  if (name === "tmux") return null;
630
684
  return null;
@@ -698,8 +752,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
698
752
  }
699
753
  });
700
754
 
701
- test("CLAUDE_BINARY=shannon writes hasTrustDialogAccepted for config.cwd", async () => {
702
- 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;
703
757
  const cwd = "/some/abs/cwd";
704
758
  const adapter = new ClaudeAdapter();
705
759
  await adapter.createSession(makeConfig({ cwd }));
@@ -709,8 +763,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
709
763
  expect(data.projects[cwd].hasCompletedProjectOnboarding).toBe(true);
710
764
  });
711
765
 
712
- test("CLAUDE_BINARY='bunx @dexh/shannon' (command string) also triggers the pre-seed", async () => {
713
- 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;
714
768
  const cwd = "/some/other/cwd";
715
769
  const adapter = new ClaudeAdapter();
716
770
  await adapter.createSession(makeConfig({ cwd }));
@@ -719,8 +773,8 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
719
773
  expect(data.projects[cwd].hasTrustDialogAccepted).toBe(true);
720
774
  });
721
775
 
722
- test("idempotent: re-creating session with shannon does not rewrite the file", async () => {
723
- 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;
724
778
  const cwd = "/some/abs/cwd";
725
779
  const adapter = new ClaudeAdapter();
726
780
  await adapter.createSession(makeConfig({ cwd }));
@@ -739,7 +793,7 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
739
793
  },
740
794
  }),
741
795
  );
742
- process.env.CLAUDE_BINARY = "shannon";
796
+ process.env.CLAUDE_BINARY = LEGACY_BRIDGE_COMPAT_BINARY;
743
797
  const adapter = new ClaudeAdapter();
744
798
  await adapter.createSession(makeConfig({ cwd: "/new/cwd" }));
745
799
 
@@ -758,14 +812,14 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
758
812
  expect(exists).toBe(false);
759
813
  });
760
814
 
761
- test("SWARM_USE_CLAUDE_BRIDGE=true does NOT use the legacy shannon trust pre-seed", async () => {
815
+ test("SWARM_USE_CLAUDE_BRIDGE=true writes hasTrustDialogAccepted for config.cwd", async () => {
762
816
  process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
817
+ const cwd = "/some/bridge/cwd";
763
818
  const adapter = new ClaudeAdapter();
764
- await adapter.createSession(makeConfig({ cwd: "/some/abs/cwd" }));
819
+ await adapter.createSession(makeConfig({ cwd }));
765
820
 
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);
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);
770
824
  });
771
825
  });
@@ -63,14 +63,13 @@ describe("create_page MCP tool", () => {
63
63
  }
64
64
  });
65
65
 
66
- test("first call creates a row + returns shareable URLs", async () => {
66
+ test("first call creates an authed row by default + returns shareable URLs", async () => {
67
67
  const tool = buildServer();
68
68
  const result = (await tool.handler(
69
69
  {
70
70
  title: "Hello Page",
71
71
  body: "<h1>hello</h1>",
72
72
  contentType: "text/html",
73
- authMode: "public",
74
73
  },
75
74
  fakeMeta,
76
75
  )) as {
@@ -97,6 +96,24 @@ describe("create_page MCP tool", () => {
97
96
  const row = getPageBySlug(agentId, "hello-page");
98
97
  expect(row).not.toBeNull();
99
98
  expect(row!.body).toBe("<h1>hello</h1>");
99
+ expect(row!.authMode).toBe("authed");
100
+ });
101
+
102
+ test("explicit public auth mode is preserved", async () => {
103
+ const tool = buildServer();
104
+ await tool.handler(
105
+ {
106
+ title: "Public Page",
107
+ body: "<h1>public</h1>",
108
+ contentType: "text/html",
109
+ authMode: "public",
110
+ },
111
+ fakeMeta,
112
+ );
113
+
114
+ const row = getPageBySlug(agentId, "public-page");
115
+ expect(row).not.toBeNull();
116
+ expect(row!.authMode).toBe("public");
100
117
  });
101
118
 
102
119
  test("re-running with the same slug upserts + bumps edit-counter", async () => {
@@ -271,6 +271,26 @@ describe("Heartbeat Checklist", () => {
271
271
  expect(tasks[0]!.task).toContain("Review blocked tasks");
272
272
  });
273
273
 
274
+ test("created task enforces HEARTBEAT tracked-item cap and seeded audit call", async () => {
275
+ const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
276
+ updateAgentProfile(lead.id, {
277
+ heartbeatMd: "- Watch PR #123 until 2026-06-07\n",
278
+ });
279
+
280
+ await checkHeartbeatChecklist();
281
+
282
+ const tasks = getDb()
283
+ .query("SELECT task FROM agent_tasks WHERE taskType = 'heartbeat-checklist'")
284
+ .all() as Array<{ task: string }>;
285
+ expect(tasks.length).toBe(1);
286
+ expect(tasks[0]!.task).toContain("Active Blockers + Watch Items + Open Discussion");
287
+ expect(tasks[0]!.task).toContain("≤10 items");
288
+ expect(tasks[0]!.task).toContain("20 is the absolute max");
289
+ expect(tasks[0]!.task).toContain("script-run");
290
+ expect(tasks[0]!.task).toContain("Heartbeat Audit");
291
+ expect(tasks[0]!.task).toContain("Rule #11");
292
+ });
293
+
274
294
  test("created task has correct tags", async () => {
275
295
  const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
276
296
  updateAgentProfile(lead.id, {
@@ -369,6 +389,22 @@ describe("Heartbeat Checklist", () => {
369
389
  expect(tasks[0]!.task).toContain("Check Slack for unaddressed requests");
370
390
  });
371
391
 
392
+ test("boot-triage task enforces HEARTBEAT cap and seeded boot-triage call", async () => {
393
+ createAgent({ name: "lead", isLead: true, status: "idle" });
394
+
395
+ await createBootTriageTask();
396
+
397
+ const tasks = getDb()
398
+ .query("SELECT task FROM agent_tasks WHERE taskType = 'boot-triage'")
399
+ .all() as Array<{ task: string }>;
400
+ expect(tasks[0]!.task).toContain("Active Blockers + Watch Items + Open Discussion");
401
+ expect(tasks[0]!.task).toContain("≤10 items");
402
+ expect(tasks[0]!.task).toContain("20 is the absolute max");
403
+ expect(tasks[0]!.task).toContain("script-run");
404
+ expect(tasks[0]!.task).toContain("boot-triage");
405
+ expect(tasks[0]!.task).toContain("one read-only call");
406
+ });
407
+
372
408
  test("dedup: skips when active boot-triage task exists", async () => {
373
409
  const lead = createAgent({ name: "lead", isLead: true, status: "idle" });
374
410
  updateAgentProfile(lead.id, {
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { closeIdleMcpTransports } from "../http/mcp";
4
+ import { closeIdleMcpUserTransports } from "../http/mcp-user";
5
+
6
+ function fakeTransport(onClose: () => void): StreamableHTTPServerTransport {
7
+ return {
8
+ close: onClose,
9
+ } as StreamableHTTPServerTransport;
10
+ }
11
+
12
+ describe("MCP transport idle GC", () => {
13
+ test("closes and deletes stale owner transports", () => {
14
+ const closed: string[] = [];
15
+ const transports: Record<string, StreamableHTTPServerTransport> = {
16
+ fresh: fakeTransport(() => closed.push("fresh")),
17
+ stale: fakeTransport(() => closed.push("stale")),
18
+ unknown: fakeTransport(() => closed.push("unknown")),
19
+ };
20
+ const activity = {
21
+ fresh: 9_500,
22
+ stale: 1_000,
23
+ };
24
+
25
+ const removed = closeIdleMcpTransports(transports, activity, {
26
+ now: 10_000,
27
+ idleTimeoutMs: 1_000,
28
+ });
29
+
30
+ expect(removed).toBe(1);
31
+ expect(closed).toEqual(["stale"]);
32
+ expect(transports.stale).toBeUndefined();
33
+ expect(activity.stale).toBeUndefined();
34
+ expect(transports.fresh).toBeDefined();
35
+ expect(transports.unknown).toBeDefined();
36
+ expect(activity.unknown).toBe(10_000);
37
+ });
38
+
39
+ test("deletes user ownership metadata for stale user transports", () => {
40
+ const closed: string[] = [];
41
+ const transports: Record<string, StreamableHTTPServerTransport> = {
42
+ stale: fakeTransport(() => closed.push("stale")),
43
+ };
44
+ const sessionUsers = { stale: "user_1" };
45
+ const activity = { stale: 1_000 };
46
+
47
+ const removed = closeIdleMcpUserTransports(transports, sessionUsers, activity, {
48
+ now: 10_000,
49
+ idleTimeoutMs: 1_000,
50
+ });
51
+
52
+ expect(removed).toBe(1);
53
+ expect(closed).toEqual(["stale"]);
54
+ expect(transports.stale).toBeUndefined();
55
+ expect(sessionUsers.stale).toBeUndefined();
56
+ expect(activity.stale).toBeUndefined();
57
+ });
58
+ });