opencode-swarm-plugin 0.32.0 → 0.34.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.
Files changed (55) hide show
  1. package/.hive/issues.jsonl +12 -0
  2. package/.hive/memories.jsonl +255 -1
  3. package/.turbo/turbo-build.log +9 -10
  4. package/.turbo/turbo-test.log +343 -337
  5. package/CHANGELOG.md +358 -0
  6. package/README.md +152 -179
  7. package/bin/swarm.test.ts +303 -1
  8. package/bin/swarm.ts +473 -16
  9. package/dist/compaction-hook.d.ts +1 -1
  10. package/dist/compaction-hook.d.ts.map +1 -1
  11. package/dist/index.d.ts +112 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +12380 -131
  14. package/dist/logger.d.ts +34 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/observability-tools.d.ts +116 -0
  17. package/dist/observability-tools.d.ts.map +1 -0
  18. package/dist/plugin.js +12254 -119
  19. package/dist/skills.d.ts.map +1 -1
  20. package/dist/swarm-orchestrate.d.ts +105 -0
  21. package/dist/swarm-orchestrate.d.ts.map +1 -1
  22. package/dist/swarm-prompts.d.ts +113 -2
  23. package/dist/swarm-prompts.d.ts.map +1 -1
  24. package/dist/swarm-research.d.ts +127 -0
  25. package/dist/swarm-research.d.ts.map +1 -0
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +73 -1
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/compaction-resumption.eval.ts +289 -0
  30. package/evals/coordinator-behavior.eval.ts +307 -0
  31. package/evals/fixtures/compaction-cases.ts +350 -0
  32. package/evals/scorers/compaction-scorers.ts +305 -0
  33. package/evals/scorers/index.ts +12 -0
  34. package/examples/plugin-wrapper-template.ts +297 -8
  35. package/package.json +6 -2
  36. package/src/compaction-hook.test.ts +617 -1
  37. package/src/compaction-hook.ts +291 -18
  38. package/src/index.ts +54 -1
  39. package/src/logger.test.ts +189 -0
  40. package/src/logger.ts +135 -0
  41. package/src/observability-tools.test.ts +346 -0
  42. package/src/observability-tools.ts +594 -0
  43. package/src/skills.integration.test.ts +137 -1
  44. package/src/skills.test.ts +42 -1
  45. package/src/skills.ts +8 -4
  46. package/src/swarm-orchestrate.test.ts +123 -0
  47. package/src/swarm-orchestrate.ts +183 -0
  48. package/src/swarm-prompts.test.ts +553 -1
  49. package/src/swarm-prompts.ts +406 -4
  50. package/src/swarm-research.integration.test.ts +544 -0
  51. package/src/swarm-research.test.ts +698 -0
  52. package/src/swarm-research.ts +472 -0
  53. package/src/swarm-review.test.ts +177 -0
  54. package/src/swarm-review.ts +12 -47
  55. package/src/swarm.ts +6 -3
@@ -8,7 +8,9 @@
8
8
  import { describe, expect, test } from "bun:test";
9
9
  import {
10
10
  formatSubtaskPromptV2,
11
+ formatResearcherPrompt,
11
12
  SUBTASK_PROMPT_V2,
13
+ RESEARCHER_PROMPT,
12
14
  } from "./swarm-prompts";
13
15
 
