@desplega.ai/agent-swarm 1.91.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 (58) hide show
  1. package/README.md +2 -1
  2. package/openapi.json +585 -5
  3. package/package.json +1 -1
  4. package/src/be/db.ts +337 -1
  5. package/src/be/migrations/083_script_workflows.sql +51 -0
  6. package/src/be/modelsdev-cache.json +42352 -38595
  7. package/src/be/scripts/typecheck.ts +49 -0
  8. package/src/be/seed-scripts/catalog/compound-insights.ts +216 -6
  9. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
  10. package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
  11. package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
  12. package/src/be/seed-scripts/index.ts +20 -2
  13. package/src/be/seed-skills/index.ts +7 -0
  14. package/src/be/swarm-config-guard.ts +17 -0
  15. package/src/commands/runner.ts +43 -2
  16. package/src/http/db-query.ts +20 -5
  17. package/src/http/index.ts +10 -0
  18. package/src/http/script-runs.ts +555 -0
  19. package/src/prompts/session-templates.ts +24 -4
  20. package/src/providers/claude-adapter.ts +60 -13
  21. package/src/script-workflows/executor.ts +110 -0
  22. package/src/script-workflows/harness.ts +73 -0
  23. package/src/script-workflows/label-lint.ts +51 -0
  24. package/src/script-workflows/limits.ts +22 -0
  25. package/src/script-workflows/supervisor.ts +139 -0
  26. package/src/script-workflows/workflow-ctx.ts +205 -0
  27. package/src/scripts-runtime/sdk-allowlist.ts +3 -0
  28. package/src/scripts-runtime/types/stdlib.d.ts +60 -0
  29. package/src/scripts-runtime/types/swarm-sdk.d.ts +60 -0
  30. package/src/server.ts +2 -0
  31. package/src/slack/handlers.ts +11 -4
  32. package/src/slack/message-text.ts +98 -0
  33. package/src/slack/thread-buffer.ts +5 -3
  34. package/src/tests/claude-adapter-binary.test.ts +147 -4
  35. package/src/tests/db-query.test.ts +28 -0
  36. package/src/tests/error-tracker.test.ts +121 -0
  37. package/src/tests/harness-provider-resolution.test.ts +33 -0
  38. package/src/tests/mcp-tools.test.ts +6 -0
  39. package/src/tests/prompt-template-session.test.ts +34 -5
  40. package/src/tests/script-runs-http.test.ts +278 -0
  41. package/src/tests/script-workflows-label-lint.test.ts +43 -0
  42. package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
  43. package/src/tests/scripts-mcp-e2e.test.ts +49 -2
  44. package/src/tests/seed-scripts.test.ts +347 -2
  45. package/src/tests/slack-message-text.test.ts +250 -0
  46. package/src/tests/system-default-skills.test.ts +40 -0
  47. package/src/tools/db-query.ts +16 -6
  48. package/src/tools/script-runs.ts +123 -0
  49. package/src/tools/slack-read.ts +12 -3
  50. package/src/tools/tool-config.ts +4 -1
  51. package/src/types.ts +52 -0
  52. package/src/utils/error-tracker.ts +40 -1
  53. package/src/utils/internal-ai/complete-structured.ts +10 -4
  54. package/src/workflows/executors/raw-llm.ts +76 -59
  55. package/templates/skills/pages/content.md +205 -55
  56. package/templates/skills/script-workflows/config.json +14 -0
  57. package/templates/skills/script-workflows/content.md +68 -0
  58. package/templates/skills/swarm-scripts/content.md +2 -3
@@ -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
  });
@@ -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
+ });
@@ -1,8 +1,14 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
+ CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
4
+ isCodexCreditsExhaustedMessage,
5
+ isRateLimitMessage,
6
+ MAX_RATE_LIMIT_RESET_MS,
7
+ MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
3
8
  parseCodexRateLimitResetTime,
4
9
  parseRateLimitResetTime,
5
10
  parseStderrForErrors,
11
+ resolveCodexCreditsExhaustedCooldownMs,
6
12
  SessionErrorTracker,
7
13
  trackErrorFromJson,
8
14
  } from "../utils/error-tracker";
@@ -567,6 +573,121 @@ describe("parseRateLimitResetTime", () => {
567
573
  });
568
574
  });
569
575
 
