opencode-swarm-plugin 0.33.0 → 0.35.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 (41) hide show
  1. package/.hive/issues.jsonl +12 -0
  2. package/.hive/memories.jsonl +255 -1
  3. package/.turbo/turbo-build.log +4 -4
  4. package/.turbo/turbo-test.log +289 -289
  5. package/CHANGELOG.md +133 -0
  6. package/README.md +29 -1
  7. package/bin/swarm.test.ts +342 -1
  8. package/bin/swarm.ts +351 -4
  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 +95 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +11848 -124
  14. package/dist/logger.d.ts +34 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/plugin.js +11722 -112
  17. package/dist/swarm-orchestrate.d.ts +105 -0
  18. package/dist/swarm-orchestrate.d.ts.map +1 -1
  19. package/dist/swarm-prompts.d.ts +54 -2
  20. package/dist/swarm-prompts.d.ts.map +1 -1
  21. package/dist/swarm-research.d.ts +127 -0
  22. package/dist/swarm-research.d.ts.map +1 -0
  23. package/dist/swarm-review.d.ts.map +1 -1
  24. package/dist/swarm.d.ts +56 -1
  25. package/dist/swarm.d.ts.map +1 -1
  26. package/evals/compaction-resumption.eval.ts +289 -0
  27. package/evals/coordinator-behavior.eval.ts +307 -0
  28. package/evals/fixtures/compaction-cases.ts +350 -0
  29. package/evals/scorers/compaction-scorers.ts +305 -0
  30. package/evals/scorers/index.ts +12 -0
  31. package/package.json +5 -2
  32. package/src/compaction-hook.test.ts +639 -1
  33. package/src/compaction-hook.ts +488 -18
  34. package/src/index.ts +29 -0
  35. package/src/logger.test.ts +189 -0
  36. package/src/logger.ts +135 -0
  37. package/src/swarm-decompose.ts +0 -7
  38. package/src/swarm-prompts.test.ts +164 -1
  39. package/src/swarm-prompts.ts +179 -12
  40. package/src/swarm-review.test.ts +177 -0
  41. package/src/swarm-review.ts +12 -47
@@ -743,10 +743,32 @@ swarm_review_feedback(
743
743
  )
744
744
  \`\`\`
745
745
 
746
- ### Step 5: ONLY THEN Continue
747
- - If approved: Close the cell, spawn next worker
748
- - If needs_changes: Worker gets feedback, retries (max 3 attempts)
749
- - If 3 failures: Mark blocked, escalate to human
746
+ ### Step 5: Take Action Based on Review
747
+
748
+ **If APPROVED:**
749
+ - Close the cell with hive_close
750
+ - Spawn next worker (if any) using swarm_spawn_subtask
751
+
752
+ **If NEEDS_CHANGES:**
753
+ - Generate retry prompt:
754
+ \`\`\`
755
+ swarm_spawn_retry(
756
+ bead_id="{task_id}",
757
+ epic_id="{epic_id}",
758
+ original_prompt="<original prompt>",
759
+ attempt=<current_attempt>,
760
+ issues="<JSON from swarm_review_feedback>",
761
+ diff="<git diff of previous changes>",
762
+ files=[{files_touched}],
763
+ project_path="{project_key}"
764
+ )
765
+ \`\`\`
766
+ - Spawn new worker with Task() using the retry prompt
767
+ - Increment attempt counter (max 3 attempts)
768
+
769
+ **If 3 FAILURES:**
770
+ - Mark task as blocked: \`hive_update(id="{task_id}", status="blocked")\`
771
+ - Escalate to human - likely an architectural problem, not execution issue
750
772
 
751
773
  **⚠️ DO NOT spawn the next worker until review is complete.**
752
774
  `;
@@ -1164,6 +1186,157 @@ export const swarm_spawn_researcher = tool({
1164
1186
  },
1165
1187
  });
1166
1188
 
