opencode-swarm-plugin 0.37.0 → 0.38.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 +9 -5
- package/.hive/memories.jsonl +13 -1
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +319 -319
- package/CHANGELOG.md +128 -0
- package/README.md +33 -0
- package/bin/swarm.ts +2 -208
- package/dist/hive.d.ts +59 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +453 -118
- package/dist/plugin.js +452 -118
- package/dist/swarm-decompose.d.ts +30 -0
- package/dist/swarm-decompose.d.ts.map +1 -1
- package/dist/swarm.d.ts +15 -0
- package/dist/swarm.d.ts.map +1 -1
- package/evals/README.md +27 -10
- package/examples/plugin-wrapper-template.ts +60 -8
- package/package.json +4 -1
- package/src/compaction-hook.test.ts +97 -2
- package/src/compaction-hook.ts +32 -2
- package/src/swarm-decompose.test.ts +40 -47
- package/src/swarm-orchestrate.test.ts +270 -7
- package/src/swarm-orchestrate.ts +98 -11
- package/src/swarm-prompts.test.ts +121 -0
- package/src/swarm-prompts.ts +295 -2
- package/src/swarm-research.integration.test.ts +157 -0
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* - Researcher spawning for identified technologies
|
|
7
7
|
* - Summary collection from semantic-memory
|
|
8
8
|
* - Research result aggregation
|
|
9
|
+
* - Eval capture integration (captureSubtaskOutcome wiring)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import { describe, test, expect, beforeEach } from "bun:test";
|
|
12
|
-
import { runResearchPhase, extractTechStack } from "./swarm-orchestrate";
|
|
12
|
+
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|
13
|
+
import { runResearchPhase, extractTechStack, swarm_complete } from "./swarm-orchestrate";
|
|
14
|
+
import * as evalCapture from "./eval-capture.js";
|
|
15
|
+
import * as fs from "node:fs";
|
|
13
16
|
|
|
14
17
|
describe("extractTechStack", () => {
|
|
15
18
|
test("extracts Next.js from task description", () => {
|
|
@@ -115,9 +118,269 @@ describe("runResearchPhase", () => {
|
|
|
115
118
|
});
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
describe("swarm_research_phase tool", () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// describe("swarm_research_phase tool", () => {
|
|
122
|
+
// test.todo("exposes research phase as plugin tool");
|
|
123
|
+
// test.todo("validates task parameter");
|
|
124
|
+
// test.todo("validates project_path parameter");
|
|
125
|
+
// test.todo("returns JSON string with research results");
|
|
126
|
+
// });
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Eval Capture Integration Tests (swarm_complete)
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
describe("captureSubtaskOutcome integration", () => {
|
|
133
|
+
const mockContext = {
|
|
134
|
+
sessionID: `test-complete-${Date.now()}`,
|
|
135
|
+
messageID: `test-message-${Date.now()}`,
|
|
136
|
+
agent: "test-agent",
|
|
137
|
+
abort: new AbortController().signal,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let testProjectPath: string;
|
|
141
|
+
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
testProjectPath = `/tmp/test-swarm-complete-${Date.now()}`;
|
|
144
|
+
fs.mkdirSync(testProjectPath, { recursive: true });
|
|
145
|
+
|
|
146
|
+
// Create .hive directory and issues.jsonl
|
|
147
|
+
const hiveDir = `${testProjectPath}/.hive`;
|
|
148
|
+
fs.mkdirSync(hiveDir, { recursive: true });
|
|
149
|
+
fs.writeFileSync(`${hiveDir}/issues.jsonl`, "", "utf-8");
|
|
150
|
+
|
|
151
|
+
// Set hive working directory to testProjectPath
|
|
152
|
+
const { setHiveWorkingDirectory } = await import("./hive");
|
|
153
|
+
setHiveWorkingDirectory(testProjectPath);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
afterEach(() => {
|
|
157
|
+
if (fs.existsSync(testProjectPath)) {
|
|
158
|
+
fs.rmSync(testProjectPath, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("calls captureSubtaskOutcome after successful completion with all params", async () => {
|
|
163
|
+
// Import hive tools
|
|
164
|
+
const { hive_create_epic } = await import("./hive");
|
|
165
|
+
|
|
166
|
+
// Spy on captureSubtaskOutcome
|
|
167
|
+
const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
|
|
168
|
+
|
|
169
|
+
// Create an epic with a subtask using hive_create_epic
|
|
170
|
+
const epicResult = await hive_create_epic.execute({
|
|
171
|
+
epic_title: "Add OAuth",
|
|
172
|
+
epic_description: "Implement OAuth authentication",
|
|
173
|
+
subtasks: [
|
|
174
|
+
{
|
|
175
|
+
title: "Add auth service",
|
|
176
|
+
priority: 2,
|
|
177
|
+
files: ["src/auth/service.ts", "src/auth/schema.ts"],
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}, mockContext);
|
|
181
|
+
|
|
182
|
+
const epicData = JSON.parse(epicResult);
|
|
183
|
+
expect(epicData.success).toBe(true);
|
|
184
|
+
|
|
185
|
+
const epicId = epicData.epic.id;
|
|
186
|
+
const beadId = epicData.subtasks[0].id;
|
|
187
|
+
|
|
188
|
+
const startTime = Date.now() - 120000; // Started 2 minutes ago
|
|
189
|
+
const plannedFiles = ["src/auth/service.ts", "src/auth/schema.ts"];
|
|
190
|
+
const actualFiles = ["src/auth/service.ts", "src/auth/schema.ts", "src/auth/types.ts"];
|
|
191
|
+
|
|
192
|
+
// Call swarm_complete
|
|
193
|
+
const result = await swarm_complete.execute(
|
|
194
|
+
{
|
|
195
|
+
project_key: testProjectPath,
|
|
196
|
+
agent_name: "TestAgent",
|
|
197
|
+
bead_id: beadId,
|
|
198
|
+
summary: "Implemented OAuth service with JWT strategy",
|
|
199
|
+
files_touched: actualFiles,
|
|
200
|
+
skip_verification: true, // Skip verification for test
|
|
201
|
+
skip_review: true, // Skip review for test
|
|
202
|
+
planned_files: plannedFiles,
|
|
203
|
+
start_time: startTime,
|
|
204
|
+
error_count: 0,
|
|
205
|
+
retry_count: 0,
|
|
206
|
+
},
|
|
207
|
+
mockContext,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const parsed = JSON.parse(result);
|
|
211
|
+
expect(parsed.success).toBe(true);
|
|
212
|
+
|
|
213
|
+
// Verify captureSubtaskOutcome was called with correct params
|
|
214
|
+
expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
|
|
215
|
+
|
|
216
|
+
const call = captureOutcomeSpy.mock.calls[0][0];
|
|
217
|
+
expect(call.epicId).toBe(epicId);
|
|
218
|
+
expect(call.projectPath).toBe(testProjectPath);
|
|
219
|
+
expect(call.beadId).toBe(beadId);
|
|
220
|
+
expect(call.title).toBe("Add auth service");
|
|
221
|
+
expect(call.plannedFiles).toEqual(plannedFiles);
|
|
222
|
+
expect(call.actualFiles).toEqual(actualFiles);
|
|
223
|
+
expect(call.durationMs).toBeGreaterThan(0);
|
|
224
|
+
expect(call.errorCount).toBe(0);
|
|
225
|
+
expect(call.retryCount).toBe(0);
|
|
226
|
+
expect(call.success).toBe(true);
|
|
227
|
+
|
|
228
|
+
captureOutcomeSpy.mockRestore();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("does not call captureSubtaskOutcome when required params missing", async () => {
|
|
232
|
+
const { hive_create_epic } = await import("./hive");
|
|
233
|
+
const captureOutcomeSpy = spyOn(evalCapture, "captureSubtaskOutcome");
|
|
234
|
+
|
|
235
|
+
// Create an epic with a subtask
|
|
236
|
+
const epicResult = await hive_create_epic.execute({
|
|
237
|
+
epic_title: "Fix bug",
|
|
238
|
+
subtasks: [
|
|
239
|
+
{
|
|
240
|
+
title: "Fix auth bug",
|
|
241
|
+
priority: 1,
|
|
242
|
+
files: ["src/auth.ts"],
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}, mockContext);
|
|
246
|
+
|
|
247
|
+
const epicData = JSON.parse(epicResult);
|
|
248
|
+
const beadId = epicData.subtasks[0].id;
|
|
249
|
+
|
|
250
|
+
// Call without planned_files or start_time
|
|
251
|
+
const result = await swarm_complete.execute(
|
|
252
|
+
{
|
|
253
|
+
project_key: testProjectPath,
|
|
254
|
+
agent_name: "TestAgent",
|
|
255
|
+
bead_id: beadId,
|
|
256
|
+
summary: "Fixed the bug",
|
|
257
|
+
skip_verification: true,
|
|
258
|
+
skip_review: true,
|
|
259
|
+
// No planned_files, start_time
|
|
260
|
+
},
|
|
261
|
+
mockContext,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const parsed = JSON.parse(result);
|
|
265
|
+
expect(parsed.success).toBe(true);
|
|
266
|
+
|
|
267
|
+
// Capture should still be called, but with default values
|
|
268
|
+
// (The function is called in all success cases, it just handles missing params)
|
|
269
|
+
expect(captureOutcomeSpy).toHaveBeenCalledTimes(1);
|
|
270
|
+
|
|
271
|
+
captureOutcomeSpy.mockRestore();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// ============================================================================
|
|
276
|
+
// Eval Capture Integration Tests (swarm_record_outcome)
|
|
277
|
+
// ============================================================================
|
|
278
|
+
|
|
279
|
+
describe("finalizeEvalRecord integration", () => {
|
|
280
|
+
const mockContext = {
|
|
281
|
+
sessionID: `test-finalize-${Date.now()}`,
|
|
282
|
+
messageID: `test-message-${Date.now()}`,
|
|
283
|
+
agent: "test-agent",
|
|
284
|
+
abort: new AbortController().signal,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
test("calls finalizeEvalRecord when project_path and epic_id provided", async () => {
|
|
288
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
289
|
+
|
|
290
|
+
// Spy on finalizeEvalRecord
|
|
291
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
292
|
+
finalizeEvalSpy.mockReturnValue(null); // Mock return value
|
|
293
|
+
|
|
294
|
+
const testProjectPath = "/tmp/test-project";
|
|
295
|
+
const testEpicId = "bd-test123";
|
|
296
|
+
const testBeadId = `${testEpicId}.0`;
|
|
297
|
+
|
|
298
|
+
// Call swarm_record_outcome with epic_id and project_path
|
|
299
|
+
await swarm_record_outcome.execute({
|
|
300
|
+
bead_id: testBeadId,
|
|
301
|
+
duration_ms: 120000,
|
|
302
|
+
error_count: 0,
|
|
303
|
+
retry_count: 0,
|
|
304
|
+
success: true,
|
|
305
|
+
files_touched: ["src/test.ts"],
|
|
306
|
+
epic_id: testEpicId,
|
|
307
|
+
project_path: testProjectPath,
|
|
308
|
+
}, mockContext);
|
|
309
|
+
|
|
310
|
+
// Verify finalizeEvalRecord was called
|
|
311
|
+
expect(finalizeEvalSpy).toHaveBeenCalledTimes(1);
|
|
312
|
+
expect(finalizeEvalSpy).toHaveBeenCalledWith({
|
|
313
|
+
epicId: testEpicId,
|
|
314
|
+
projectPath: testProjectPath,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
finalizeEvalSpy.mockRestore();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("does not call finalizeEvalRecord when epic_id or project_path missing", async () => {
|
|
321
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
322
|
+
|
|
323
|
+
// Spy on finalizeEvalRecord
|
|
324
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
325
|
+
|
|
326
|
+
const testBeadId = "bd-test123.0";
|
|
327
|
+
|
|
328
|
+
// Call without epic_id or project_path
|
|
329
|
+
await swarm_record_outcome.execute({
|
|
330
|
+
bead_id: testBeadId,
|
|
331
|
+
duration_ms: 120000,
|
|
332
|
+
error_count: 0,
|
|
333
|
+
retry_count: 0,
|
|
334
|
+
success: true,
|
|
335
|
+
}, mockContext);
|
|
336
|
+
|
|
337
|
+
// Verify finalizeEvalRecord was NOT called
|
|
338
|
+
expect(finalizeEvalSpy).toHaveBeenCalledTimes(0);
|
|
339
|
+
|
|
340
|
+
finalizeEvalSpy.mockRestore();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("includes finalized record in response when available", async () => {
|
|
344
|
+
const { swarm_record_outcome } = await import("./swarm-orchestrate");
|
|
345
|
+
|
|
346
|
+
// Mock finalizeEvalRecord to return a record
|
|
347
|
+
const mockFinalRecord = {
|
|
348
|
+
id: "bd-test123",
|
|
349
|
+
timestamp: new Date().toISOString(),
|
|
350
|
+
project_path: "/tmp/test-project",
|
|
351
|
+
task: "Test task",
|
|
352
|
+
strategy: "file-based" as const,
|
|
353
|
+
subtask_count: 2,
|
|
354
|
+
epic_title: "Test Epic",
|
|
355
|
+
subtasks: [],
|
|
356
|
+
overall_success: true,
|
|
357
|
+
total_duration_ms: 240000,
|
|
358
|
+
total_errors: 0,
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const finalizeEvalSpy = spyOn(evalCapture, "finalizeEvalRecord");
|
|
362
|
+
finalizeEvalSpy.mockReturnValue(mockFinalRecord);
|
|
363
|
+
|
|
364
|
+
const testProjectPath = "/tmp/test-project";
|
|
365
|
+
const testEpicId = "bd-test123";
|
|
366
|
+
const testBeadId = `${testEpicId}.0`;
|
|
367
|
+
|
|
368
|
+
// Call with epic_id and project_path
|
|
369
|
+
const result = await swarm_record_outcome.execute({
|
|
370
|
+
bead_id: testBeadId,
|
|
371
|
+
duration_ms: 120000,
|
|
372
|
+
error_count: 0,
|
|
373
|
+
retry_count: 0,
|
|
374
|
+
success: true,
|
|
375
|
+
epic_id: testEpicId,
|
|
376
|
+
project_path: testProjectPath,
|
|
377
|
+
}, mockContext);
|
|
378
|
+
|
|
379
|
+
// Parse result and check for finalized record
|
|
380
|
+
const parsed = JSON.parse(result);
|
|
381
|
+
expect(parsed).toHaveProperty("finalized_eval_record");
|
|
382
|
+
expect(parsed.finalized_eval_record).toEqual(mockFinalRecord);
|
|
383
|
+
|
|
384
|
+
finalizeEvalSpy.mockRestore();
|
|
385
|
+
});
|
|
123
386
|
});
|
package/src/swarm-orchestrate.ts
CHANGED
|
@@ -83,7 +83,8 @@ import {
|
|
|
83
83
|
isReviewApproved,
|
|
84
84
|
getReviewStatus,
|
|
85
85
|
} from "./swarm-review";
|
|
86
|
-
import { captureCoordinatorEvent } from "./eval-capture.js";
|
|
86
|
+
import { captureCoordinatorEvent, type EvalRecord } from "./eval-capture.js";
|
|
87
|
+
import { formatResearcherPrompt } from "./swarm-prompts";
|
|
87
88
|
|
|
88
89
|
// ============================================================================
|
|
89
90
|
// Helper Functions
|
|
@@ -1710,6 +1711,31 @@ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
|
|
|
1710
1711
|
},
|
|
1711
1712
|
};
|
|
1712
1713
|
|
|
1714
|
+
// Capture subtask completion outcome for eval data
|
|
1715
|
+
try {
|
|
1716
|
+
const { captureSubtaskOutcome } = await import("./eval-capture.js");
|
|
1717
|
+
const durationMs = args.start_time ? Date.now() - args.start_time : 0;
|
|
1718
|
+
|
|
1719
|
+
// Determine epic ID: use parent_id if available, otherwise fall back to extracting from bead_id
|
|
1720
|
+
const evalEpicId = cell.parent_id || epicId;
|
|
1721
|
+
|
|
1722
|
+
captureSubtaskOutcome({
|
|
1723
|
+
epicId: evalEpicId,
|
|
1724
|
+
projectPath: args.project_key,
|
|
1725
|
+
beadId: args.bead_id,
|
|
1726
|
+
title: cell.title,
|
|
1727
|
+
plannedFiles: args.planned_files || [],
|
|
1728
|
+
actualFiles: args.files_touched || [],
|
|
1729
|
+
durationMs,
|
|
1730
|
+
errorCount: args.error_count || 0,
|
|
1731
|
+
retryCount: args.retry_count || 0,
|
|
1732
|
+
success: true,
|
|
1733
|
+
});
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
// Non-fatal - don't block completion if capture fails
|
|
1736
|
+
console.warn("[swarm_complete] Failed to capture subtask outcome:", error);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1713
1739
|
// Capture subtask completion outcome
|
|
1714
1740
|
try {
|
|
1715
1741
|
const durationMs = args.start_time ? Date.now() - args.start_time : 0;
|
|
@@ -1946,6 +1972,14 @@ export const swarm_record_outcome = tool({
|
|
|
1946
1972
|
.string()
|
|
1947
1973
|
.optional()
|
|
1948
1974
|
.describe("Detailed failure context (error message, stack trace, etc.)"),
|
|
1975
|
+
project_path: tool.schema
|
|
1976
|
+
.string()
|
|
1977
|
+
.optional()
|
|
1978
|
+
.describe("Project path (for finalizing eval records when all subtasks complete)"),
|
|
1979
|
+
epic_id: tool.schema
|
|
1980
|
+
.string()
|
|
1981
|
+
.optional()
|
|
1982
|
+
.describe("Epic ID (for finalizing eval records when all subtasks complete)"),
|
|
1949
1983
|
},
|
|
1950
1984
|
async execute(args) {
|
|
1951
1985
|
// Build outcome signals
|
|
@@ -1980,6 +2014,21 @@ export const swarm_record_outcome = tool({
|
|
|
1980
2014
|
// Get error patterns from accumulator
|
|
1981
2015
|
const errorStats = await globalErrorAccumulator.getErrorStats(args.bead_id);
|
|
1982
2016
|
|
|
2017
|
+
// Finalize eval record if project_path and epic_id provided
|
|
2018
|
+
let finalizedRecord: EvalRecord | null = null;
|
|
2019
|
+
if (args.project_path && args.epic_id) {
|
|
2020
|
+
try {
|
|
2021
|
+
const { finalizeEvalRecord } = await import("./eval-capture.js");
|
|
2022
|
+
finalizedRecord = finalizeEvalRecord({
|
|
2023
|
+
epicId: args.epic_id,
|
|
2024
|
+
projectPath: args.project_path,
|
|
2025
|
+
});
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
// Non-fatal - log and continue
|
|
2028
|
+
console.warn("[swarm_record_outcome] Failed to finalize eval record:", error);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
1983
2032
|
// Generate feedback events for each criterion
|
|
1984
2033
|
const criteriaToScore = args.criteria ?? [
|
|
1985
2034
|
"type_safe",
|
|
@@ -2030,6 +2079,7 @@ export const swarm_record_outcome = tool({
|
|
|
2030
2079
|
accumulated_errors: errorStats.total,
|
|
2031
2080
|
unresolved_errors: errorStats.unresolved,
|
|
2032
2081
|
},
|
|
2082
|
+
finalized_eval_record: finalizedRecord || undefined,
|
|
2033
2083
|
note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights.",
|
|
2034
2084
|
},
|
|
2035
2085
|
null,
|
|
@@ -2087,12 +2137,28 @@ export function extractTechStack(task: string): string[] {
|
|
|
2087
2137
|
return Array.from(detected);
|
|
2088
2138
|
}
|
|
2089
2139
|
|
|
2140
|
+
/**
|
|
2141
|
+
* Spawn instruction for a researcher worker
|
|
2142
|
+
*/
|
|
2143
|
+
export interface ResearchSpawnInstruction {
|
|
2144
|
+
/** Unique ID for this research task */
|
|
2145
|
+
research_id: string;
|
|
2146
|
+
/** Technology being researched */
|
|
2147
|
+
tech: string;
|
|
2148
|
+
/** Full prompt for the researcher agent */
|
|
2149
|
+
prompt: string;
|
|
2150
|
+
/** Agent type for the Task tool */
|
|
2151
|
+
subagent_type: "swarm/researcher";
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2090
2154
|
/**
|
|
2091
2155
|
* Research result from documentation discovery phase
|
|
2092
2156
|
*/
|
|
2093
2157
|
export interface ResearchResult {
|
|
2094
2158
|
/** Technologies identified and researched */
|
|
2095
2159
|
tech_stack: string[];
|
|
2160
|
+
/** Spawn instructions for researcher workers */
|
|
2161
|
+
spawn_instructions: ResearchSpawnInstruction[];
|
|
2096
2162
|
/** Summaries keyed by technology name */
|
|
2097
2163
|
summaries: Record<string, string>;
|
|
2098
2164
|
/** Semantic-memory IDs where research is stored */
|
|
@@ -2154,24 +2220,45 @@ export async function runResearchPhase(
|
|
|
2154
2220
|
if (techStack.length === 0) {
|
|
2155
2221
|
return {
|
|
2156
2222
|
tech_stack: [],
|
|
2223
|
+
spawn_instructions: [],
|
|
2157
2224
|
summaries: {},
|
|
2158
2225
|
memory_ids: [],
|
|
2159
2226
|
};
|
|
2160
2227
|
}
|
|
2161
2228
|
|
|
2162
|
-
// Step 2:
|
|
2163
|
-
//
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2229
|
+
// Step 2: Generate spawn instructions for each technology
|
|
2230
|
+
// The coordinator will use these to spawn researcher workers via Task()
|
|
2231
|
+
const spawnInstructions: ResearchSpawnInstruction[] = [];
|
|
2232
|
+
|
|
2233
|
+
for (const tech of techStack) {
|
|
2234
|
+
// Generate unique research ID
|
|
2235
|
+
const researchId = `research-${tech}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
2236
|
+
|
|
2237
|
+
// Generate researcher prompt
|
|
2238
|
+
const prompt = formatResearcherPrompt({
|
|
2239
|
+
research_id: researchId,
|
|
2240
|
+
epic_id: "standalone-research", // No epic context for standalone research
|
|
2241
|
+
tech_stack: [tech], // Single tech per researcher
|
|
2242
|
+
project_path: projectPath,
|
|
2243
|
+
check_upgrades: options?.checkUpgrades ?? false,
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
spawnInstructions.push({
|
|
2247
|
+
research_id: researchId,
|
|
2248
|
+
tech,
|
|
2249
|
+
prompt,
|
|
2250
|
+
subagent_type: "swarm/researcher",
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2170
2253
|
|
|
2254
|
+
// Step 3: Return spawn instructions for coordinator
|
|
2255
|
+
// The coordinator will spawn Task() agents using these instructions
|
|
2256
|
+
// and collect results from swarm mail after completion
|
|
2171
2257
|
return {
|
|
2172
2258
|
tech_stack: techStack,
|
|
2173
|
-
|
|
2174
|
-
|
|
2259
|
+
spawn_instructions: spawnInstructions,
|
|
2260
|
+
summaries: {}, // Will be populated by coordinator after researchers complete
|
|
2261
|
+
memory_ids: [], // Will be populated by coordinator after researchers store in semantic-memory
|
|
2175
2262
|
};
|
|
2176
2263
|
}
|
|
2177
2264
|
|
|
@@ -9,8 +9,10 @@ import { describe, expect, test } from "bun:test";
|
|
|
9
9
|
import {
|
|
10
10
|
formatSubtaskPromptV2,
|
|
11
11
|
formatResearcherPrompt,
|
|
12
|
+
formatCoordinatorPrompt,
|
|
12
13
|
SUBTASK_PROMPT_V2,
|
|
13
14
|
RESEARCHER_PROMPT,
|
|
15
|
+
COORDINATOR_PROMPT,
|
|
14
16
|
} from "./swarm-prompts";
|
|
15
17
|
|
|
16
18
|
describe("SUBTASK_PROMPT_V2", () => {
|
|
@@ -818,3 +820,122 @@ describe("swarm_spawn_retry tool", () => {
|
|
|
818
820
|
expect(parsed.prompt).toMatch(/preserve.*working|fix.*while preserving/i);
|
|
819
821
|
});
|
|
820
822
|
});
|
|
823
|
+
|
|
824
|
+
describe("COORDINATOR_PROMPT", () => {
|
|
825
|
+
test("constant exists and is exported", () => {
|
|
826
|
+
expect(COORDINATOR_PROMPT).toBeDefined();
|
|
827
|
+
expect(typeof COORDINATOR_PROMPT).toBe("string");
|
|
828
|
+
expect(COORDINATOR_PROMPT.length).toBeGreaterThan(100);
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("contains all phase headers (0-8)", () => {
|
|
832
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 0:");
|
|
833
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 1:");
|
|
834
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 2:");
|
|
835
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 3:");
|
|
836
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 4:");
|
|
837
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 5:");
|
|
838
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 6:");
|
|
839
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 7:");
|
|
840
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 8:");
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("contains Phase 1.5: Research Phase section", () => {
|
|
844
|
+
expect(COORDINATOR_PROMPT).toContain("Phase 1.5:");
|
|
845
|
+
expect(COORDINATOR_PROMPT).toMatch(/Phase 1\.5:.*Research/i);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("Phase 1.5 documents swarm_spawn_researcher usage", () => {
|
|
849
|
+
// Extract Phase 1.5 section
|
|
850
|
+
const phase15Match = COORDINATOR_PROMPT.match(/Phase 1\.5:[\s\S]*?Phase 2:/);
|
|
851
|
+
expect(phase15Match).not.toBeNull();
|
|
852
|
+
if (!phase15Match) return;
|
|
853
|
+
const phase15Content = phase15Match[0];
|
|
854
|
+
|
|
855
|
+
expect(phase15Content).toContain("swarm_spawn_researcher");
|
|
856
|
+
expect(phase15Content).toContain("Task(subagent_type=\"swarm/researcher\"");
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
test("has section explicitly forbidding direct research tool calls", () => {
|
|
860
|
+
expect(COORDINATOR_PROMPT).toMatch(/NEVER.*direct|forbidden.*tools|do not call directly/i);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
test("forbidden tools section lists all prohibited tools", () => {
|
|
864
|
+
const forbiddenTools = [
|
|
865
|
+
"repo-crawl_",
|
|
866
|
+
"repo-autopsy_",
|
|
867
|
+
"webfetch",
|
|
868
|
+
"fetch_fetch",
|
|
869
|
+
"context7_",
|
|
870
|
+
"pdf-brain_search",
|
|
871
|
+
"pdf-brain_read"
|
|
872
|
+
];
|
|
873
|
+
|
|
874
|
+
for (const tool of forbiddenTools) {
|
|
875
|
+
expect(COORDINATOR_PROMPT).toContain(tool);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test("forbidden tools section explains to use swarm_spawn_researcher instead", () => {
|
|
880
|
+
// Find the forbidden tools section
|
|
881
|
+
const forbiddenMatch = COORDINATOR_PROMPT.match(/(FORBIDDEN.*for coordinators|NEVER.*FETCH.*DIRECTLY)[\s\S]{0,500}swarm_spawn_researcher/i);
|
|
882
|
+
expect(forbiddenMatch).not.toBeNull();
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
test("contains coordinator role boundaries section", () => {
|
|
886
|
+
expect(COORDINATOR_PROMPT).toContain("Coordinator Role Boundaries");
|
|
887
|
+
expect(COORDINATOR_PROMPT).toMatch(/COORDINATORS NEVER.*EXECUTE.*WORK/i);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test("contains MANDATORY review loop section", () => {
|
|
891
|
+
expect(COORDINATOR_PROMPT).toContain("MANDATORY Review Loop");
|
|
892
|
+
expect(COORDINATOR_PROMPT).toContain("swarm_review");
|
|
893
|
+
expect(COORDINATOR_PROMPT).toContain("swarm_review_feedback");
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("Phase 1.5 positioned between Phase 1 (Initialize) and Phase 2 (Knowledge)", () => {
|
|
897
|
+
const phase1Pos = COORDINATOR_PROMPT.indexOf("Phase 1:");
|
|
898
|
+
const phase15Pos = COORDINATOR_PROMPT.indexOf("Phase 1.5:");
|
|
899
|
+
const phase2Pos = COORDINATOR_PROMPT.indexOf("Phase 2:");
|
|
900
|
+
|
|
901
|
+
expect(phase15Pos).toBeGreaterThan(phase1Pos);
|
|
902
|
+
expect(phase15Pos).toBeLessThan(phase2Pos);
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
describe("formatCoordinatorPrompt", () => {
|
|
907
|
+
test("function exists and returns string", () => {
|
|
908
|
+
expect(formatCoordinatorPrompt).toBeDefined();
|
|
909
|
+
const result = formatCoordinatorPrompt({ task: "test task", projectPath: "/test" });
|
|
910
|
+
expect(typeof result).toBe("string");
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("substitutes {task} placeholder", () => {
|
|
914
|
+
const result = formatCoordinatorPrompt({
|
|
915
|
+
task: "Implement auth",
|
|
916
|
+
projectPath: "/test"
|
|
917
|
+
});
|
|
918
|
+
expect(result).toContain("Implement auth");
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test("substitutes {project_path} placeholder", () => {
|
|
922
|
+
const result = formatCoordinatorPrompt({
|
|
923
|
+
task: "test",
|
|
924
|
+
projectPath: "/Users/joel/my-project"
|
|
925
|
+
});
|
|
926
|
+
expect(result).toContain("/Users/joel/my-project");
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
test("returns complete prompt with all phases", () => {
|
|
930
|
+
const result = formatCoordinatorPrompt({
|
|
931
|
+
task: "test",
|
|
932
|
+
projectPath: "/test"
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
// Should contain all phase headers
|
|
936
|
+
for (let i = 0; i <= 8; i++) {
|
|
937
|
+
expect(result).toContain(`Phase ${i}:`);
|
|
938
|
+
}
|
|
939
|
+
expect(result).toContain("Phase 1.5:");
|
|
940
|
+
});
|
|
941
|
+
});
|