opencode-swarm-plugin 0.35.0 → 0.36.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
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",
@@ -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
@@ -330,279 +332,6 @@ describe("Compaction Hook", () => {
330
332
  });
331
333
  });
332
334
 
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
335
  describe("Logging instrumentation", () => {
607
336
  it("logs compaction start with session_id", async () => {
608
337
  const hook = createCompactionHook();
@@ -736,13 +465,230 @@ describe("Compaction Hook", () => {
736
465
  await hook(input, output);
737
466
 
738
467
  // If context was injected, should log the size
739
- if (output.context.length > 0) {
740
- const injectionLog = logCalls.find(
741
- (log) =>
742
- log.level === "info" && log.message === "injected swarm context",
743
- );
744
- expect(injectionLog?.data.context_length).toBeGreaterThan(0);
745
- }
468
+ if (output.context.length > 0) {
469
+ const injectionLog = logCalls.find(
470
+ (log) =>
471
+ log.level === "info" && log.message === "injected swarm context",
472
+ );
473
+ expect(injectionLog?.data.context_length).toBeGreaterThan(0);
474
+ }
475
+ });
476
+ });
477
+
478
+ describe("scanSessionMessages", () => {
479
+ it("returns empty state when client is undefined", async () => {
480
+ const state = await scanSessionMessages(undefined, "test-session");
481
+ expect(state.epicId).toBeUndefined();
482
+ expect(state.agentName).toBeUndefined();
483
+ expect(state.subtasks.size).toBe(0);
484
+ });
485
+
486
+ it("returns empty state when client is null", async () => {
487
+ const state = await scanSessionMessages(null, "test-session");
488
+ expect(state.epicId).toBeUndefined();
489
+ expect(state.subtasks.size).toBe(0);
490
+ });
491
+
492
+ it("extracts epic data from hive_create_epic tool call", async () => {
493
+ const mockClient = {
494
+ session: {
495
+ messages: async () => ({
496
+ data: [
497
+ {
498
+ info: { id: "msg-1", sessionID: "test-session" },
499
+ parts: [
500
+ {
501
+ type: "tool",
502
+ tool: "hive_create_epic",
503
+ state: {
504
+ status: "completed",
505
+ input: { epic_title: "Test Epic" },
506
+ output: JSON.stringify({ epic: { id: "epic-123" } }),
507
+ time: { start: 1000, end: 2000 },
508
+ },
509
+ },
510
+ ],
511
+ },
512
+ ],
513
+ }),
514
+ },
515
+ };
516
+
517
+ const state = await scanSessionMessages(mockClient, "test-session");
518
+ expect(state.epicId).toBe("epic-123");
519
+ expect(state.epicTitle).toBe("Test Epic");
520
+ });
521
+
522
+ it("extracts agent name from swarmmail_init tool call", async () => {
523
+ const mockClient = {
524
+ session: {
525
+ messages: async () => ({
526
+ data: [
527
+ {
528
+ info: { id: "msg-1", sessionID: "test-session" },
529
+ parts: [
530
+ {
531
+ type: "tool",
532
+ tool: "swarmmail_init",
533
+ state: {
534
+ status: "completed",
535
+ input: {},
536
+ output: JSON.stringify({
537
+ agent_name: "BlueLake",
538
+ project_key: "/test/project",
539
+ }),
540
+ time: { start: 1000, end: 2000 },
541
+ },
542
+ },
543
+ ],
544
+ },
545
+ ],
546
+ }),
547
+ },
548
+ };
549
+
550
+ const state = await scanSessionMessages(mockClient, "test-session");
551
+ expect(state.agentName).toBe("BlueLake");
552
+ expect(state.projectPath).toBe("/test/project");
553
+ });
554
+
555
+ it("tracks subtasks from swarm_spawn_subtask tool calls", async () => {
556
+ const mockClient = {
557
+ session: {
558
+ messages: async () => ({
559
+ data: [
560
+ {
561
+ info: { id: "msg-1", sessionID: "test-session" },
562
+ parts: [
563
+ {
564
+ type: "tool",
565
+ tool: "swarm_spawn_subtask",
566
+ state: {
567
+ status: "completed",
568
+ input: {
569
+ bead_id: "bd-123.1",
570
+ epic_id: "epic-123",
571
+ subtask_title: "Add auth",
572
+ files: ["src/auth.ts"],
573
+ },
574
+ output: JSON.stringify({ worker: "RedMountain" }),
575
+ time: { start: 1000, end: 2000 },
576
+ },
577
+ },
578
+ ],
579
+ },
580
+ ],
581
+ }),
582
+ },
583
+ };
584
+
585
+ const state = await scanSessionMessages(mockClient, "test-session");
586
+ expect(state.subtasks.size).toBe(1);
587
+ const subtask = state.subtasks.get("bd-123.1");
588
+ expect(subtask?.title).toBe("Add auth");
589
+ expect(subtask?.status).toBe("spawned");
590
+ expect(subtask?.worker).toBe("RedMountain");
591
+ expect(subtask?.files).toEqual(["src/auth.ts"]);
592
+ });
593
+
594
+ it("marks subtasks as completed from swarm_complete tool calls", async () => {
595
+ const mockClient = {
596
+ session: {
597
+ messages: async () => ({
598
+ data: [
599
+ {
600
+ info: { id: "msg-1", sessionID: "test-session" },
601
+ parts: [
602
+ {
603
+ type: "tool",
604
+ tool: "swarm_spawn_subtask",
605
+ state: {
606
+ status: "completed",
607
+ input: {
608
+ bead_id: "bd-123.1",
609
+ subtask_title: "Add auth",
610
+ },
611
+ output: "{}",
612
+ time: { start: 1000, end: 2000 },
613
+ },
614
+ },
615
+ {
616
+ type: "tool",
617
+ tool: "swarm_complete",
618
+ state: {
619
+ status: "completed",
620
+ input: { bead_id: "bd-123.1" },
621
+ output: "{}",
622
+ time: { start: 3000, end: 4000 },
623
+ },
624
+ },
625
+ ],
626
+ },
627
+ ],
628
+ }),
629
+ },
630
+ };
631
+
632
+ const state = await scanSessionMessages(mockClient, "test-session");
633
+ const subtask = state.subtasks.get("bd-123.1");
634
+ expect(subtask?.status).toBe("completed");
635
+ });
636
+
637
+ it("tracks last action", async () => {
638
+ const mockClient = {
639
+ session: {
640
+ messages: async () => ({
641
+ data: [
642
+ {
643
+ info: { id: "msg-1", sessionID: "test-session" },
644
+ parts: [
645
+ {
646
+ type: "tool",
647
+ tool: "swarm_status",
648
+ state: {
649
+ status: "completed",
650
+ input: { epic_id: "epic-123", project_key: "/test" },
651
+ output: "{}",
652
+ time: { start: 5000, end: 6000 },
653
+ },
654
+ },
655
+ ],
656
+ },
657
+ ],
658
+ }),
659
+ },
660
+ };
661
+
662
+ const state = await scanSessionMessages(mockClient, "test-session");
663
+ expect(state.lastAction?.tool).toBe("swarm_status");
664
+ expect(state.lastAction?.timestamp).toBe(6000);
665
+ });
666
+
667
+ it("handles SDK errors gracefully", async () => {
668
+ const mockClient = {
669
+ session: {
670
+ messages: async () => {
671
+ throw new Error("SDK error");
672
+ },
673
+ },
674
+ };
675
+
676
+ // Should not throw, just return empty state
677
+ const state = await scanSessionMessages(mockClient, "test-session");
678
+ expect(state.subtasks.size).toBe(0);
679
+ });
680
+
681
+ it("respects limit parameter", async () => {
682
+ const mockClient = {
683
+ session: {
684
+ messages: async (opts: { query?: { limit?: number } }) => {
685
+ expect(opts.query?.limit).toBe(50);
686
+ return { data: [] };
687
+ },
688
+ },
689
+ };
690
+
691
+ await scanSessionMessages(mockClient, "test-session", 50);
746
692
  });
747
693
  });
748
694
  });