1189
+ /**
1190
+ * Generate retry prompt for a worker that needs to fix issues from review feedback
1191
+ *
1192
+ * Coordinators use this when swarm_review_feedback returns "needs_changes".
1193
+ * Creates a new worker spawn with context about what went wrong and what to fix.
1194
+ */
1195
+ export const swarm_spawn_retry = tool({
1196
+ description:
1197
+ "Generate retry prompt for a worker that failed review. Includes issues from previous attempt, diff if provided, and standard worker contract.",
1198
+ args: {
1199
+ bead_id: tool.schema.string().describe("Original subtask bead ID"),
1200
+ epic_id: tool.schema.string().describe("Parent epic bead ID"),
1201
+ original_prompt: tool.schema.string().describe("The prompt given to failed worker"),
1202
+ attempt: tool.schema.number().int().min(1).max(3).describe("Current attempt number (1, 2, or 3)"),
1203
+ issues: tool.schema.string().describe("JSON array of ReviewIssue objects from swarm_review_feedback"),
1204
+ diff: tool.schema
1205
+ .string()
1206
+ .optional()
1207
+ .describe("Git diff of previous changes"),
1208
+ files: tool.schema
1209
+ .array(tool.schema.string())
1210
+ .describe("Files to modify (from original subtask)"),
1211
+ project_path: tool.schema
1212
+ .string()
1213
+ .optional()
1214
+ .describe("Absolute project path for swarmmail_init"),
1215
+ },
1216
+ async execute(args) {
1217
+ // Validate attempt number
1218
+ if (args.attempt > 3) {
1219
+ throw new Error(
1220
+ `Retry attempt ${args.attempt} exceeds maximum of 3. After 3 failures, task should be marked blocked.`,
1221
+ );
1222
+ }
1223
+
1224
+ // Parse issues
1225
+ let issuesArray: Array<{
1226
+ file: string;
1227
+ line: number;
1228
+ issue: string;
1229
+ suggestion: string;
1230
+ }> = [];
1231
+ try {
1232
+ issuesArray = JSON.parse(args.issues);
1233
+ } catch (e) {
1234
+ // If issues is not valid JSON, treat as empty array
1235
+ issuesArray = [];
1236
+ }
1237
+
1238
+ // Format issues section
1239
+ const issuesSection = issuesArray.length > 0
1240
+ ? `## ISSUES FROM PREVIOUS ATTEMPT
1241
+
1242
+ The previous attempt had the following issues that need to be fixed:
1243
+
1244
+ ${issuesArray
1245
+ .map(
1246
+ (issue, idx) =>
1247
+ `**${idx + 1}. ${issue.file}:${issue.line}**
1248
+ - **Issue**: ${issue.issue}
1249
+ - **Suggestion**: ${issue.suggestion}`,
1250
+ )
1251
+ .join("\n\n")}
1252
+
1253
+ **Critical**: Fix these issues while preserving any working changes from the previous attempt.`
1254
+ : "";
1255
+
1256
+ // Format diff section
1257
+ const diffSection = args.diff
1258
+ ? `## PREVIOUS ATTEMPT
1259
+
1260
+ Here's what was tried in the previous attempt:
1261
+
1262
+ \`\`\`diff
1263
+ ${args.diff}
1264
+ \`\`\`
1265
+
1266
+ Review this carefully - some changes may be correct and should be preserved.`
1267
+ : "";
1268
+
1269
+ // Build the retry prompt
1270
+ const retryPrompt = `⚠️ **RETRY ATTEMPT ${args.attempt}/3**
1271
+
1272
+ This is a retry of a previously attempted subtask. The coordinator reviewed the previous attempt and found issues that need to be fixed.
1273
+
1274
+ ${issuesSection}
1275
+
1276
+ ${diffSection}
1277
+
1278
+ ## ORIGINAL TASK
1279
+
1280
+ ${args.original_prompt}
1281
+
1282
+ ## YOUR MISSION
1283
+
1284
+ 1. **Understand what went wrong** - Read the issues carefully
1285
+ 2. **Fix the specific problems** - Address each issue listed above
1286
+ 3. **Preserve working code** - Don't throw away correct changes from previous attempt
1287
+ 4. **Follow the standard worker contract** - See below
1288
+
1289
+ ## MANDATORY WORKER CONTRACT
1290
+
1291
+ ### Step 1: Initialize (REQUIRED FIRST)
1292
+ \`\`\`
1293
+ swarmmail_init(project_path="${args.project_path || "$PWD"}", task_description="${args.bead_id}: Retry ${args.attempt}/3")
1294
+ \`\`\`
1295
+
1296
+ ### Step 2: Reserve Files
1297
+ \`\`\`
1298
+ swarmmail_reserve(
1299
+ paths=${JSON.stringify(args.files)},
1300
+ reason="${args.bead_id}: Retry attempt ${args.attempt}",
1301
+ exclusive=true
1302
+ )
1303
+ \`\`\`
1304
+
1305
+ ### Step 3: Fix the Issues
1306
+ - Address each issue listed above
1307
+ - Run tests to verify fixes
1308
+ - Don't introduce new bugs
1309
+
1310
+ ### Step 4: Complete
1311
+ \`\`\`
1312
+ swarm_complete(
1313
+ project_key="${args.project_path || "$PWD"}",
1314
+ agent_name="<your-agent-name>",
1315
+ bead_id="${args.bead_id}",
1316
+ summary="Fixed issues from review: <brief summary>",
1317
+ files_touched=[<files you modified>]
1318
+ )
1319
+ \`\`\`
1320
+
1321
+ **Remember**: This is attempt ${args.attempt} of 3. If this fails review again, there may be an architectural problem that needs human intervention.
1322
+
1323
+ Begin work now.`;
1324
+
1325
+ return JSON.stringify(
1326
+ {
1327
+ prompt: retryPrompt,
1328
+ bead_id: args.bead_id,
1329
+ attempt: args.attempt,
1330
+ max_attempts: 3,
1331
+ files: args.files,
1332
+ issues_count: issuesArray.length,
1333
+ },
1334
+ null,
1335
+ 2,
1336
+ );
1337
+ },
1338
+ });
1339
+
1167
1340
  /**
1168
1341
  * Generate self-evaluation prompt
1169
1342
  */