576
+ describe("isCodexCreditsExhaustedMessage", () => {
577
+ const CANONICAL =
578
+ "Your workspace is out of credits. Ask your workspace owner to refill in order to continue.";
579
+
580
+ test("returns true for the canonical credits-exhausted message", () => {
581
+ expect(isCodexCreditsExhaustedMessage(CANONICAL)).toBe(true);
582
+ });
583
+
584
+ test("matches 'out of credits' fragment", () => {
585
+ expect(isCodexCreditsExhaustedMessage("Your workspace is out of credits.")).toBe(true);
586
+ });
587
+
588
+ test("matches 'refill in order to continue' fragment", () => {
589
+ expect(isCodexCreditsExhaustedMessage("Please refill in order to continue.")).toBe(true);
590
+ });
591
+
592
+ test("matches 'workspace owner to refill' fragment", () => {
593
+ expect(isCodexCreditsExhaustedMessage("Ask your workspace owner to refill credits.")).toBe(
594
+ true,
595
+ );
596
+ });
597
+
598
+ test("is case-insensitive", () => {
599
+ expect(isCodexCreditsExhaustedMessage("OUT OF CREDITS")).toBe(true);
600
+ });
601
+
602
+ test("returns false for unrelated errors", () => {
603
+ expect(isCodexCreditsExhaustedMessage("No conversation found with session ID abc123")).toBe(
604
+ false,
605
+ );
606
+ expect(isCodexCreditsExhaustedMessage("Authentication failed")).toBe(false);
607
+ expect(isCodexCreditsExhaustedMessage("Rate limit exceeded")).toBe(false);
608
+ expect(isCodexCreditsExhaustedMessage("Connection timeout")).toBe(false);
609
+ });
610
+
611
+ test("returns false for bare 'refill' without qualifying context", () => {
612
+ expect(isCodexCreditsExhaustedMessage("Please refill your coffee")).toBe(false);
613
+ });
614
+ });
615
+
616
+ describe("isRateLimitMessage — Codex credits-exhausted integration", () => {
617
+ const CANONICAL =
618
+ "Your workspace is out of credits. Ask your workspace owner to refill in order to continue.";
619
+
620
+ test("returns true for canonical Codex credits-exhausted message", () => {
621
+ expect(isRateLimitMessage(CANONICAL)).toBe(true);
622
+ });
623
+
624
+ test("still returns true for standard rate-limit messages", () => {
625
+ expect(isRateLimitMessage("Rate limit exceeded")).toBe(true);
626
+ expect(isRateLimitMessage("429 Too Many Requests")).toBe(true);
627
+ expect(isRateLimitMessage("You've hit your weekly limit")).toBe(true);
628
+ });
629
+
630
+ test("still returns false for unrelated errors", () => {
631
+ expect(isRateLimitMessage("No conversation found with session ID abc123")).toBe(false);
632
+ expect(isRateLimitMessage("Authentication failed")).toBe(false);
633
+ expect(isRateLimitMessage("Server error 500")).toBe(false);
634
+ });
635
+ });
636
+
637
+ describe("resolveCodexCreditsExhaustedCooldownMs", () => {
638
+ test("absent (undefined) → default constant", () => {
639
+ expect(resolveCodexCreditsExhaustedCooldownMs(undefined)).toBe(
640
+ CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
641
+ );
642
+ });
643
+
644
+ test("null → default constant", () => {
645
+ expect(resolveCodexCreditsExhaustedCooldownMs(null)).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
646
+ });
647
+
648
+ test("empty string → default constant", () => {
649
+ expect(resolveCodexCreditsExhaustedCooldownMs("")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
650
+ });
651
+
652
+ test("non-numeric string → default constant", () => {
653
+ expect(resolveCodexCreditsExhaustedCooldownMs("abc")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
654
+ });
655
+
656
+ test("zero → default constant", () => {
657
+ expect(resolveCodexCreditsExhaustedCooldownMs("0")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
658
+ });
659
+
660
+ test("negative → default constant", () => {
661
+ expect(resolveCodexCreditsExhaustedCooldownMs("-5")).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
662
+ });
663
+
664
+ test("valid in-range string (30m) → parsed value", () => {
665
+ expect(resolveCodexCreditsExhaustedCooldownMs("1800000")).toBe(1_800_000);
666
+ });
667
+
668
+ test("valid in-range number (30m) → parsed value", () => {
669
+ expect(resolveCodexCreditsExhaustedCooldownMs(1_800_000)).toBe(1_800_000);
670
+ });
671
+
672
+ test("below floor → clamped to MIN", () => {
673
+ expect(resolveCodexCreditsExhaustedCooldownMs("1000")).toBe(
674
+ MIN_CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS,
675
+ );
676
+ });
677
+
678
+ test("above ceiling (8d) → clamped to MAX", () => {
679
+ expect(resolveCodexCreditsExhaustedCooldownMs(String(8 * 24 * 60 * 60 * 1000))).toBe(
680
+ MAX_RATE_LIMIT_RESET_MS,
681
+ );
682
+ });
683
+
684
+ test("partial-numeric strings → default constant (not silently truncated)", () => {
685
+ for (const bad of ["60000ms", "1.5", "123abc", "1e5"]) {
686
+ expect(resolveCodexCreditsExhaustedCooldownMs(bad)).toBe(CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS);
687
+ }
688
+ });
689
+ });
690
+
570
691
  describe("Codex/Claude coexistence — single tracker handles both providers", () => {
571
692
  test("processRateLimitEvent (Claude) and processCodexUsageLimitMessage (Codex) both stash into getRateLimitResetAt", () => {
572
693
  // Claude path: processRateLimitEvent
@@ -138,6 +138,39 @@ describe("validateConfigValue", () => {
138
138
  expect(validateConfigValue("HARNESS_PROVIDER", 42)).not.toBeNull();
139
139
  expect(validateConfigValue("HARNESS_PROVIDER", null)).not.toBeNull();
140
140
  });
141
+
142
+ test("accepts a valid CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", () => {
143
+ expect(validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", "7200000")).toBeNull();
144
+ expect(validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", "1800000")).toBeNull();
145
+ // case-insensitive key lookup
146
+ expect(validateConfigValue("codex_credits_exhausted_cooldown_ms", "60000")).toBeNull();
147
+ });
148
+
149
+ test("rejects non-positive / non-numeric CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", () => {
150
+ for (const bad of ["abc", "0", "-5", ""]) {
151
+ const err = validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", bad);
152
+ expect(err).not.toBeNull();
153
+ expect(err).toMatch(/CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS/);
154
+ }
155
+ });
156
+
157
+ test("rejects partial-numeric CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS values", () => {
158
+ for (const bad of ["60000ms", "1.5", "123abc", "1e5", " 60000 ms"]) {
159
+ const err = validateConfigValue("CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS", bad);
160
+ expect(err).not.toBeNull();
161
+ expect(err).toMatch(/CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS/);
162
+ }
163
+ });
164
+
165
+ test("accepts only boolean-like SWARM_USE_CLAUDE_BRIDGE values", () => {
166
+ for (const value of ["true", "false", "1", "0", " TRUE "]) {
167
+ expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", value)).toBeNull();
168
+ }
169
+ expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", "yes")).toMatch(
170
+ /SWARM_USE_CLAUDE_BRIDGE/,
171
+ );
172
+ expect(validateConfigValue("SWARM_USE_CLAUDE_BRIDGE", true)).toMatch(/SWARM_USE_CLAUDE_BRIDGE/);
173
+ });
141
174
  });
142
175
 
143
176
  // ─── getResolvedConfig — scope precedence for HARNESS_PROVIDER ───────────────
@@ -55,6 +55,12 @@ describe("script MCP tools", () => {
55
55
  "Remove a swarm-shared script from the catalog. Versions table preserves history.",
56
56
  "script-query-types":
57
57
  "Fetch the signature + the auto-generated `swarm-sdk.d.ts` (derived from the live MCP tool registry) + the `stdlib.d.ts` blobs — for IDE-style introspection before authoring or running a script. The same types are used by `script-upsert`'s typecheck pass, so they are authoritative.",
58
+ "launch-script-run":
59
+ "Launch a durable one-off script workflow run. The run executes in the background and can be inspected with get-script-run for terminal status and journal entries.",
60
+ "get-script-run":
61
+ "Get a durable script workflow run by ID, including its journal entries for swarm-script, raw-llm, and agent-task steps.",
62
+ "list-script-runs":
63
+ "List durable script workflow runs, optionally filtered by status or agent ID.",
58
64
  };
59
65
 
60
66
  for (const [name, description] of Object.entries(expected)) {
@@ -54,7 +54,7 @@ describe("Session templates — registration", () => {
54
54
  await ensureTemplatesRegistered();
55
55
  });
56
56
 
57
- test("all 14 system templates are registered", () => {
57
+ test("all 15 system templates are registered", () => {
58
58
  const systemTemplates = [
59
59
  "system.agent.role",
60
60
  "system.agent.register",
@@ -66,6 +66,7 @@ describe("Session templates — registration", () => {
66
66
  "system.agent.agent_fs",
67
67
  "system.agent.self_awareness",
68
68
  "system.agent.context_mode",
69
+ "system.agent.seed_scripts",
69
70
 
70
71
  "system.agent.system",
71
72
  "system.agent.services",
@@ -90,12 +91,12 @@ describe("Session templates — registration", () => {
90
91
  }
91
92
  });
92
93
 
93
- test("total of 20 session/system templates registered", () => {
94
+ test("total of 21 session/system templates registered", () => {
94
95
  const all = getAllTemplateDefinitions();
95
96
  const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
96
- // 20 = the original 19 + `system.session.worker.pi` (a pi-specific worker
97
- // composite that omits the context_mode block see session-templates.ts).
98
- expect(sessionSystem.length).toBe(20);
97
+ // 21 = the original 19 + `system.session.worker.pi` + `system.agent.seed_scripts`.
98
+ // The pi composite omits script nudges because pi workers do not have MCP.
99
+ expect(sessionSystem.length).toBe(21);
99
100
  });
100
101
  });
101
102
 
@@ -187,6 +188,15 @@ describe("Session templates — individual resolution", () => {
187
188
  expect(result.text).toContain("batch_execute");
188
189
  });
189
190
 
191
+ test("system.agent.seed_scripts points agents at task-start scripts", () => {
192
+ const result = resolveTemplate("system.agent.seed_scripts", {});
193
+ expect(result.text).toContain("Pre-built Seed Scripts");
194
+ expect(result.text).toContain("task-context-gathering");
195
+ expect(result.text).toContain("smart-recall");
196
+ expect(result.text).toContain("script-search");
197
+ expect(result.text).not.toContain("compound-insights");
198
+ });
199
+
190
200
  // system.agent.guidelines was removed — its content was redundant with worker/lead templates
191
201
 
192
202
  test("system.agent.system contains package info", () => {
@@ -240,6 +250,7 @@ describe("Session templates — composite resolution", () => {
240
250
 
241
251
  // Contains context_mode
242
252
  expect(result.text).toContain("Context Window Management");
253
+ expect(result.text).toContain("Pre-built Seed Scripts");
243
254
 
244
255
  // Guidelines template was removed (redundant with lead/worker templates)
245
256
 
@@ -275,6 +286,7 @@ describe("Session templates — composite resolution", () => {
275
286
 
276
287
  // Contains context_mode
277
288
  expect(result.text).toContain("Context Window Management");
289
+ expect(result.text).toContain("Pre-built Seed Scripts");
278
290
 
279
291
  // Guidelines template was removed (redundant with lead/worker templates)
280
292
 
@@ -309,6 +321,8 @@ describe("Session templates — composite resolution", () => {
309
321
  expect(workerResult.text).toContain("join-swarm");
310
322
  expect(leadResult.text).toContain("How You Are Built");
311
323
  expect(workerResult.text).toContain("How You Are Built");
324
+ expect(leadResult.text).toContain("Pre-built Seed Scripts");
325
+ expect(workerResult.text).toContain("Pre-built Seed Scripts");
312
326
 
313
327
  // Lead has lead content, not worker
314
328
  expect(leadResult.text).toContain("CRITICAL: You are a coordinator");
@@ -343,6 +357,7 @@ describe("Session templates — getBasePrompt integration", () => {
343
357
  expect(result).toContain("join-swarm");
344
358
  expect(result).toContain("store-progress");
345
359
  expect(result).toContain("How You Are Built");
360
+ expect(result).toContain("Pre-built Seed Scripts");
346
361
  expect(result).toContain("System packages available");
347
362
 
348
363
  // Conditional sections (services included by default)
@@ -365,8 +380,22 @@ describe("Session templates — getBasePrompt integration", () => {
365
380
  expect(result).toContain("your role is: lead");
366
381
  expect(result).toContain("integration-test-lead");
367
382
  expect(result).toContain("CRITICAL: You are a coordinator");
383
+ expect(result).toContain("Pre-built Seed Scripts");
368
384
 
369
385
  // Should NOT have worker content
370
386
  expect(result).not.toContain("task-action");
371
387
  });
388
+
389
+ test("getBasePrompt excludes seed_scripts for pi worker", async () => {
390
+ const { getBasePrompt } = await import("../prompts/base-prompt");
391
+ const result = await getBasePrompt({
392
+ role: "worker",
393
+ agentId: "integration-test-pi",
394
+ swarmUrl: "swarm.test.com",
395
+ provider: "pi",
396
+ });
397
+
398
+ expect(result).not.toContain("Pre-built Seed Scripts");
399
+ expect(result).not.toContain("task-context-gathering");
400
+ });
372
401
  });