opencode-swarm-plugin 0.36.1 → 0.38.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.
@@ -495,6 +495,158 @@ describe("End-to-end research workflow", () => {
495
495
  });
496
496
  });
497
497
 
498
+ describe("Research spawn instructions (NEW)", () => {
499
+ let testProjectPath: string;
500
+
501
+ beforeEach(() => {
502
+ testProjectPath = join(tmpdir(), `spawn-test-${Date.now()}`);
503
+ mkdirSync(testProjectPath, { recursive: true });
504
+ });
505
+
506
+ afterEach(() => {
507
+ rmSync(testProjectPath, { recursive: true, force: true });
508
+ });
509
+
510
+ test("runResearchPhase generates spawn instructions for each technology", async () => {
511
+ // Create package.json with dependencies
512
+ const packageJson = {
513
+ dependencies: {
514
+ zod: "^3.22.4",
515
+ typescript: "^5.3.3",
516
+ },
517
+ };
518
+
519
+ writeFileSync(
520
+ join(testProjectPath, "package.json"),
521
+ JSON.stringify(packageJson, null, 2),
522
+ );
523
+
524
+ // Run research phase
525
+ const result = await runResearchPhase(
526
+ "Add Zod validation to TypeScript API",
527
+ testProjectPath,
528
+ );
529
+
530
+ // Should have spawn_instructions array
531
+ expect(result.spawn_instructions).toBeDefined();
532
+ expect(Array.isArray(result.spawn_instructions)).toBe(true);
533
+
534
+ // Should have one instruction per technology
535
+ expect(result.spawn_instructions.length).toBe(result.tech_stack.length);
536
+
537
+ // Each instruction should have required fields
538
+ for (const instruction of result.spawn_instructions) {
539
+ expect(instruction.research_id).toBeDefined();
540
+ expect(instruction.research_id).toMatch(/^research-/); // Should start with "research-"
541
+ expect(instruction.tech).toBeDefined();
542
+ expect(result.tech_stack).toContain(instruction.tech); // Tech should be from tech_stack
543
+ expect(instruction.prompt).toBeDefined();
544
+ expect(typeof instruction.prompt).toBe("string");
545
+ expect(instruction.prompt.length).toBeGreaterThan(0);
546
+ expect(instruction.subagent_type).toBe("swarm/researcher");
547
+ }
548
+ });
549
+
550
+ test("runResearchPhase prompts contain correct technology", async () => {
551
+ const packageJson = {
552
+ dependencies: {
553
+ zod: "^3.22.4",
554
+ },
555
+ };
556
+
557
+ writeFileSync(
558
+ join(testProjectPath, "package.json"),
559
+ JSON.stringify(packageJson, null, 2),
560
+ );
561
+
562
+ const result = await runResearchPhase("Use Zod", testProjectPath);
563
+
564
+ // Should have exactly one spawn instruction (one tech)
565
+ expect(result.spawn_instructions.length).toBe(1);
566
+
567
+ const instruction = result.spawn_instructions[0];
568
+ expect(instruction.tech).toBe("zod");
569
+ expect(instruction.prompt).toContain("zod");
570
+ expect(instruction.prompt).toContain(testProjectPath);
571
+ });
572
+
573
+ test("runResearchPhase with multiple technologies generates multiple instructions", async () => {
574
+ const packageJson = {
575
+ dependencies: {
576
+ zod: "^3.22.4",
577
+ typescript: "^5.3.3",
578
+ react: "^18.2.0",
579
+ },
580
+ };
581
+
582
+ writeFileSync(
583
+ join(testProjectPath, "package.json"),
584
+ JSON.stringify(packageJson, null, 2),
585
+ );
586
+
587
+ const result = await runResearchPhase(
588
+ "Build React app with Zod and TypeScript",
589
+ testProjectPath,
590
+ );
591
+
592
+ // Should extract 3 technologies
593
+ expect(result.tech_stack.length).toBe(3);
594
+
595
+ // Should have 3 spawn instructions
596
+ expect(result.spawn_instructions.length).toBe(3);
597
+
598
+ // Each tech should have one instruction
599
+ const techs = result.spawn_instructions.map((i) => i.tech);
600
+ expect(techs).toContain("zod");
601
+ expect(techs).toContain("typescript");
602
+ expect(techs).toContain("react");
603
+
604
+ // Research IDs should be unique
605
+ const researchIds = result.spawn_instructions.map((i) => i.research_id);
606
+ const uniqueIds = new Set(researchIds);
607
+ expect(uniqueIds.size).toBe(researchIds.length);
608
+ });
609
+
610
+ test("runResearchPhase with empty tech_stack returns empty spawn_instructions", async () => {
611
+ // Don't create package.json - no dependencies
612
+
613
+ const result = await runResearchPhase(
614
+ "Implement something with FooBarBaz",
615
+ testProjectPath,
616
+ );
617
+
618
+ // Should have empty tech_stack (no known technologies)
619
+ expect(result.tech_stack).toEqual([]);
620
+
621
+ // Should have empty spawn_instructions
622
+ expect(result.spawn_instructions).toEqual([]);
623
+
624
+ // Other fields should be empty
625
+ expect(result.summaries).toEqual({});
626
+ expect(result.memory_ids).toEqual([]);
627
+ });
628
+
629
+ test("spawn instruction prompts include swarmmail_init", async () => {
630
+ const packageJson = {
631
+ dependencies: {
632
+ zod: "^3.22.4",
633
+ },
634
+ };
635
+
636
+ writeFileSync(
637
+ join(testProjectPath, "package.json"),
638
+ JSON.stringify(packageJson, null, 2),
639
+ );
640
+
641
+ const result = await runResearchPhase("Use Zod", testProjectPath);
642
+
643
+ // Prompt should include swarmmail_init (researcher workers need this)
644
+ const instruction = result.spawn_instructions[0];
645
+ expect(instruction.prompt).toContain("swarmmail_init");
646
+ expect(instruction.prompt).toContain("semantic-memory_store");
647
+ });
648
+ });
649
+
498
650
  describe("Real-world fixture: this repo", () => {
499
651
  test("discovers tools and versions from actual repo", async () => {
500
652
  // Use the plugin package directory, not monorepo root
@@ -540,5 +692,10 @@ describe("Real-world fixture: this repo", () => {
540
692
  expect(result.summaries).toBeDefined();
541
693
  expect(result.memory_ids).toBeDefined();
542
694
  expect(Array.isArray(result.memory_ids)).toBe(true);
695
+
696
+ // NEW: Should have spawn_instructions
697
+ expect(result.spawn_instructions).toBeDefined();
698
+ expect(Array.isArray(result.spawn_instructions)).toBe(true);
699
+ expect(result.spawn_instructions.length).toBeGreaterThan(0);
543
700
  });
544
701
  });
@@ -2,18 +2,15 @@
2
2
  * Integration tests for swarm review feedback flow
3
3
  *
4
4
  * Tests the coordinator review feedback workflow with real HiveAdapter and swarm-mail.
5
- * Verifies that review approval/rejection properly updates state and sends messages.
5
+ * Verifies that review approval/rejection properly updates state.
6
6
  *
7
- * **STATUS**: URL_INVALID bug FIXED by commit 7bf9385 (libSQL URL normalization).
8
- * Tests now execute without URL errors. sendSwarmMessage successfully creates adapters.
9
- *
10
- * **REMAINING ISSUE**: Message retrieval not working. getInbox returns empty even though
11
- * sendSwarmMessage succeeds. Possible causes:
12
- * - Database adapter instance mismatch (sendSwarmMessage creates new adapter each call)
13
- * - Message projection not materializing from events
14
- * - Database path resolution issue between send and receive
15
- *
16
- * Tests currently SKIPPED pending message retrieval fix.
7
+ * **ARCHITECTURE**: Coordinator-driven retry pattern (swarm_spawn_retry)
8
+ * - `approved` status: Sends message to worker (worker can complete)
9
+ * - `needs_changes` status: NO message sent (worker is dead, coordinator spawns retry)
10
+ * - After 3 rejections: Task marked blocked, NO message sent
11
+ *
12
+ * This aligns with the "worker is dead" philosophy - failed reviews require coordinator
13
+ * intervention via swarm_spawn_retry, not worker self-retry.
17
14
  */
18
15
 
19
16
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
@@ -195,23 +192,23 @@ describe("swarm_review integration", () => {
195
192
  expect(feedbackParsed.attempt).toBe(1);
196
193
  expect(feedbackParsed.remaining_attempts).toBe(2);
197
194
 
198
- // Verify retry count incremented
199
- expect(feedbackParsed.attempt).toBe(1);
200
-
201
- // Verify message was sent with issues
195
+ // Verify retry_context is provided for coordinator to spawn retry
196
+ expect(feedbackParsed.retry_context).toBeDefined();
197
+ expect(feedbackParsed.retry_context.task_id).toBe(subtask.id);
198
+ expect(feedbackParsed.retry_context.attempt).toBe(1);
199
+ expect(feedbackParsed.retry_context.max_attempts).toBe(3);
200
+ expect(feedbackParsed.retry_context.issues).toEqual(issues);
201
+ expect(feedbackParsed.retry_context.next_action).toContain("swarm_spawn_retry");
202
+
203
+ // ARCHITECTURE CHANGE: No longer sends message to worker
204
+ // Worker is considered "dead" - coordinator must spawn retry
205
+ // Inbox should remain empty
202
206
  const messages = await swarmMail.getInbox(
203
207
  testProjectPath,
204
208
  "TestWorker",
205
209
  { limit: 10 }
206
210
  );
207
- expect(messages.length).toBeGreaterThan(0);
208
-
209
- const needsChangesMessage = messages.find((m) =>
210
- m.subject.includes("NEEDS CHANGES")
211
- );
212
- expect(needsChangesMessage).toBeDefined();
213
- expect(needsChangesMessage?.subject).toContain(subtask.id);
214
- expect(needsChangesMessage?.subject).toContain("attempt 1/3");
211
+ expect(messages.length).toBe(0);
215
212
  });
216
213
 
217
214
  test("3-strike rule: task marked blocked after 3 rejections", async () => {
@@ -275,16 +272,14 @@ describe("swarm_review integration", () => {
275
272
  const updatedCell = await hive.getCell(testProjectPath, subtask.id);
276
273
  expect(updatedCell?.status).toBe("blocked");
277
274
 
278
- // Verify final failure message was sent
275
+ // ARCHITECTURE CHANGE: No longer sends failure message
276
+ // Worker is dead, coordinator handles escalation
277
+ // Inbox should remain empty
279
278
  const messages = await swarmMail.getInbox(
280
279
  testProjectPath,
281
280
  "TestWorker",
282
281
  { limit: 10 }
283
282
  );
284
-
285
- const failedMessage = messages.find((m) => m.subject.includes("FAILED"));
286
- expect(failedMessage).toBeDefined();
287
- expect(failedMessage?.subject).toContain("max review attempts reached");
288
- expect(failedMessage?.importance).toBe("urgent");
283
+ expect(messages.length).toBe(0);
289
284
  });
290
285
  });