@@ -1220,12 +1393,6 @@ export const swarm_plan_prompt = tool({
1220
1393
  .enum(["file-based", "feature-based", "risk-based", "auto"])
1221
1394
  .optional()
1222
1395
  .describe("Decomposition strategy (default: auto-detect)"),
1223
- max_subtasks: tool.schema
1224
- .number()
1225
- .int()
1226
- .min(1)
1227
- .optional()
1228
- .describe("Suggested max subtasks (optional - LLM decides if not specified)"),
1229
1396
  context: tool.schema
1230
1397
  .string()
1231
1398
  .optional()
@@ -1309,8 +1476,7 @@ export const swarm_plan_prompt = tool({
1309
1476
  .replace("{strategy_guidelines}", strategyGuidelines)
1310
1477
  .replace("{context_section}", contextSection)
1311
1478
  .replace("{cass_history}", "") // Empty for now
1312
- .replace("{skills_context}", skillsContext || "")
1313
- .replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
1479
+ .replace("{skills_context}", skillsContext || "");
1314
1480
 
1315
1481
  return JSON.stringify(
1316
1482
  {
@@ -1353,6 +1519,7 @@ export const promptTools = {
1353
1519
  swarm_subtask_prompt,
1354
1520
  swarm_spawn_subtask,
1355
1521
  swarm_spawn_researcher,
1522
+ swarm_spawn_retry,
1356
1523
  swarm_evaluation_prompt,
1357
1524
  swarm_plan_prompt,
1358
1525
  };
@@ -700,3 +700,180 @@ describe("edge cases", () => {
700
700
  expect(prompt).toContain(longDiff);
701
701
  });
702
702
  });
703
+
704
+ // ============================================================================
705
+ // Coordinator-Driven Retry: swarm_review_feedback returns retry_context
706
+ // ============================================================================
707
+
708
+ describe("swarm_review_feedback retry_context", () => {
709
+ beforeEach(() => {
710
+ clearReviewStatus("bd-retry-test");
711
+ vi.clearAllMocks();
712
+ });
713
+
714
+ it("returns retry_context when status is needs_changes", async () => {
715
+ const issues = JSON.stringify([
716
+ { file: "src/auth.ts", line: 42, issue: "Missing null check", suggestion: "Add null check" }
717
+ ]);
718
+
719
+ const result = await swarm_review_feedback.execute(
720
+ {
721
+ project_key: "/tmp/test-project",
722
+ task_id: "bd-retry-test",
723
+ worker_id: "worker-test",
724
+ status: "needs_changes",
725
+ issues,
726
+ },
727
+ mockContext
728
+ );
729
+
730
+ const parsed = JSON.parse(result);
731
+ expect(parsed.success).toBe(true);
732
+ expect(parsed.status).toBe("needs_changes");
733
+ // NEW: Should include retry_context for coordinator
734
+ expect(parsed).toHaveProperty("retry_context");
735
+ expect(parsed.retry_context).toHaveProperty("task_id", "bd-retry-test");
736
+ expect(parsed.retry_context).toHaveProperty("attempt", 1);
737
+ expect(parsed.retry_context).toHaveProperty("issues");
738
+ expect(parsed.retry_context.issues).toHaveLength(1);
739
+ });
740
+
741
+ it("retry_context includes issues in structured format", async () => {
742
+ const issues = [
743
+ { file: "src/a.ts", line: 10, issue: "Bug A", suggestion: "Fix A" },
744
+ { file: "src/b.ts", line: 20, issue: "Bug B", suggestion: "Fix B" },
745
+ ];
746
+
747
+ const result = await swarm_review_feedback.execute(
748
+ {
749
+ project_key: "/tmp/test-project",
750
+ task_id: "bd-retry-test",
751
+ worker_id: "worker-test",
752
+ status: "needs_changes",
753
+ issues: JSON.stringify(issues),
754
+ },
755
+ mockContext
756
+ );
757
+
758
+ const parsed = JSON.parse(result);
759
+ expect(parsed.retry_context.issues).toEqual(issues);
760
+ });
761
+
762
+ it("retry_context includes next_action hint for coordinator", async () => {
763
+ const issues = JSON.stringify([{ file: "x.ts", issue: "bug" }]);
764
+
765
+ const result = await swarm_review_feedback.execute(
766
+ {
767
+ project_key: "/tmp/test-project",
768
+ task_id: "bd-retry-test",
769
+ worker_id: "worker-test",
770
+ status: "needs_changes",
771
+ issues,
772
+ },
773
+ mockContext
774
+ );
775
+
776
+ const parsed = JSON.parse(result);
777
+ // Should tell coordinator what to do next
778
+ expect(parsed.retry_context).toHaveProperty("next_action");
779
+ expect(parsed.retry_context.next_action).toContain("swarm_spawn_retry");
780
+ });
781
+
782
+ it("does NOT include retry_context when approved", async () => {
783
+ const result = await swarm_review_feedback.execute(
784
+ {
785
+ project_key: "/tmp/test-project",
786
+ task_id: "bd-retry-test",
787
+ worker_id: "worker-test",
788
+ status: "approved",
789
+ summary: "Looks good!",
790
+ },
791
+ mockContext
792
+ );
793
+
794
+ const parsed = JSON.parse(result);
795
+ expect(parsed.success).toBe(true);
796
+ expect(parsed.status).toBe("approved");
797
+ expect(parsed).not.toHaveProperty("retry_context");
798
+ });
799
+
800
+ it("does NOT include retry_context when task fails (3 attempts)", async () => {
801
+ const issues = JSON.stringify([{ file: "x.ts", issue: "still broken" }]);
802
+
803
+ // Exhaust all attempts
804
+ let result: string = "";
805
+ for (let i = 0; i < 3; i++) {
806
+ result = await swarm_review_feedback.execute(
807
+ {
808
+ project_key: "/tmp/test-project",
809
+ task_id: "bd-retry-test",
810
+ worker_id: "worker-test",
811
+ status: "needs_changes",
812
+ issues,
813
+ },
814
+ mockContext
815
+ );
816
+ }
817
+
818
+ const parsed = JSON.parse(result);
819
+ expect(parsed.task_failed).toBe(true);
820
+ // No retry_context when task is failed - nothing more to retry
821
+ expect(parsed).not.toHaveProperty("retry_context");
822
+ });
823
+
824
+ it("retry_context includes max_attempts for coordinator awareness", async () => {
825
+ const issues = JSON.stringify([{ file: "x.ts", issue: "bug" }]);
826
+
827
+ const result = await swarm_review_feedback.execute(
828
+ {
829
+ project_key: "/tmp/test-project",
830
+ task_id: "bd-retry-test",
831
+ worker_id: "worker-test",
832
+ status: "needs_changes",
833
+ issues,
834
+ },
835
+ mockContext
836
+ );
837
+
838
+ const parsed = JSON.parse(result);
839
+ expect(parsed.retry_context).toHaveProperty("max_attempts", 3);
840
+ });
841
+
842
+ it("does NOT send message to dead worker for needs_changes", async () => {
843
+ const { sendSwarmMessage } = await import("swarm-mail");
844
+ const issues = JSON.stringify([{ file: "x.ts", issue: "bug" }]);
845
+
846
+ await swarm_review_feedback.execute(
847
+ {
848
+ project_key: "/tmp/test-project",
849
+ task_id: "bd-retry-test",
850
+ worker_id: "worker-test",
851
+ status: "needs_changes",
852
+ issues,
853
+ },
854
+ mockContext
855
+ );
856
+
857
+ // Should NOT call sendSwarmMessage for needs_changes
858
+ // Workers are dead - they can't read messages
859
+ expect(sendSwarmMessage).not.toHaveBeenCalled();
860
+ });
861
+
862
+ it("DOES send message for approved status (audit trail)", async () => {
863
+ const { sendSwarmMessage } = await import("swarm-mail");
864
+
865
+ await swarm_review_feedback.execute(
866
+ {
867
+ project_key: "/tmp/test-project",
868
+ task_id: "bd-retry-test",
869
+ worker_id: "worker-test",
870
+ status: "approved",
871
+ summary: "Good work!",
872
+ },
873
+ mockContext
874
+ );
875
+
876
+ // Approved messages are still sent for audit trail
877
+ expect(sendSwarmMessage).toHaveBeenCalled();
878
+ });
879
+ });
@@ -551,23 +551,8 @@ You may now complete the task with \`swarm_complete\`.`,
551
551
  }
552
552
  }
553
553
 
554
- // Send failure message
555
- await sendSwarmMessage({
556
- projectPath: args.project_key,
557
- fromAgent: "coordinator",
558
- toAgents: [args.worker_id],
559
- subject: `FAILED: ${args.task_id} - max review attempts reached`,
560
- body: `## Task Failed ✗
561
-
562
- Maximum review attempts (${MAX_REVIEW_ATTEMPTS}) reached.
563
-
564
- **Last Issues:**
565
- ${parsedIssues.map((i: ReviewIssue) => `- ${i.file}${i.line ? `:${i.line}` : ""}: ${i.issue}`).join("\n")}
566
-
567
- The task has been marked as blocked. A human or different approach is needed.`,
568
- threadId: epicId,
569
- importance: "urgent",
570
- });
554
+ // NO sendSwarmMessage - worker is dead, can't read it
555
+ // Coordinator handles retry or escalation
571
556
 
572
557
  return JSON.stringify(
573
558
  {
@@ -584,35 +569,8 @@ The task has been marked as blocked. A human or different approach is needed.`,
584
569
  );
585
570
  }
586
571
 
587
- // Send feedback message
588
- const issuesList = parsedIssues
589
- .map((i: ReviewIssue) => {
590
- let line = `- **${i.file}**`;
591
- if (i.line) line += `:${i.line}`;
592
- line += `: ${i.issue}`;
593
- if (i.suggestion) line += `\n → ${i.suggestion}`;
594
- return line;
595
- })
596
- .join("\n");
597
-
598
- await sendSwarmMessage({
599
- projectPath: args.project_key,
600
- fromAgent: "coordinator",
601
- toAgents: [args.worker_id],
602
- subject: `NEEDS CHANGES: ${args.task_id} (attempt ${attemptNumber}/${MAX_REVIEW_ATTEMPTS})`,
603
- body: `## Review: Changes Needed
604
-
605
- ${args.summary || "Please address the following issues:"}
606
-
607
- **Issues:**
608
- ${issuesList}
609
-
610
- **Remaining attempts:** ${remaining}
611
-
612
- Please fix these issues and request another review.`,
613
- threadId: epicId,
614
- importance: "high",
615
- });
572
+ // NO sendSwarmMessage for needs_changes - worker is dead
573
+ // Instead, return retry_context for coordinator to use with swarm_spawn_retry
616
574
 
617
575
  return JSON.stringify(
618
576
  {
@@ -622,7 +580,14 @@ Please fix these issues and request another review.`,
622
580
  attempt: attemptNumber,
623
581
  remaining_attempts: remaining,
624
582
  issues: parsedIssues,
625
- message: `Feedback sent. ${remaining} attempt(s) remaining.`,
583
+ message: `Review feedback ready. ${remaining} attempt(s) remaining.`,
584
+ retry_context: {
585
+ task_id: args.task_id,
586
+ attempt: attemptNumber,
587
+ max_attempts: MAX_REVIEW_ATTEMPTS,
588
+ issues: parsedIssues,
589
+ next_action: "Use swarm_spawn_retry to spawn new worker with these issues",
590
+ },
626
591
  },
627
592
  null,
628
593
  2