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.
- 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 +133 -0
- package/README.md +29 -1
- package/bin/swarm.test.ts +342 -1
- package/bin/swarm.ts +351 -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 +639 -1
- package/src/compaction-hook.ts +488 -18
- package/src/index.ts +29 -0
- package/src/logger.test.ts +189 -0
- package/src/logger.ts +135 -0
- package/src/swarm-decompose.ts +0 -7
- package/src/swarm-prompts.test.ts +164 -1
- package/src/swarm-prompts.ts +179 -12
- package/src/swarm-review.test.ts +177 -0
- package/src/swarm-review.ts +12 -47
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { mkdir, rm, readdir } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
|
|
7
|
+
describe("Logger Infrastructure", () => {
|
|
8
|
+
const testLogDir = join(homedir(), ".config", "swarm-tools", "logs-test");
|
|
9
|
+
let originalEnv: string | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
// Clean up test log directory
|
|
13
|
+
if (existsSync(testLogDir)) {
|
|
14
|
+
await rm(testLogDir, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
await mkdir(testLogDir, { recursive: true });
|
|
17
|
+
originalEnv = process.env.SWARM_LOG_PRETTY;
|
|
18
|
+
|
|
19
|
+
// Clear module cache to reset logger instances
|
|
20
|
+
delete require.cache[require.resolve("./logger")];
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
// Restore environment
|
|
25
|
+
if (originalEnv !== undefined) {
|
|
26
|
+
process.env.SWARM_LOG_PRETTY = originalEnv;
|
|
27
|
+
} else {
|
|
28
|
+
delete process.env.SWARM_LOG_PRETTY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clean up test directory
|
|
32
|
+
if (existsSync(testLogDir)) {
|
|
33
|
+
await rm(testLogDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("getLogger", () => {
|
|
38
|
+
test("returns a valid Pino logger instance", async () => {
|
|
39
|
+
const { getLogger } = await import("./logger");
|
|
40
|
+
const logger = getLogger(testLogDir);
|
|
41
|
+
|
|
42
|
+
expect(logger).toBeDefined();
|
|
43
|
+
expect(typeof logger.info).toBe("function");
|
|
44
|
+
expect(typeof logger.error).toBe("function");
|
|
45
|
+
expect(typeof logger.debug).toBe("function");
|
|
46
|
+
expect(typeof logger.warn).toBe("function");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("creates log directory if it doesn't exist", async () => {
|
|
50
|
+
const newDir = join(testLogDir, "nested", "path");
|
|
51
|
+
const { getLogger } = await import("./logger");
|
|
52
|
+
|
|
53
|
+
getLogger(newDir);
|
|
54
|
+
|
|
55
|
+
expect(existsSync(newDir)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("creates log file with numeric rotation pattern", async () => {
|
|
59
|
+
const { getLogger } = await import("./logger");
|
|
60
|
+
const logger = getLogger(testLogDir);
|
|
61
|
+
|
|
62
|
+
// Write a log to force file creation
|
|
63
|
+
logger.info("test message");
|
|
64
|
+
|
|
65
|
+
// Wait for async file creation (pino-roll is async)
|
|
66
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
67
|
+
|
|
68
|
+
const files = await readdir(testLogDir);
|
|
69
|
+
// pino-roll format: {filename}.{number}log (e.g., swarm.1log)
|
|
70
|
+
const logFile = files.find((f) => f.match(/^swarm\.\d+log$/));
|
|
71
|
+
|
|
72
|
+
expect(logFile).toBeDefined();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("writes log entries to file", async () => {
|
|
76
|
+
const { getLogger } = await import("./logger");
|
|
77
|
+
const logger = getLogger(testLogDir);
|
|
78
|
+
|
|
79
|
+
logger.info("test log entry");
|
|
80
|
+
logger.error("test error entry");
|
|
81
|
+
|
|
82
|
+
// Wait for async file writes
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
84
|
+
|
|
85
|
+
const files = await readdir(testLogDir);
|
|
86
|
+
expect(files.length).toBeGreaterThan(0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("createChildLogger", () => {
|
|
91
|
+
test("creates child logger with module namespace", async () => {
|
|
92
|
+
const { getLogger, createChildLogger } = await import("./logger");
|
|
93
|
+
getLogger(testLogDir); // Initialize main logger
|
|
94
|
+
|
|
95
|
+
const childLogger = createChildLogger("compaction", testLogDir);
|
|
96
|
+
|
|
97
|
+
expect(childLogger).toBeDefined();
|
|
98
|
+
expect(typeof childLogger.info).toBe("function");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("child logger writes to module-specific file", async () => {
|
|
102
|
+
const { getLogger, createChildLogger } = await import("./logger");
|
|
103
|
+
getLogger(testLogDir);
|
|
104
|
+
|
|
105
|
+
const childLogger = createChildLogger("compaction", testLogDir);
|
|
106
|
+
childLogger.info("compaction test message");
|
|
107
|
+
|
|
108
|
+
// Wait for async file writes
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
110
|
+
|
|
111
|
+
const files = await readdir(testLogDir);
|
|
112
|
+
// pino-roll format: {module}.{number}log (e.g., compaction.1log)
|
|
113
|
+
const compactionLog = files.find((f) => f.match(/^compaction\.\d+log$/));
|
|
114
|
+
|
|
115
|
+
expect(compactionLog).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("multiple child loggers write to separate files", async () => {
|
|
119
|
+
const { getLogger, createChildLogger } = await import("./logger");
|
|
120
|
+
getLogger(testLogDir);
|
|
121
|
+
|
|
122
|
+
const compactionLogger = createChildLogger("compaction", testLogDir);
|
|
123
|
+
const cliLogger = createChildLogger("cli", testLogDir);
|
|
124
|
+
|
|
125
|
+
compactionLogger.info("compaction message");
|
|
126
|
+
cliLogger.info("cli message");
|
|
127
|
+
|
|
128
|
+
// Wait for async file writes
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
130
|
+
|
|
131
|
+
const files = await readdir(testLogDir);
|
|
132
|
+
// pino-roll format: {module}.{number}log
|
|
133
|
+
const compactionLog = files.find((f) => f.match(/^compaction\.\d+log$/));
|
|
134
|
+
const cliLog = files.find((f) => f.match(/^cli\.\d+log$/));
|
|
135
|
+
|
|
136
|
+
expect(compactionLog).toBeDefined();
|
|
137
|
+
expect(cliLog).toBeDefined();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("Pretty mode", () => {
|
|
142
|
+
test("respects SWARM_LOG_PRETTY=1 environment variable", async () => {
|
|
143
|
+
process.env.SWARM_LOG_PRETTY = "1";
|
|
144
|
+
|
|
145
|
+
// Force reimport to pick up env var
|
|
146
|
+
delete require.cache[require.resolve("./logger")];
|
|
147
|
+
const { getLogger } = await import("./logger");
|
|
148
|
+
|
|
149
|
+
const logger = getLogger(testLogDir);
|
|
150
|
+
|
|
151
|
+
// If pretty mode is enabled, logger should have prettyPrint config
|
|
152
|
+
// We can't easily inspect Pino internals, but we can verify it doesn't throw
|
|
153
|
+
expect(logger).toBeDefined();
|
|
154
|
+
expect(typeof logger.info).toBe("function");
|
|
155
|
+
|
|
156
|
+
logger.info("pretty test message");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("works without pretty mode by default", async () => {
|
|
160
|
+
delete process.env.SWARM_LOG_PRETTY;
|
|
161
|
+
|
|
162
|
+
// Force reimport
|
|
163
|
+
delete require.cache[require.resolve("./logger")];
|
|
164
|
+
const { getLogger } = await import("./logger");
|
|
165
|
+
|
|
166
|
+
const logger = getLogger(testLogDir);
|
|
167
|
+
|
|
168
|
+
expect(logger).toBeDefined();
|
|
169
|
+
logger.info("normal mode message");
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("Log rotation", () => {
|
|
174
|
+
test("sets up daily rotation with 14-day retention", async () => {
|
|
175
|
+
const { getLogger } = await import("./logger");
|
|
176
|
+
const logger = getLogger(testLogDir);
|
|
177
|
+
|
|
178
|
+
// Write logs to trigger rotation setup
|
|
179
|
+
logger.info("rotation test");
|
|
180
|
+
|
|
181
|
+
// Wait for async file creation
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
183
|
+
|
|
184
|
+
// Verify log file exists (rotation config is internal to pino-roll)
|
|
185
|
+
const files = await readdir(testLogDir);
|
|
186
|
+
expect(files.length).toBeGreaterThan(0);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
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();
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -434,12 +434,6 @@ export const swarm_decompose = tool({
|
|
|
434
434
|
"Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
|
|
435
435
|
args: {
|
|
436
436
|
task: tool.schema.string().min(1).describe("Task description to decompose"),
|
|
437
|
-
max_subtasks: tool.schema
|
|
438
|
-
.number()
|
|
439
|
-
.int()
|
|
440
|
-
.min(1)
|
|
441
|
-
.optional()
|
|
442
|
-
.describe("Suggested max subtasks (optional - LLM decides if not specified)"),
|
|
443
437
|
context: tool.schema
|
|
444
438
|
.string()
|
|
445
439
|
.optional()
|
|
@@ -503,7 +497,6 @@ export const swarm_decompose = tool({
|
|
|
503
497
|
: "## Additional Context\n(none provided)";
|
|
504
498
|
|
|
505
499
|
const prompt = DECOMPOSITION_PROMPT.replace("{task}", args.task)
|
|
506
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString())
|
|
507
500
|
.replace("{context_section}", contextSection);
|
|
508
501
|
|
|
509
502
|
// Return the prompt and schema info for the caller
|
|
@@ -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
|
+
});
|