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.
- package/.hive/issues.jsonl +12 -0
- package/.hive/memories.jsonl +255 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +289 -289
- package/CHANGELOG.md +98 -0
- package/README.md +29 -1
- package/bin/swarm.test.ts +272 -1
- package/bin/swarm.ts +226 -4
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/index.d.ts +95 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11848 -124
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/plugin.js +11722 -112
- package/dist/swarm-orchestrate.d.ts +105 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +54 -2
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-research.d.ts +127 -0
- package/dist/swarm-research.d.ts.map +1 -0
- package/dist/swarm-review.d.ts.map +1 -1
- package/dist/swarm.d.ts +56 -1
- package/dist/swarm.d.ts.map +1 -1
- package/evals/compaction-resumption.eval.ts +289 -0
- package/evals/coordinator-behavior.eval.ts +307 -0
- package/evals/fixtures/compaction-cases.ts +350 -0
- package/evals/scorers/compaction-scorers.ts +305 -0
- package/evals/scorers/index.ts +12 -0
- package/package.json +5 -2
- package/src/compaction-hook.test.ts +617 -1
- package/src/compaction-hook.ts +291 -18
- package/src/index.ts +29 -0
- package/src/logger.test.ts +189 -0
- package/src/logger.ts +135 -0
- package/src/swarm-prompts.test.ts +164 -1
- package/src/swarm-prompts.ts +178 -4
- package/src/swarm-review.test.ts +177 -0
- 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:
|
|
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
|
+
});
|
package/src/swarm-prompts.ts
CHANGED
|
@@ -743,10 +743,32 @@ swarm_review_feedback(
|
|
|
743
743
|
)
|
|
744
744
|
\`\`\`
|
|
745
745
|
|
|
746
|
-
### Step 5:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
-
|
|
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
|
};
|
package/src/swarm-review.test.ts
CHANGED
|
@@ -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
|
+
});
|