opencode-swarm-plugin 0.35.0 → 0.36.1
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/.hive/issues.jsonl +4 -4
- package/.hive/memories.jsonl +274 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +307 -307
- package/CHANGELOG.md +133 -0
- package/bin/swarm.ts +234 -179
- package/dist/compaction-hook.d.ts +54 -4
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/eval-capture.d.ts +122 -17
- package/dist/eval-capture.d.ts.map +1 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1278 -619
- package/dist/planning-guardrails.d.ts +121 -0
- package/dist/planning-guardrails.d.ts.map +1 -1
- package/dist/plugin.d.ts +9 -9
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +1283 -329
- package/dist/schemas/task.d.ts +0 -1
- package/dist/schemas/task.d.ts.map +1 -1
- package/dist/swarm-decompose.d.ts +0 -8
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +0 -4
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +0 -6
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +38 -0
- package/evals/coordinator-session.eval.ts +154 -0
- package/evals/fixtures/coordinator-sessions.ts +328 -0
- package/evals/lib/data-loader.ts +69 -0
- package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
- package/evals/scorers/coordinator-discipline.ts +315 -0
- package/evals/scorers/index.ts +12 -0
- package/examples/plugin-wrapper-template.ts +747 -34
- package/package.json +2 -2
- package/src/compaction-hook.test.ts +234 -281
- package/src/compaction-hook.ts +221 -63
- package/src/eval-capture.test.ts +390 -0
- package/src/eval-capture.ts +168 -10
- package/src/index.ts +89 -2
- package/src/learning.integration.test.ts +0 -2
- package/src/planning-guardrails.test.ts +387 -2
- package/src/planning-guardrails.ts +289 -0
- package/src/plugin.ts +10 -10
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +21 -8
- package/src/swarm-orchestrate.ts +44 -0
- package/src/swarm-prompts.ts +20 -0
- package/src/swarm-review.ts +41 -0
- package/src/swarm.integration.test.ts +0 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.1",
|
|
4
4
|
"description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"minimatch": "^10.1.1",
|
|
42
42
|
"pino": "^9.6.0",
|
|
43
43
|
"pino-roll": "^1.3.0",
|
|
44
|
-
"swarm-mail": "1.5.
|
|
44
|
+
"swarm-mail": "1.5.1",
|
|
45
45
|
"yaml": "^2.8.2",
|
|
46
46
|
"zod": "4.1.8"
|
|
47
47
|
},
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
SWARM_COMPACTION_CONTEXT,
|
|
8
8
|
SWARM_DETECTION_FALLBACK,
|
|
9
9
|
createCompactionHook,
|
|
10
|
+
scanSessionMessages,
|
|
11
|
+
type ScannedSwarmState,
|
|
10
12
|
} from "./compaction-hook";
|
|
11
13
|
|
|
12
14
|
// Track log calls for verification
|
|
@@ -62,7 +64,14 @@ describe("Compaction Hook", () => {
|
|
|
62
64
|
describe("SWARM_COMPACTION_CONTEXT", () => {
|
|
63
65
|
it("contains coordinator instructions", () => {
|
|
64
66
|
expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
|
|
65
|
-
expect(SWARM_COMPACTION_CONTEXT).toContain("
|
|
67
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("You Are The COORDINATOR");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("contains prohibition-first anti-patterns", () => {
|
|
71
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
|
|
72
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("edit");
|
|
73
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("write");
|
|
74
|
+
expect(SWARM_COMPACTION_CONTEXT).toContain("SPAWN A WORKER");
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
it("contains resume instructions", () => {
|
|
@@ -330,279 +339,6 @@ describe("Compaction Hook", () => {
|
|
|
330
339
|
});
|
|
331
340
|
});
|
|
332
341
|
|
|
333
|
-
describe("scanSessionMessages", () => {
|
|
334
|
-
it("returns empty state when client is undefined", async () => {
|
|
335
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
336
|
-
|
|
337
|
-
const result = await scanSessionMessages(undefined, "session-123");
|
|
338
|
-
|
|
339
|
-
expect(result.epicId).toBeUndefined();
|
|
340
|
-
expect(result.epicTitle).toBeUndefined();
|
|
341
|
-
expect(result.projectPath).toBeUndefined();
|
|
342
|
-
expect(result.agentName).toBeUndefined();
|
|
343
|
-
expect(result.subtasks.size).toBe(0);
|
|
344
|
-
expect(result.lastAction).toBeUndefined();
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it("extracts epic data from hive_create_epic tool call", async () => {
|
|
348
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
349
|
-
|
|
350
|
-
// Mock SDK client
|
|
351
|
-
const mockClient = {
|
|
352
|
-
session: {
|
|
353
|
-
messages: async ({ sessionID, limit }: { sessionID: string; limit?: number }) => {
|
|
354
|
-
return [
|
|
355
|
-
{
|
|
356
|
-
info: { id: "msg-1", sessionID: "session-123" },
|
|
357
|
-
parts: [
|
|
358
|
-
{
|
|
359
|
-
id: "part-1",
|
|
360
|
-
sessionID: "session-123",
|
|
361
|
-
messageID: "msg-1",
|
|
362
|
-
type: "tool" as const,
|
|
363
|
-
callID: "call-1",
|
|
364
|
-
tool: "hive_create_epic",
|
|
365
|
-
state: {
|
|
366
|
-
status: "completed" as const,
|
|
367
|
-
input: {
|
|
368
|
-
epic_title: "Add authentication system",
|
|
369
|
-
epic_description: "Implement OAuth flow",
|
|
370
|
-
},
|
|
371
|
-
output: JSON.stringify({
|
|
372
|
-
success: true,
|
|
373
|
-
epic: { id: "bd-epic-123" },
|
|
374
|
-
}),
|
|
375
|
-
title: "Create Epic",
|
|
376
|
-
metadata: {},
|
|
377
|
-
time: { start: Date.now(), end: Date.now() },
|
|
378
|
-
},
|
|
379
|
-
},
|
|
380
|
-
],
|
|
381
|
-
},
|
|
382
|
-
];
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
} as any;
|
|
386
|
-
|
|
387
|
-
const result = await scanSessionMessages(mockClient, "session-123");
|
|
388
|
-
|
|
389
|
-
expect(result.epicId).toBe("bd-epic-123");
|
|
390
|
-
expect(result.epicTitle).toBe("Add authentication system");
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("extracts agent name from swarmmail_init tool call", async () => {
|
|
394
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
395
|
-
|
|
396
|
-
const mockClient = {
|
|
397
|
-
session: {
|
|
398
|
-
messages: async () => [
|
|
399
|
-
{
|
|
400
|
-
info: { id: "msg-1", sessionID: "session-123" },
|
|
401
|
-
parts: [
|
|
402
|
-
{
|
|
403
|
-
id: "part-1",
|
|
404
|
-
sessionID: "session-123",
|
|
405
|
-
messageID: "msg-1",
|
|
406
|
-
type: "tool" as const,
|
|
407
|
-
callID: "call-1",
|
|
408
|
-
tool: "swarmmail_init",
|
|
409
|
-
state: {
|
|
410
|
-
status: "completed" as const,
|
|
411
|
-
input: {
|
|
412
|
-
project_path: "/test/project",
|
|
413
|
-
task_description: "Working on auth",
|
|
414
|
-
},
|
|
415
|
-
output: JSON.stringify({
|
|
416
|
-
agent_name: "BlueLake",
|
|
417
|
-
project_key: "/test/project",
|
|
418
|
-
}),
|
|
419
|
-
title: "Init Swarm Mail",
|
|
420
|
-
metadata: {},
|
|
421
|
-
time: { start: Date.now(), end: Date.now() },
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
],
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
},
|
|
428
|
-
} as any;
|
|
429
|
-
|
|
430
|
-
const result = await scanSessionMessages(mockClient, "session-123");
|
|
431
|
-
|
|
432
|
-
expect(result.agentName).toBe("BlueLake");
|
|
433
|
-
expect(result.projectPath).toBe("/test/project");
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it("extracts subtask data from swarm_spawn_subtask tool call", async () => {
|
|
437
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
438
|
-
|
|
439
|
-
const mockClient = {
|
|
440
|
-
session: {
|
|
441
|
-
messages: async () => [
|
|
442
|
-
{
|
|
443
|
-
info: { id: "msg-1", sessionID: "session-123" },
|
|
444
|
-
parts: [
|
|
445
|
-
{
|
|
446
|
-
id: "part-1",
|
|
447
|
-
sessionID: "session-123",
|
|
448
|
-
messageID: "msg-1",
|
|
449
|
-
type: "tool" as const,
|
|
450
|
-
callID: "call-1",
|
|
451
|
-
tool: "swarm_spawn_subtask",
|
|
452
|
-
state: {
|
|
453
|
-
status: "completed" as const,
|
|
454
|
-
input: {
|
|
455
|
-
bead_id: "bd-task-1",
|
|
456
|
-
epic_id: "bd-epic-123",
|
|
457
|
-
subtask_title: "Implement OAuth service",
|
|
458
|
-
files: ["src/auth/oauth.ts"],
|
|
459
|
-
},
|
|
460
|
-
output: JSON.stringify({
|
|
461
|
-
worker: "RedMountain",
|
|
462
|
-
bead_id: "bd-task-1",
|
|
463
|
-
}),
|
|
464
|
-
title: "Spawn Subtask",
|
|
465
|
-
metadata: {},
|
|
466
|
-
time: { start: Date.now(), end: Date.now() },
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
],
|
|
470
|
-
},
|
|
471
|
-
],
|
|
472
|
-
},
|
|
473
|
-
} as any;
|
|
474
|
-
|
|
475
|
-
const result = await scanSessionMessages(mockClient, "session-123");
|
|
476
|
-
|
|
477
|
-
expect(result.subtasks.size).toBe(1);
|
|
478
|
-
expect(result.subtasks.get("bd-task-1")).toEqual({
|
|
479
|
-
title: "Implement OAuth service",
|
|
480
|
-
status: "spawned",
|
|
481
|
-
worker: "RedMountain",
|
|
482
|
-
files: ["src/auth/oauth.ts"],
|
|
483
|
-
});
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it("marks subtask as completed from swarm_complete tool call", async () => {
|
|
487
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
488
|
-
|
|
489
|
-
const mockClient = {
|
|
490
|
-
session: {
|
|
491
|
-
messages: async () => [
|
|
492
|
-
{
|
|
493
|
-
info: { id: "msg-1", sessionID: "session-123" },
|
|
494
|
-
parts: [
|
|
495
|
-
{
|
|
496
|
-
id: "part-1",
|
|
497
|
-
sessionID: "session-123",
|
|
498
|
-
messageID: "msg-1",
|
|
499
|
-
type: "tool" as const,
|
|
500
|
-
callID: "call-1",
|
|
501
|
-
tool: "swarm_spawn_subtask",
|
|
502
|
-
state: {
|
|
503
|
-
status: "completed" as const,
|
|
504
|
-
input: {
|
|
505
|
-
bead_id: "bd-task-1",
|
|
506
|
-
epic_id: "bd-epic-123",
|
|
507
|
-
subtask_title: "Fix bug",
|
|
508
|
-
files: [],
|
|
509
|
-
},
|
|
510
|
-
output: "{}",
|
|
511
|
-
title: "Spawn",
|
|
512
|
-
metadata: {},
|
|
513
|
-
time: { start: 100, end: 200 },
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
{
|
|
517
|
-
id: "part-2",
|
|
518
|
-
sessionID: "session-123",
|
|
519
|
-
messageID: "msg-1",
|
|
520
|
-
type: "tool" as const,
|
|
521
|
-
callID: "call-2",
|
|
522
|
-
tool: "swarm_complete",
|
|
523
|
-
state: {
|
|
524
|
-
status: "completed" as const,
|
|
525
|
-
input: {
|
|
526
|
-
bead_id: "bd-task-1",
|
|
527
|
-
summary: "Fixed the bug",
|
|
528
|
-
},
|
|
529
|
-
output: JSON.stringify({ success: true, closed: true }),
|
|
530
|
-
title: "Complete",
|
|
531
|
-
metadata: {},
|
|
532
|
-
time: { start: 300, end: 400 },
|
|
533
|
-
},
|
|
534
|
-
},
|
|
535
|
-
],
|
|
536
|
-
},
|
|
537
|
-
],
|
|
538
|
-
},
|
|
539
|
-
} as any;
|
|
540
|
-
|
|
541
|
-
const result = await scanSessionMessages(mockClient, "session-123");
|
|
542
|
-
|
|
543
|
-
expect(result.subtasks.get("bd-task-1")?.status).toBe("completed");
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
it("captures last action timestamp", async () => {
|
|
547
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
548
|
-
|
|
549
|
-
const mockClient = {
|
|
550
|
-
session: {
|
|
551
|
-
messages: async () => [
|
|
552
|
-
{
|
|
553
|
-
info: { id: "msg-1", sessionID: "session-123" },
|
|
554
|
-
parts: [
|
|
555
|
-
{
|
|
556
|
-
id: "part-1",
|
|
557
|
-
sessionID: "session-123",
|
|
558
|
-
messageID: "msg-1",
|
|
559
|
-
type: "tool" as const,
|
|
560
|
-
callID: "call-1",
|
|
561
|
-
tool: "swarm_status",
|
|
562
|
-
state: {
|
|
563
|
-
status: "completed" as const,
|
|
564
|
-
input: {
|
|
565
|
-
epic_id: "bd-epic-123",
|
|
566
|
-
project_key: "/test",
|
|
567
|
-
},
|
|
568
|
-
output: "{}",
|
|
569
|
-
title: "Check Status",
|
|
570
|
-
metadata: {},
|
|
571
|
-
time: { start: 1000, end: 2000 },
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
],
|
|
575
|
-
},
|
|
576
|
-
],
|
|
577
|
-
},
|
|
578
|
-
} as any;
|
|
579
|
-
|
|
580
|
-
const result = await scanSessionMessages(mockClient, "session-123");
|
|
581
|
-
|
|
582
|
-
expect(result.lastAction).toBeDefined();
|
|
583
|
-
expect(result.lastAction?.tool).toBe("swarm_status");
|
|
584
|
-
expect(result.lastAction?.timestamp).toBe(2000);
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it("respects limit parameter", async () => {
|
|
588
|
-
const { scanSessionMessages } = await import("./compaction-hook");
|
|
589
|
-
|
|
590
|
-
let capturedLimit: number | undefined;
|
|
591
|
-
const mockClient = {
|
|
592
|
-
session: {
|
|
593
|
-
messages: async ({ limit }: { limit?: number }) => {
|
|
594
|
-
capturedLimit = limit;
|
|
595
|
-
return [];
|
|
596
|
-
},
|
|
597
|
-
},
|
|
598
|
-
} as any;
|
|
599
|
-
|
|
600
|
-
await scanSessionMessages(mockClient, "session-123", 50);
|
|
601
|
-
|
|
602
|
-
expect(capturedLimit).toBe(50);
|
|
603
|
-
});
|
|
604
|
-
});
|
|
605
|
-
|
|
606
342
|
describe("Logging instrumentation", () => {
|
|
607
343
|
it("logs compaction start with session_id", async () => {
|
|
608
344
|
const hook = createCompactionHook();
|
|
@@ -736,13 +472,230 @@ describe("Compaction Hook", () => {
|
|
|
736
472
|
await hook(input, output);
|
|
737
473
|
|
|
738
474
|
// If context was injected, should log the size
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
475
|
+
if (output.context.length > 0) {
|
|
476
|
+
const injectionLog = logCalls.find(
|
|
477
|
+
(log) =>
|
|
478
|
+
log.level === "info" && log.message === "injected swarm context",
|
|
479
|
+
);
|
|
480
|
+
expect(injectionLog?.data.context_length).toBeGreaterThan(0);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
describe("scanSessionMessages", () => {
|
|
486
|
+
it("returns empty state when client is undefined", async () => {
|
|
487
|
+
const state = await scanSessionMessages(undefined, "test-session");
|
|
488
|
+
expect(state.epicId).toBeUndefined();
|
|
489
|
+
expect(state.agentName).toBeUndefined();
|
|
490
|
+
expect(state.subtasks.size).toBe(0);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("returns empty state when client is null", async () => {
|
|
494
|
+
const state = await scanSessionMessages(null, "test-session");
|
|
495
|
+
expect(state.epicId).toBeUndefined();
|
|
496
|
+
expect(state.subtasks.size).toBe(0);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("extracts epic data from hive_create_epic tool call", async () => {
|
|
500
|
+
const mockClient = {
|
|
501
|
+
session: {
|
|
502
|
+
messages: async () => ({
|
|
503
|
+
data: [
|
|
504
|
+
{
|
|
505
|
+
info: { id: "msg-1", sessionID: "test-session" },
|
|
506
|
+
parts: [
|
|
507
|
+
{
|
|
508
|
+
type: "tool",
|
|
509
|
+
tool: "hive_create_epic",
|
|
510
|
+
state: {
|
|
511
|
+
status: "completed",
|
|
512
|
+
input: { epic_title: "Test Epic" },
|
|
513
|
+
output: JSON.stringify({ epic: { id: "epic-123" } }),
|
|
514
|
+
time: { start: 1000, end: 2000 },
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
}),
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
525
|
+
expect(state.epicId).toBe("epic-123");
|
|
526
|
+
expect(state.epicTitle).toBe("Test Epic");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("extracts agent name from swarmmail_init tool call", async () => {
|
|
530
|
+
const mockClient = {
|
|
531
|
+
session: {
|
|
532
|
+
messages: async () => ({
|
|
533
|
+
data: [
|
|
534
|
+
{
|
|
535
|
+
info: { id: "msg-1", sessionID: "test-session" },
|
|
536
|
+
parts: [
|
|
537
|
+
{
|
|
538
|
+
type: "tool",
|
|
539
|
+
tool: "swarmmail_init",
|
|
540
|
+
state: {
|
|
541
|
+
status: "completed",
|
|
542
|
+
input: {},
|
|
543
|
+
output: JSON.stringify({
|
|
544
|
+
agent_name: "BlueLake",
|
|
545
|
+
project_key: "/test/project",
|
|
546
|
+
}),
|
|
547
|
+
time: { start: 1000, end: 2000 },
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
}),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
558
|
+
expect(state.agentName).toBe("BlueLake");
|
|
559
|
+
expect(state.projectPath).toBe("/test/project");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("tracks subtasks from swarm_spawn_subtask tool calls", async () => {
|
|
563
|
+
const mockClient = {
|
|
564
|
+
session: {
|
|
565
|
+
messages: async () => ({
|
|
566
|
+
data: [
|
|
567
|
+
{
|
|
568
|
+
info: { id: "msg-1", sessionID: "test-session" },
|
|
569
|
+
parts: [
|
|
570
|
+
{
|
|
571
|
+
type: "tool",
|
|
572
|
+
tool: "swarm_spawn_subtask",
|
|
573
|
+
state: {
|
|
574
|
+
status: "completed",
|
|
575
|
+
input: {
|
|
576
|
+
bead_id: "bd-123.1",
|
|
577
|
+
epic_id: "epic-123",
|
|
578
|
+
subtask_title: "Add auth",
|
|
579
|
+
files: ["src/auth.ts"],
|
|
580
|
+
},
|
|
581
|
+
output: JSON.stringify({ worker: "RedMountain" }),
|
|
582
|
+
time: { start: 1000, end: 2000 },
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
},
|
|
587
|
+
],
|
|
588
|
+
}),
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
593
|
+
expect(state.subtasks.size).toBe(1);
|
|
594
|
+
const subtask = state.subtasks.get("bd-123.1");
|
|
595
|
+
expect(subtask?.title).toBe("Add auth");
|
|
596
|
+
expect(subtask?.status).toBe("spawned");
|
|
597
|
+
expect(subtask?.worker).toBe("RedMountain");
|
|
598
|
+
expect(subtask?.files).toEqual(["src/auth.ts"]);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("marks subtasks as completed from swarm_complete tool calls", async () => {
|
|
602
|
+
const mockClient = {
|
|
603
|
+
session: {
|
|
604
|
+
messages: async () => ({
|
|
605
|
+
data: [
|
|
606
|
+
{
|
|
607
|
+
info: { id: "msg-1", sessionID: "test-session" },
|
|
608
|
+
parts: [
|
|
609
|
+
{
|
|
610
|
+
type: "tool",
|
|
611
|
+
tool: "swarm_spawn_subtask",
|
|
612
|
+
state: {
|
|
613
|
+
status: "completed",
|
|
614
|
+
input: {
|
|
615
|
+
bead_id: "bd-123.1",
|
|
616
|
+
subtask_title: "Add auth",
|
|
617
|
+
},
|
|
618
|
+
output: "{}",
|
|
619
|
+
time: { start: 1000, end: 2000 },
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
type: "tool",
|
|
624
|
+
tool: "swarm_complete",
|
|
625
|
+
state: {
|
|
626
|
+
status: "completed",
|
|
627
|
+
input: { bead_id: "bd-123.1" },
|
|
628
|
+
output: "{}",
|
|
629
|
+
time: { start: 3000, end: 4000 },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
],
|
|
635
|
+
}),
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
640
|
+
const subtask = state.subtasks.get("bd-123.1");
|
|
641
|
+
expect(subtask?.status).toBe("completed");
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("tracks last action", async () => {
|
|
645
|
+
const mockClient = {
|
|
646
|
+
session: {
|
|
647
|
+
messages: async () => ({
|
|
648
|
+
data: [
|
|
649
|
+
{
|
|
650
|
+
info: { id: "msg-1", sessionID: "test-session" },
|
|
651
|
+
parts: [
|
|
652
|
+
{
|
|
653
|
+
type: "tool",
|
|
654
|
+
tool: "swarm_status",
|
|
655
|
+
state: {
|
|
656
|
+
status: "completed",
|
|
657
|
+
input: { epic_id: "epic-123", project_key: "/test" },
|
|
658
|
+
output: "{}",
|
|
659
|
+
time: { start: 5000, end: 6000 },
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
}),
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
670
|
+
expect(state.lastAction?.tool).toBe("swarm_status");
|
|
671
|
+
expect(state.lastAction?.timestamp).toBe(6000);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
it("handles SDK errors gracefully", async () => {
|
|
675
|
+
const mockClient = {
|
|
676
|
+
session: {
|
|
677
|
+
messages: async () => {
|
|
678
|
+
throw new Error("SDK error");
|
|
679
|
+
},
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Should not throw, just return empty state
|
|
684
|
+
const state = await scanSessionMessages(mockClient, "test-session");
|
|
685
|
+
expect(state.subtasks.size).toBe(0);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("respects limit parameter", async () => {
|
|
689
|
+
const mockClient = {
|
|
690
|
+
session: {
|
|
691
|
+
messages: async (opts: { query?: { limit?: number } }) => {
|
|
692
|
+
expect(opts.query?.limit).toBe(50);
|
|
693
|
+
return { data: [] };
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
await scanSessionMessages(mockClient, "test-session", 50);
|
|
746
699
|
});
|
|
747
700
|
});
|
|
748
701
|
});
|