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.
- package/.hive/issues.jsonl +16 -0
- package/.hive/memories.jsonl +13 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +286 -286
- package/CHANGELOG.md +170 -0
- package/README.md +33 -0
- package/bin/swarm.test.ts +106 -0
- package/bin/swarm.ts +181 -208
- package/dist/hive.d.ts +59 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +453 -118
- package/dist/plugin.js +452 -118
- package/dist/swarm-decompose.d.ts +30 -0
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm.d.ts +15 -0
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +27 -10
- package/examples/plugin-wrapper-template.ts +60 -8
- package/package.json +4 -1
- package/src/compaction-hook.test.ts +97 -2
- package/src/compaction-hook.ts +32 -2
- package/src/hive.integration.test.ts +148 -0
- package/src/hive.ts +89 -0
- package/src/swarm-decompose.test.ts +188 -0
- package/src/swarm-decompose.ts +52 -1
- package/src/swarm-orchestrate.test.ts +270 -7
- package/src/swarm-orchestrate.ts +98 -11
- package/src/swarm-prompts.test.ts +121 -0
- package/src/swarm-prompts.ts +295 -2
- package/src/swarm-research.integration.test.ts +157 -0
- package/src/swarm-review.integration.test.ts +24 -29
|
@@ -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
|
|
5
|
+
* Verifies that review approval/rejection properly updates state.
|
|
6
6
|
*
|
|
7
|
-
* **
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
199
|
-
expect(feedbackParsed.
|
|
200
|
-
|
|
201
|
-
|
|
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).
|
|
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
|
-
//
|
|
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
|
});
|