@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.
- package/README.md +2 -1
- package/openapi.json +585 -5
- package/package.json +1 -1
- package/src/be/db.ts +337 -1
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/modelsdev-cache.json +42352 -38595
- package/src/be/scripts/typecheck.ts +49 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +216 -6
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/tool-usage.ts +6 -3
- package/src/be/seed-scripts/index.ts +20 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/runner.ts +43 -2
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +10 -0
- package/src/http/script-runs.ts +555 -0
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +60 -13
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +205 -0
- package/src/scripts-runtime/sdk-allowlist.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +60 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +60 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +147 -4
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +49 -2
- package/src/tests/seed-scripts.test.ts +347 -2
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/db-query.ts +16 -6
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +4 -1
- package/src/types.ts +52 -0
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- 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.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 3.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
97
|
-
// composite
|
|
98
|
-
expect(sessionSystem.length).toBe(
|
|
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
|
});
|