opencode-swarm-plugin 0.33.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 (40) 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 +98 -0
  6. package/README.md +29 -1
  7. package/bin/swarm.test.ts +272 -1
  8. package/bin/swarm.ts +226 -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 +617 -1
  33. package/src/compaction-hook.ts +291 -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-prompts.test.ts +164 -1
  38. package/src/swarm-prompts.ts +178 -4
  39. package/src/swarm-review.test.ts +177 -0
  40. package/src/swarm-review.ts +12 -47
package/src/logger.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Logger infrastructure using Pino with daily rotation
3
+ *
4
+ * Features:
5
+ * - Daily log rotation via pino-roll (numeric format: swarm.1log, swarm.2log, etc.)
6
+ * - 14-day retention (14 files max in addition to current file)
7
+ * - Module-specific child loggers with separate log files
8
+ * - Pretty mode for development (SWARM_LOG_PRETTY=1 env var)
9
+ * - Logs to ~/.config/swarm-tools/logs/ by default
10
+ *
11
+ * Note: pino-roll uses numeric rotation (e.g., swarm.1log, swarm.2log) not date-based names.
12
+ * Files rotate daily based on frequency='daily', with a maximum of 14 retained files.
13
+ */
14
+
15
+ import { mkdirSync, existsSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { homedir } from "node:os";
18
+ import type { Logger } from "pino";
19
+ import pino from "pino";
20
+
21
+ const DEFAULT_LOG_DIR = join(homedir(), ".config", "swarm-tools", "logs");
22
+
23
+ /**
24
+ * Creates the log directory if it doesn't exist
25
+ */
26
+ function ensureLogDir(logDir: string): void {
27
+ if (!existsSync(logDir)) {
28
+ mkdirSync(logDir, { recursive: true });
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Creates a Pino transport with file rotation
34
+ *
35
+ * @param filename - Log file base name (e.g., "swarm" becomes swarm.1log, swarm.2log, etc.)
36
+ * @param logDir - Directory to store logs
37
+ */
38
+ function createTransport(
39
+ filename: string,
40
+ logDir: string,
41
+ ): pino.TransportTargetOptions {
42
+ const isPretty = process.env.SWARM_LOG_PRETTY === "1";
43
+
44
+ if (isPretty) {
45
+ // Pretty mode - output to console with pino-pretty
46
+ return {
47
+ target: "pino-pretty",
48
+ options: {
49
+ colorize: true,
50
+ translateTime: "HH:MM:ss",
51
+ ignore: "pid,hostname",
52
+ },
53
+ };
54
+ }
55
+
56
+ // Production mode - file rotation with pino-roll
57
+ // pino-roll format: {file}.{number}{extension}
58
+ // So "swarm" becomes "swarm.1log", "swarm.2log", etc.
59
+ return {
60
+ target: "pino-roll",
61
+ options: {
62
+ file: join(logDir, filename),
63
+ frequency: "daily",
64
+ extension: "log",
65
+ limit: { count: 14 },
66
+ mkdir: true,
67
+ },
68
+ };
69
+ }
70
+
71
+ const loggerCache = new Map<string, Logger>();
72
+
73
+ /**
74
+ * Gets or creates the main logger instance
75
+ *
76
+ * @param logDir - Optional log directory (defaults to ~/.config/swarm-tools/logs)
77
+ * @returns Pino logger instance
78
+ */
79
+ export function getLogger(logDir: string = DEFAULT_LOG_DIR): Logger {
80
+ const cacheKey = `swarm:${logDir}`;
81
+
82
+ if (loggerCache.has(cacheKey)) {
83
+ return loggerCache.get(cacheKey)!;
84
+ }
85
+
86
+ ensureLogDir(logDir);
87
+
88
+ const logger = pino(
89
+ {
90
+ level: process.env.LOG_LEVEL || "info",
91
+ timestamp: pino.stdTimeFunctions.isoTime,
92
+ },
93
+ pino.transport(createTransport("swarm", logDir)),
94
+ );
95
+
96
+ loggerCache.set(cacheKey, logger);
97
+ return logger;
98
+ }
99
+
100
+ /**
101
+ * Creates a child logger for a specific module with its own log file
102
+ *
103
+ * @param module - Module name (e.g., "compaction", "cli")
104
+ * @param logDir - Optional log directory (defaults to ~/.config/swarm-tools/logs)
105
+ * @returns Child logger instance
106
+ */
107
+ export function createChildLogger(
108
+ module: string,
109
+ logDir: string = DEFAULT_LOG_DIR,
110
+ ): Logger {
111
+ const cacheKey = `${module}:${logDir}`;
112
+
113
+ if (loggerCache.has(cacheKey)) {
114
+ return loggerCache.get(cacheKey)!;
115
+ }
116
+
117
+ ensureLogDir(logDir);
118
+
119
+ const childLogger = pino(
120
+ {
121
+ level: process.env.LOG_LEVEL || "info",
122
+ timestamp: pino.stdTimeFunctions.isoTime,
123
+ },
124
+ pino.transport(createTransport(module, logDir)),
125
+ );
126
+
127
+ const logger = childLogger.child({ module });
128
+ loggerCache.set(cacheKey, logger);
129
+ return logger;
130
+ }
131
+
132
+ /**
133
+ * Default logger instance for immediate use
134
+ */
135
+ export const logger = getLogger();
@@ -218,7 +218,8 @@ describe("swarm_spawn_subtask tool", () => {
218
218
  expect(instructions).toContain("Step 3: Evaluate Against Criteria");
219
219
  expect(instructions).toContain("Step 4: Send Feedback");
220
220
  expect(instructions).toContain("swarm_review_feedback");
221
- 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
222
223
  });
223
224
 
224
225
  test("post_completion_instructions substitutes placeholders", async () => {
@@ -655,3 +656,165 @@ describe("swarm_spawn_researcher tool", () => {
655
656
  expect(parsed.check_upgrades).toBe(true);
656
657
  });
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
+ });
@@ -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
  */
@@ -1353,6 +1526,7 @@ export const promptTools = {
1353
1526
  swarm_subtask_prompt,
1354
1527
  swarm_spawn_subtask,
1355
1528
  swarm_spawn_researcher,
1529
+ swarm_spawn_retry,
1356
1530
  swarm_evaluation_prompt,
1357
1531
  swarm_plan_prompt,
1358
1532
  };
@@ -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
+ });