14
16
  describe("SUBTASK_PROMPT_V2", () => {
@@ -216,7 +218,8 @@ describe("swarm_spawn_subtask tool", () => {
216
218
  expect(instructions).toContain("Step 3: Evaluate Against Criteria");
217
219
  expect(instructions).toContain("Step 4: Send Feedback");
218
220
  expect(instructions).toContain("swarm_review_feedback");
219
- expect(instructions).toContain("Step 5: ONLY THEN Continue");
221
+ expect(instructions).toContain("Step 5: Take Action Based on Review");
222
+ expect(instructions).toContain("swarm_spawn_retry"); // Should include retry flow
220
223
  });
221
224
 
222
225
  test("post_completion_instructions substitutes placeholders", async () => {
@@ -266,3 +269,552 @@ describe("swarm_spawn_subtask tool", () => {
266
269
  expect(instructions).toContain("DO THIS IMMEDIATELY");
267
270
  });
268
271
  });
272
+
273
+ describe("RESEARCHER_PROMPT", () => {
274
+ describe("required sections", () => {
275
+ test("includes IDENTITY section with research_id and epic_id", () => {
276
+ expect(RESEARCHER_PROMPT).toContain("## [IDENTITY]");
277
+ expect(RESEARCHER_PROMPT).toContain("{research_id}");
278
+ expect(RESEARCHER_PROMPT).toContain("{epic_id}");
279
+ });
280
+
281
+ test("includes MISSION section explaining the role", () => {
282
+ expect(RESEARCHER_PROMPT).toContain("## [MISSION]");
283
+ expect(RESEARCHER_PROMPT).toMatch(/gather.*documentation/i);
284
+ });
285
+
286
+ test("includes WORKFLOW section with numbered steps", () => {
287
+ expect(RESEARCHER_PROMPT).toContain("## [WORKFLOW]");
288
+ expect(RESEARCHER_PROMPT).toContain("### Step 1:");
289
+ expect(RESEARCHER_PROMPT).toContain("### Step 2:");
290
+ });
291
+
292
+ test("includes CRITICAL REQUIREMENTS section", () => {
293
+ expect(RESEARCHER_PROMPT).toContain("## [CRITICAL REQUIREMENTS]");
294
+ expect(RESEARCHER_PROMPT).toMatch(/NON-NEGOTIABLE/i);
295
+ });
296
+ });
297
+
298
+ describe("workflow steps", () => {
299
+ test("Step 1 is swarmmail_init (MANDATORY FIRST)", () => {
300
+ expect(RESEARCHER_PROMPT).toMatch(/### Step 1:.*Initialize/i);
301
+ expect(RESEARCHER_PROMPT).toContain("swarmmail_init");
302
+ expect(RESEARCHER_PROMPT).toContain("project_path");
303
+ });
304
+
305
+ test("Step 2 is discovering available documentation tools", () => {
306
+ const step2Match = RESEARCHER_PROMPT.match(/### Step 2:[\s\S]*?### Step 3:/);
307
+ expect(step2Match).not.toBeNull();
308
+ if (!step2Match) return;
309
+
310
+ const step2Content = step2Match[0];
311
+ expect(step2Content).toMatch(/discover.*tools/i);
312
+ expect(step2Content).toContain("nextjs_docs");
313
+ expect(step2Content).toContain("context7");
314
+ expect(step2Content).toContain("fetch");
315
+ expect(step2Content).toContain("pdf-brain");
316
+ });
317
+
318
+ test("Step 3 is reading installed versions", () => {
319
+ const step3Match = RESEARCHER_PROMPT.match(/### Step 3:[\s\S]*?### Step 4:/);
320
+ expect(step3Match).not.toBeNull();
321
+ if (!step3Match) return;
322
+
323
+ const step3Content = step3Match[0];
324
+ expect(step3Content).toMatch(/read.*installed.*version/i);
325
+ expect(step3Content).toContain("package.json");
326
+ });
327
+
328
+ test("Step 4 is fetching documentation", () => {
329
+ const step4Match = RESEARCHER_PROMPT.match(/### Step 4:[\s\S]*?### Step 5:/);
330
+ expect(step4Match).not.toBeNull();
331
+ if (!step4Match) return;
332
+
333
+ const step4Content = step4Match[0];
334
+ expect(step4Content).toMatch(/fetch.*documentation/i);
335
+ expect(step4Content).toContain("INSTALLED version");
336
+ });
337
+
338
+ test("Step 5 is storing detailed findings in semantic-memory", () => {
339
+ const step5Match = RESEARCHER_PROMPT.match(/### Step 5:[\s\S]*?### Step 6:/);
340
+ expect(step5Match).not.toBeNull();
341
+ if (!step5Match) return;
342
+
343
+ const step5Content = step5Match[0];
344
+ expect(step5Content).toContain("semantic-memory_store");
345
+ expect(step5Content).toMatch(/store.*individually/i);
346
+ });
347
+
348
+ test("Step 6 is broadcasting summary to coordinator", () => {
349
+ const step6Match = RESEARCHER_PROMPT.match(/### Step 6:[\s\S]*?### Step 7:/);
350
+ expect(step6Match).not.toBeNull();
351
+ if (!step6Match) return;
352
+
353
+ const step6Content = step6Match[0];
354
+ expect(step6Content).toContain("swarmmail_send");
355
+ expect(step6Content).toContain("coordinator");
356
+ });
357
+
358
+ test("Step 7 is returning structured JSON output", () => {
359
+ const step7Match = RESEARCHER_PROMPT.match(/### Step 7:[\s\S]*?(?=## \[|$)/);
360
+ expect(step7Match).not.toBeNull();
361
+ if (!step7Match) return;
362
+
363
+ const step7Content = step7Match[0];
364
+ expect(step7Content).toContain("JSON");
365
+ expect(step7Content).toContain("technologies");
366
+ expect(step7Content).toContain("summary");
367
+ });
368
+ });
369
+
370
+ describe("coordinator-provided tech stack", () => {
371
+ test("emphasizes that coordinator provides the tech list", () => {
372
+ expect(RESEARCHER_PROMPT).toMatch(/COORDINATOR PROVIDED.*TECHNOLOGIES/i);
373
+ expect(RESEARCHER_PROMPT).toContain("{tech_stack}");
374
+ });
375
+
376
+ test("clarifies researcher does NOT discover what to research", () => {
377
+ expect(RESEARCHER_PROMPT).toMatch(/NOT discover what to research/i);
378
+ expect(RESEARCHER_PROMPT).toMatch(/DO discover.*TOOLS/i);
379
+ });
380
+ });
381
+
382
+ describe("upgrade comparison mode", () => {
383
+ test("includes placeholder for check_upgrades mode", () => {
384
+ expect(RESEARCHER_PROMPT).toContain("{check_upgrades}");
385
+ });
386
+
387
+ test("mentions comparing installed vs latest when in upgrade mode", () => {
388
+ expect(RESEARCHER_PROMPT).toMatch(/check-upgrades/i);
389
+ expect(RESEARCHER_PROMPT).toMatch(/compare|latest.*version/i);
390
+ });
391
+ });
392
+
393
+ describe("output requirements", () => {
394
+ test("specifies TWO output destinations: semantic-memory and return JSON", () => {
395
+ expect(RESEARCHER_PROMPT).toMatch(/TWO places/i);
396
+ expect(RESEARCHER_PROMPT).toContain("semantic-memory");
397
+ expect(RESEARCHER_PROMPT).toContain("Return JSON");
398
+ });
399
+
400
+ test("explains semantic-memory is for detailed findings", () => {
401
+ expect(RESEARCHER_PROMPT).toMatch(/semantic-memory.*detailed/i);
402
+ });
403
+
404
+ test("explains return JSON is for condensed summary", () => {
405
+ expect(RESEARCHER_PROMPT).toMatch(/return.*condensed.*summary/i);
406
+ });
407
+ });
408
+ });
409
+
410
+ describe("formatResearcherPrompt", () => {
411
+ test("substitutes research_id placeholder", () => {
412
+ const result = formatResearcherPrompt({
413
+ research_id: "research-abc123",
414
+ epic_id: "epic-xyz789",
415
+ tech_stack: ["Next.js", "React"],
416
+ project_path: "/path/to/project",
417
+ check_upgrades: false,
418
+ });
419
+
420
+ expect(result).toContain("research-abc123");
421
+ expect(result).not.toContain("{research_id}");
422
+ });
423
+
424
+ test("substitutes epic_id placeholder", () => {
425
+ const result = formatResearcherPrompt({
426
+ research_id: "research-abc123",
427
+ epic_id: "epic-xyz789",
428
+ tech_stack: ["Next.js"],
429
+ project_path: "/path/to/project",
430
+ check_upgrades: false,
431
+ });
432
+
433
+ expect(result).toContain("epic-xyz789");
434
+ expect(result).not.toContain("{epic_id}");
435
+ });
436
+
437
+ test("formats tech_stack as bulleted list", () => {
438
+ const result = formatResearcherPrompt({
439
+ research_id: "research-abc123",
440
+ epic_id: "epic-xyz789",
441
+ tech_stack: ["Next.js", "React", "TypeScript"],
442
+ project_path: "/path/to/project",
443
+ check_upgrades: false,
444
+ });
445
+
446
+ expect(result).toContain("- Next.js");
447
+ expect(result).toContain("- React");
448
+ expect(result).toContain("- TypeScript");
449
+ });
450
+
451
+ test("substitutes project_path placeholder", () => {
452
+ const result = formatResearcherPrompt({
453
+ research_id: "research-abc123",
454
+ epic_id: "epic-xyz789",
455
+ tech_stack: ["Next.js"],
456
+ project_path: "/Users/joel/Code/my-project",
457
+ check_upgrades: false,
458
+ });
459
+
460
+ expect(result).toContain("/Users/joel/Code/my-project");
461
+ expect(result).not.toContain("{project_path}");
462
+ });
463
+
464
+ test("includes DEFAULT MODE text when check_upgrades=false", () => {
465
+ const result = formatResearcherPrompt({
466
+ research_id: "research-abc123",
467
+ epic_id: "epic-xyz789",
468
+ tech_stack: ["Next.js"],
469
+ project_path: "/path/to/project",
470
+ check_upgrades: false,
471
+ });
472
+
473
+ expect(result).toContain("DEFAULT MODE");
474
+ expect(result).toContain("INSTALLED versions only");
475
+ });
476
+
477
+ test("includes UPGRADE COMPARISON MODE text when check_upgrades=true", () => {
478
+ const result = formatResearcherPrompt({
479
+ research_id: "research-abc123",
480
+ epic_id: "epic-xyz789",
481
+ tech_stack: ["Next.js"],
482
+ project_path: "/path/to/project",
483
+ check_upgrades: true,
484
+ });
485
+
486
+ expect(result).toContain("UPGRADE COMPARISON MODE");
487
+ expect(result).toContain("BOTH installed AND latest");
488
+ expect(result).toContain("breaking changes");
489
+ });
490
+ });
491
+
492
+ describe("on-demand research section", () => {
493
+ test("includes ON-DEMAND RESEARCH section after Step 9", () => {
494
+ // Find Step 9 and the section after it
495
+ const step9Pos = SUBTASK_PROMPT_V2.indexOf("### Step 9:");
496
+ const swarmMailPos = SUBTASK_PROMPT_V2.indexOf("## [SWARM MAIL COMMUNICATION]");
497
+
498
+ expect(step9Pos).toBeGreaterThan(0);
499
+ expect(swarmMailPos).toBeGreaterThan(step9Pos);
500
+
501
+ // Extract the section between Step 9 and SWARM MAIL
502
+ const betweenSection = SUBTASK_PROMPT_V2.substring(step9Pos, swarmMailPos);
503
+
504
+ expect(betweenSection).toContain("## [ON-DEMAND RESEARCH]");
505
+ });
506
+
507
+ test("research section instructs to check semantic-memory first", () => {
508
+ const researchMatch = SUBTASK_PROMPT_V2.match(/## \[ON-DEMAND RESEARCH\][\s\S]*?## \[SWARM MAIL/);
509
+ expect(researchMatch).not.toBeNull();
510
+ if (!researchMatch) return;
511
+
512
+ const researchContent = researchMatch[0];
513
+ expect(researchContent).toContain("semantic-memory_find");
514
+ expect(researchContent).toMatch(/check.*semantic-memory.*first/i);
515
+ });
516
+
517
+ test("research section includes swarm_spawn_researcher tool usage", () => {
518
+ const researchMatch = SUBTASK_PROMPT_V2.match(/## \[ON-DEMAND RESEARCH\][\s\S]*?## \[SWARM MAIL/);
519
+ expect(researchMatch).not.toBeNull();
520
+ if (!researchMatch) return;
521
+
522
+ const researchContent = researchMatch[0];
523
+ expect(researchContent).toContain("swarm_spawn_researcher");
524
+ });
525
+
526
+ test("research section lists specific research triggers", () => {
527
+ const researchMatch = SUBTASK_PROMPT_V2.match(/## \[ON-DEMAND RESEARCH\][\s\S]*?## \[SWARM MAIL/);
528
+ expect(researchMatch).not.toBeNull();
529
+ if (!researchMatch) return;
530
+
531
+ const researchContent = researchMatch[0];
532
+
533
+ // Should list when TO research
534
+ expect(researchContent).toMatch(/triggers|when to research/i);
535
+ expect(researchContent).toMatch(/API.*works|breaking changes|outdated/i);
536
+ });
537
+
538
+ test("research section lists when NOT to research", () => {
539
+ const researchMatch = SUBTASK_PROMPT_V2.match(/## \[ON-DEMAND RESEARCH\][\s\S]*?## \[SWARM MAIL/);
540
+ expect(researchMatch).not.toBeNull();
541
+ if (!researchMatch) return;
542
+
543
+ const researchContent = researchMatch[0];
544
+
545
+ // Should list when to SKIP research
546
+ expect(researchContent).toMatch(/don't research|skip research/i);
547
+ expect(researchContent).toMatch(/standard patterns|well-documented|obvious/i);
548
+ });
549
+
550
+ test("research section includes 3-step workflow", () => {
551
+ const researchMatch = SUBTASK_PROMPT_V2.match(/## \[ON-DEMAND RESEARCH\][\s\S]*?## \[SWARM MAIL/);
552
+ expect(researchMatch).not.toBeNull();
553
+ if (!researchMatch) return;
554
+
555
+ const researchContent = researchMatch[0];
556
+
557
+ // Should have numbered steps
558
+ expect(researchContent).toMatch(/1\.\s*.*Check semantic-memory/i);
559
+ expect(researchContent).toMatch(/2\.\s*.*spawn researcher/i);
560
+ expect(researchContent).toMatch(/3\.\s*.*wait.*continue/i);
561
+ });
562
+ });
563
+
564
+ describe("swarm_spawn_researcher tool", () => {
565
+ test("returns JSON with prompt field", async () => {
566
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
567
+
568
+ const result = await swarm_spawn_researcher.execute({
569
+ research_id: "research-abc123",
570
+ epic_id: "epic-xyz789",
571
+ tech_stack: ["Next.js", "React"],
572
+ project_path: "/Users/joel/Code/project",
573
+ });
574
+
575
+ const parsed = JSON.parse(result);
576
+ expect(parsed).toHaveProperty("prompt");
577
+ expect(typeof parsed.prompt).toBe("string");
578
+ expect(parsed.prompt.length).toBeGreaterThan(100);
579
+ });
580
+
581
+ test("returns subagent_type field as 'swarm/researcher'", async () => {
582
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
583
+
584
+ const result = await swarm_spawn_researcher.execute({
585
+ research_id: "research-abc123",
586
+ epic_id: "epic-xyz789",
587
+ tech_stack: ["Next.js"],
588
+ project_path: "/Users/joel/Code/project",
589
+ });
590
+
591
+ const parsed = JSON.parse(result);
592
+ expect(parsed.subagent_type).toBe("swarm/researcher");
593
+ });
594
+
595
+ test("returns expected_output schema", async () => {
596
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
597
+
598
+ const result = await swarm_spawn_researcher.execute({
599
+ research_id: "research-abc123",
600
+ epic_id: "epic-xyz789",
601
+ tech_stack: ["Next.js"],
602
+ project_path: "/Users/joel/Code/project",
603
+ });
604
+
605
+ const parsed = JSON.parse(result);
606
+ expect(parsed).toHaveProperty("expected_output");
607
+ expect(parsed.expected_output).toHaveProperty("technologies");
608
+ expect(parsed.expected_output).toHaveProperty("summary");
609
+ });
610
+
611
+ test("defaults check_upgrades to false when not provided", async () => {
612
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
613
+
614
+ const result = await swarm_spawn_researcher.execute({
615
+ research_id: "research-abc123",
616
+ epic_id: "epic-xyz789",
617
+ tech_stack: ["Next.js"],
618
+ project_path: "/Users/joel/Code/project",
619
+ });
620
+
621
+ const parsed = JSON.parse(result);
622
+ expect(parsed.check_upgrades).toBe(false);
623
+ });
624
+
625
+ test("respects check_upgrades when provided as true", async () => {
626
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
627
+
628
+ const result = await swarm_spawn_researcher.execute({
629
+ research_id: "research-abc123",
630
+ epic_id: "epic-xyz789",
631
+ tech_stack: ["Next.js"],
632
+ project_path: "/Users/joel/Code/project",
633
+ check_upgrades: true,
634
+ });
635
+
636
+ const parsed = JSON.parse(result);
637
+ expect(parsed.check_upgrades).toBe(true);
638
+ });
639
+
640
+ test("includes all input parameters in returned JSON", async () => {
641
+ const { swarm_spawn_researcher } = await import("./swarm-prompts");
642
+
643
+ const result = await swarm_spawn_researcher.execute({
644
+ research_id: "research-abc123",
645
+ epic_id: "epic-xyz789",
646
+ tech_stack: ["Next.js", "React", "TypeScript"],
647
+ project_path: "/Users/joel/Code/project",
648
+ check_upgrades: true,
649
+ });
650
+
651
+ const parsed = JSON.parse(result);
652
+ expect(parsed.research_id).toBe("research-abc123");
653
+ expect(parsed.epic_id).toBe("epic-xyz789");
654
+ expect(parsed.tech_stack).toEqual(["Next.js", "React", "TypeScript"]);
655
+ expect(parsed.project_path).toBe("/Users/joel/Code/project");
656
+ expect(parsed.check_upgrades).toBe(true);
657
+ });
658
+ });
659
+
660
+ describe("swarm_spawn_retry tool", () => {
661
+ test("generates valid retry prompt with issues", async () => {
662
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
663
+
664
+ const result = await swarm_spawn_retry.execute({
665
+ bead_id: "test-project-abc123-task1",
666
+ epic_id: "test-project-abc123-epic1",
667
+ original_prompt: "Original task: implement feature X",
668
+ attempt: 1,
669
+ issues: JSON.stringify([
670
+ { file: "src/feature.ts", line: 42, issue: "Missing null check", suggestion: "Add null check" }
671
+ ]),
672
+ files: ["src/feature.ts"],
673
+ project_path: "/Users/joel/Code/project",
674
+ });
675
+
676
+ const parsed = JSON.parse(result);
677
+ expect(parsed).toHaveProperty("prompt");
678
+ expect(typeof parsed.prompt).toBe("string");
679
+ expect(parsed.prompt).toContain("RETRY ATTEMPT");
680
+ expect(parsed.prompt).toContain("Missing null check");
681
+ });
682
+
683
+ test("includes attempt number in prompt header", async () => {
684
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
685
+
686
+ const result = await swarm_spawn_retry.execute({
687
+ bead_id: "test-project-abc123-task1",
688
+ epic_id: "test-project-abc123-epic1",
689
+ original_prompt: "Original task",
690
+ attempt: 2,
691
+ issues: "[]",
692
+ files: ["src/test.ts"],
693
+ });
694
+
695
+ const parsed = JSON.parse(result);
696
+ expect(parsed.prompt).toContain("RETRY ATTEMPT 2/3");
697
+ expect(parsed.attempt).toBe(2);
698
+ });
699
+
700
+ test("includes diff when provided", async () => {
701
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
702
+
703
+ const diffContent = `diff --git a/src/test.ts b/src/test.ts
704
+ +++ b/src/test.ts
705
+ @@ -1 +1 @@
706
+ -const x = 1;
707
+ +const x = null;`;
708
+
709
+ const result = await swarm_spawn_retry.execute({
710
+ bead_id: "test-project-abc123-task1",
711
+ epic_id: "test-project-abc123-epic1",
712
+ original_prompt: "Original task",
713
+ attempt: 1,
714
+ issues: "[]",
715
+ diff: diffContent,
716
+ files: ["src/test.ts"],
717
+ });
718
+
719
+ const parsed = JSON.parse(result);
720
+ expect(parsed.prompt).toContain(diffContent);
721
+ expect(parsed.prompt).toContain("PREVIOUS ATTEMPT");
722
+ });
723
+
724
+ test("rejects attempt > 3 with error", async () => {
725
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
726
+
727
+ await expect(async () => {
728
+ await swarm_spawn_retry.execute({
729
+ bead_id: "test-project-abc123-task1",
730
+ epic_id: "test-project-abc123-epic1",
731
+ original_prompt: "Original task",
732
+ attempt: 4,
733
+ issues: "[]",
734
+ files: ["src/test.ts"],
735
+ });
736
+ }).toThrow(/attempt.*exceeds.*maximum/i);
737
+ });
738
+
739
+ test("formats issues as readable list", async () => {
740
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
741
+
742
+ const issues = [
743
+ { file: "src/a.ts", line: 10, issue: "Missing error handling", suggestion: "Add try-catch" },
744
+ { file: "src/b.ts", line: 20, issue: "Type mismatch", suggestion: "Fix types" }
745
+ ];
746
+
747
+ const result = await swarm_spawn_retry.execute({
748
+ bead_id: "test-project-abc123-task1",
749
+ epic_id: "test-project-abc123-epic1",
750
+ original_prompt: "Original task",
751
+ attempt: 1,
752
+ issues: JSON.stringify(issues),
753
+ files: ["src/a.ts", "src/b.ts"],
754
+ });
755
+
756
+ const parsed = JSON.parse(result);
757
+ expect(parsed.prompt).toContain("ISSUES FROM PREVIOUS ATTEMPT");
758
+ expect(parsed.prompt).toContain("src/a.ts:10");
759
+ expect(parsed.prompt).toContain("Missing error handling");
760
+ expect(parsed.prompt).toContain("src/b.ts:20");
761
+ expect(parsed.prompt).toContain("Type mismatch");
762
+ });
763
+
764
+ test("returns expected response structure", async () => {
765
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
766
+
767
+ const result = await swarm_spawn_retry.execute({
768
+ bead_id: "test-project-abc123-task1",
769
+ epic_id: "test-project-abc123-epic1",
770
+ original_prompt: "Original task",
771
+ attempt: 1,
772
+ issues: "[]",
773
+ files: ["src/test.ts"],
774
+ project_path: "/Users/joel/Code/project",
775
+ });
776
+
777
+ const parsed = JSON.parse(result);
778
+ expect(parsed).toHaveProperty("prompt");
779
+ expect(parsed).toHaveProperty("bead_id", "test-project-abc123-task1");
780
+ expect(parsed).toHaveProperty("attempt", 1);
781
+ expect(parsed).toHaveProperty("max_attempts", 3);
782
+ expect(parsed).toHaveProperty("files");
783
+ expect(parsed.files).toEqual(["src/test.ts"]);
784
+ });
785
+
786
+ test("includes standard worker contract (swarmmail_init, reserve, complete)", async () => {
787
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
788
+
789
+ const result = await swarm_spawn_retry.execute({
790
+ bead_id: "test-project-abc123-task1",
791
+ epic_id: "test-project-abc123-epic1",
792
+ original_prompt: "Original task",
793
+ attempt: 1,
794
+ issues: "[]",
795
+ files: ["src/test.ts"],
796
+ project_path: "/Users/joel/Code/project",
797
+ });
798
+
799
+ const parsed = JSON.parse(result);
800
+ expect(parsed.prompt).toContain("swarmmail_init");
801
+ expect(parsed.prompt).toContain("swarmmail_reserve");
802
+ expect(parsed.prompt).toContain("swarm_complete");
803
+ });
804
+
805
+ test("instructs to preserve working changes", async () => {
806
+ const { swarm_spawn_retry } = await import("./swarm-prompts");
807
+
808
+ const result = await swarm_spawn_retry.execute({
809
+ bead_id: "test-project-abc123-task1",
810
+ epic_id: "test-project-abc123-epic1",
811
+ original_prompt: "Original task",
812
+ attempt: 1,
813
+ issues: JSON.stringify([{ file: "src/test.ts", line: 1, issue: "Bug", suggestion: "Fix" }]),
814
+ files: ["src/test.ts"],
815
+ });
816
+
817
+ const parsed = JSON.parse(result);
818
+ expect(parsed.prompt).toMatch(/preserve.*working|fix.*while preserving/i);
819
+ });
820
+ });