@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.
- package/README.md +1 -1
- package/openapi.json +276 -3
- package/package.json +6 -6
- package/plugin/skills/pages/SKILL.md +5 -2
- package/src/be/db.ts +416 -20
- package/src/be/memory/boot-reembed.ts +85 -0
- package/src/be/memory/constants.ts +44 -2
- package/src/be/memory/providers/openai-embedding.ts +15 -5
- package/src/be/memory/providers/sqlite-store.ts +325 -76
- package/src/be/memory/reranker.ts +35 -17
- package/src/be/memory/types.ts +43 -0
- package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
- package/src/be/migrations/085_script_runs_kind.sql +9 -0
- package/src/be/migrations/086_pages_default_authed.sql +64 -0
- package/src/be/migrations/087_skill_files.sql +19 -0
- package/src/be/modelsdev-cache.json +5622 -2543
- package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
- package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
- package/src/be/seed-scripts/catalog/compound-insights.ts +465 -0
- package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
- package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
- package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
- package/src/be/seed-scripts/index.ts +32 -4
- package/src/be/seed-skills/index.ts +0 -7
- package/src/be/skill-sync.ts +91 -7
- package/src/commands/runner.ts +6 -2
- package/src/heartbeat/templates.ts +20 -16
- package/src/http/index.ts +50 -7
- package/src/http/mcp-user.ts +23 -0
- package/src/http/mcp.ts +58 -0
- package/src/http/memory.ts +62 -0
- package/src/http/pages.ts +1 -1
- package/src/http/script-runs.ts +2 -0
- package/src/http/scripts.ts +39 -2
- package/src/http/skills.ts +225 -0
- package/src/providers/claude-adapter.ts +56 -24
- package/src/script-workflows/workflow-ctx.ts +7 -3
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/swarm-sdk.ts +13 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/server.ts +2 -0
- package/src/tasks/worker-follow-up.ts +12 -0
- package/src/tests/claude-adapter-binary.test.ts +135 -81
- package/src/tests/create-page-tool.test.ts +19 -2
- package/src/tests/heartbeat-checklist.test.ts +36 -0
- package/src/tests/mcp-transport-gc.test.ts +58 -0
- package/src/tests/memory-e2e.test.ts +6 -6
- package/src/tests/memory-health-endpoint.test.ts +78 -0
- package/src/tests/memory-rater-e2e.test.ts +4 -5
- package/src/tests/memory-reranker.test.ts +135 -124
- package/src/tests/memory-store.test.ts +221 -1
- package/src/tests/memory.test.ts +13 -12
- package/src/tests/pages-http.test.ts +20 -2
- package/src/tests/pages-storage.test.ts +26 -0
- package/src/tests/scripts-mcp-e2e.test.ts +53 -0
- package/src/tests/seed-scripts.test.ts +328 -3
- package/src/tests/skill-files-http.test.ts +171 -0
- package/src/tests/skill-files.test.ts +162 -0
- package/src/tests/skill-get-file-tool.test.ts +110 -0
- package/src/tests/skill-sync.test.ts +125 -6
- package/src/tests/task-cascade-fail.test.ts +304 -0
- package/src/tools/create-page.ts +2 -2
- package/src/tools/skills/index.ts +1 -0
- package/src/tools/skills/skill-get-file.ts +80 -0
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +20 -0
- package/src/utils/internal-ai/complete-structured.ts +2 -2
- package/templates/schedules/daily-blocker-digest/content.md +68 -54
- package/templates/schedules/daily-compounding-reflection/content.md +4 -4
- package/templates/schedules/daily-hn-briefing/content.md +5 -5
- package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
- package/templates/schedules/gtm-weekly-review/content.md +9 -9
- package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
- package/templates/skills/agentmail-sending/content.md +6 -7
- package/templates/skills/desloppify/content.md +8 -9
- package/templates/skills/jira-interaction/content.md +25 -33
- package/templates/skills/kapso-whatsapp/content.md +29 -30
- package/templates/skills/linear-interaction/content.md +8 -9
- package/templates/skills/profile-corruption-escalation/content.md +44 -85
- package/templates/skills/sprite-cli/content.md +4 -5
- package/templates/skills/turso-interaction/content.md +14 -17
- package/templates/skills/workflow-iterate/content.md +38 -391
- package/templates/skills/x-api-interactions/content.md +4 -6
- package/templates/workflows/llm-safe-release-context/config.json +13 -0
- package/templates/workflows/llm-safe-release-context/content.md +69 -0
- package/templates/skills/scheduled-task-resilience/config.json +0 -14
- 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
|
|
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
|
|
13
|
-
* or claude-bridge is enabled, createSession
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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(
|
|
87
|
-
expect(parseClaudeBinary(
|
|
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(
|
|
92
|
-
|
|
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(
|
|
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(
|
|
101
|
-
|
|
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:
|
|
131
|
+
const resolvedEnv = { CLAUDE_BINARY: LEGACY_BRIDGE_COMPAT_BINARY };
|
|
108
132
|
const fallbackEnv = { CLAUDE_BINARY: "claude" };
|
|
109
|
-
expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe(
|
|
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:
|
|
115
|
-
expect(resolveClaudeBinary(resolvedEnv, fallbackEnv)).toBe(
|
|
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(
|
|
125
|
-
"
|
|
126
|
-
);
|
|
127
|
-
expect(
|
|
128
|
-
"
|
|
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:
|
|
138
|
-
expect(resolveClaudeBinary(resolvedEnv, {})).toBe(
|
|
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
|
-
|
|
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]
|
|
356
|
-
process.env.CLAUDE_BINARY =
|
|
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(
|
|
389
|
+
expect(argv[0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
|
|
363
390
|
});
|
|
364
391
|
|
|
365
|
-
test("custom path: argv[0] is the absolute path
|
|
366
|
-
process.env.CLAUDE_BINARY =
|
|
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(
|
|
398
|
+
expect(spawnedArgs[0][0]).toBe(`/usr/local/bin/${LEGACY_BRIDGE_COMPAT_BINARY}`);
|
|
372
399
|
});
|
|
373
400
|
|
|
374
|
-
test("command string
|
|
375
|
-
process.env.CLAUDE_BINARY =
|
|
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(
|
|
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
|
|
389
|
-
process.env.CLAUDE_BINARY =
|
|
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(
|
|
423
|
+
expect(argv[1]).toBe(`${LEGACY_BRIDGE_COMPAT_PACKAGE}@1.2.3`);
|
|
397
424
|
});
|
|
398
425
|
|
|
399
|
-
test("multiple-space tolerance
|
|
400
|
-
process.env.CLAUDE_BINARY =
|
|
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(
|
|
434
|
+
expect(argv[1]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
|
|
408
435
|
expect(argv).toContain("--model");
|
|
409
436
|
});
|
|
410
437
|
|
|
411
|
-
test("argv[1..]
|
|
412
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
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(
|
|
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
|
|
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: {
|
|
437
|
-
|
|
438
|
-
|
|
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(
|
|
470
|
+
expect(spawnedArgs[0][0]).toBe(LEGACY_BRIDGE_COMPAT_BINARY);
|
|
444
471
|
});
|
|
445
472
|
|
|
446
|
-
test("config.env
|
|
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:
|
|
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(
|
|
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 =
|
|
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(
|
|
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=
|
|
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 =
|
|
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("
|
|
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
|
|
582
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
593
|
-
process.env.CLAUDE_BINARY =
|
|
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("
|
|
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
|
|
616
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
627
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
702
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
713
|
-
process.env.CLAUDE_BINARY =
|
|
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
|
|
723
|
-
process.env.CLAUDE_BINARY =
|
|
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 =
|
|
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
|
|
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
|
|
819
|
+
await adapter.createSession(makeConfig({ cwd }));
|
|
765
820
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
|
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
|
+
});
|