opencode-swarm-plugin 0.32.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 +9 -10
- package/.turbo/turbo-test.log +343 -337
- package/CHANGELOG.md +358 -0
- package/README.md +152 -179
- package/bin/swarm.test.ts +303 -1
- package/bin/swarm.ts +473 -16
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/index.d.ts +112 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12380 -131
- package/dist/logger.d.ts +34 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/observability-tools.d.ts +116 -0
- package/dist/observability-tools.d.ts.map +1 -0
- package/dist/plugin.js +12254 -119
- package/dist/skills.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts +105 -0
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +113 -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 +73 -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/examples/plugin-wrapper-template.ts +297 -8
- package/package.json +6 -2
- package/src/compaction-hook.test.ts +617 -1
- package/src/compaction-hook.ts +291 -18
- package/src/index.ts +54 -1
- package/src/logger.test.ts +189 -0
- package/src/logger.ts +135 -0
- package/src/observability-tools.test.ts +346 -0
- package/src/observability-tools.ts +594 -0
- package/src/skills.integration.test.ts +137 -1
- package/src/skills.test.ts +42 -1
- package/src/skills.ts +8 -4
- package/src/swarm-orchestrate.test.ts +123 -0
- package/src/swarm-orchestrate.ts +183 -0
- package/src/swarm-prompts.test.ts +553 -1
- package/src/swarm-prompts.ts +406 -4
- package/src/swarm-research.integration.test.ts +544 -0
- package/src/swarm-research.test.ts +698 -0
- package/src/swarm-research.ts +472 -0
- package/src/swarm-review.test.ts +177 -0
- package/src/swarm-review.ts +12 -47
- package/src/swarm.ts +6 -3
package/bin/swarm.test.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - rmWithStatus: logs file removal
|
|
9
9
|
*/
|
|
10
10
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
11
|
-
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs";
|
|
11
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync, readFileSync, readdirSync } from "fs";
|
|
12
12
|
import { join } from "path";
|
|
13
13
|
import { tmpdir } from "os";
|
|
14
14
|
|
|
@@ -160,4 +160,306 @@ describe("File operation helpers", () => {
|
|
|
160
160
|
expect(logger.logs.length).toBe(0);
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
describe("getResearcherAgent", () => {
|
|
165
|
+
// Mock implementation for testing - will match actual implementation
|
|
166
|
+
function getResearcherAgent(model: string): string {
|
|
167
|
+
return `---
|
|
168
|
+
name: swarm-researcher
|
|
169
|
+
description: Research agent for discovering and documenting context
|
|
170
|
+
model: ${model}
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
READ-ONLY research agent. Never modifies code - only gathers intel and stores findings.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
test("includes model in frontmatter", () => {
|
|
177
|
+
const template = getResearcherAgent("anthropic/claude-haiku-4-5");
|
|
178
|
+
|
|
179
|
+
expect(template).toContain("model: anthropic/claude-haiku-4-5");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("emphasizes READ-ONLY nature", () => {
|
|
183
|
+
const template = getResearcherAgent("anthropic/claude-haiku-4-5");
|
|
184
|
+
|
|
185
|
+
expect(template).toContain("READ-ONLY");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("includes agent name in frontmatter", () => {
|
|
189
|
+
const template = getResearcherAgent("anthropic/claude-haiku-4-5");
|
|
190
|
+
|
|
191
|
+
expect(template).toContain("name: swarm-researcher");
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Log Command Tests (TDD)
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
describe("Log command helpers", () => {
|
|
201
|
+
let testDir: string;
|
|
202
|
+
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
testDir = join(tmpdir(), `swarm-log-test-${Date.now()}`);
|
|
205
|
+
mkdirSync(testDir, { recursive: true });
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
afterEach(() => {
|
|
209
|
+
if (existsSync(testDir)) {
|
|
210
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("parseLogLine", () => {
|
|
215
|
+
function parseLogLine(line: string): { level: number; time: string; module: string; msg: string } | null {
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(line);
|
|
218
|
+
if (typeof parsed.level === "number" && parsed.time && parsed.msg) {
|
|
219
|
+
return {
|
|
220
|
+
level: parsed.level,
|
|
221
|
+
time: parsed.time,
|
|
222
|
+
module: parsed.module || "unknown",
|
|
223
|
+
msg: parsed.msg,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
// Invalid JSON
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
test("parses valid log line", () => {
|
|
233
|
+
const line = '{"level":30,"time":"2024-12-24T16:00:00.000Z","module":"compaction","msg":"started"}';
|
|
234
|
+
const result = parseLogLine(line);
|
|
235
|
+
|
|
236
|
+
expect(result).not.toBeNull();
|
|
237
|
+
expect(result?.level).toBe(30);
|
|
238
|
+
expect(result?.module).toBe("compaction");
|
|
239
|
+
expect(result?.msg).toBe("started");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("returns null for invalid JSON", () => {
|
|
243
|
+
const line = "not json";
|
|
244
|
+
expect(parseLogLine(line)).toBeNull();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("defaults module to 'unknown' if missing", () => {
|
|
248
|
+
const line = '{"level":30,"time":"2024-12-24T16:00:00.000Z","msg":"test"}';
|
|
249
|
+
const result = parseLogLine(line);
|
|
250
|
+
|
|
251
|
+
expect(result?.module).toBe("unknown");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("filterLogsByLevel", () => {
|
|
256
|
+
function filterLogsByLevel(logs: Array<{ level: number }>, minLevel: number): Array<{ level: number }> {
|
|
257
|
+
return logs.filter((log) => log.level >= minLevel);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
test("filters logs by minimum level", () => {
|
|
261
|
+
const logs = [
|
|
262
|
+
{ level: 10 }, // trace
|
|
263
|
+
{ level: 30 }, // info
|
|
264
|
+
{ level: 50 }, // error
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
const result = filterLogsByLevel(logs, 30);
|
|
268
|
+
expect(result).toHaveLength(2);
|
|
269
|
+
expect(result[0].level).toBe(30);
|
|
270
|
+
expect(result[1].level).toBe(50);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("includes all logs when minLevel is 0", () => {
|
|
274
|
+
const logs = [
|
|
275
|
+
{ level: 10 },
|
|
276
|
+
{ level: 20 },
|
|
277
|
+
{ level: 30 },
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const result = filterLogsByLevel(logs, 0);
|
|
281
|
+
expect(result).toHaveLength(3);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("filterLogsByModule", () => {
|
|
286
|
+
function filterLogsByModule(logs: Array<{ module: string }>, module: string): Array<{ module: string }> {
|
|
287
|
+
return logs.filter((log) => log.module === module);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
test("filters logs by exact module name", () => {
|
|
291
|
+
const logs = [
|
|
292
|
+
{ module: "compaction" },
|
|
293
|
+
{ module: "swarm" },
|
|
294
|
+
{ module: "compaction" },
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
const result = filterLogsByModule(logs, "compaction");
|
|
298
|
+
expect(result).toHaveLength(2);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("returns empty array when no match", () => {
|
|
302
|
+
const logs = [
|
|
303
|
+
{ module: "compaction" },
|
|
304
|
+
];
|
|
305
|
+
|
|
306
|
+
const result = filterLogsByModule(logs, "swarm");
|
|
307
|
+
expect(result).toHaveLength(0);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("filterLogsBySince", () => {
|
|
312
|
+
function parseDuration(duration: string): number | null {
|
|
313
|
+
const match = duration.match(/^(\d+)([smhd])$/);
|
|
314
|
+
if (!match) return null;
|
|
315
|
+
|
|
316
|
+
const [, num, unit] = match;
|
|
317
|
+
const value = parseInt(num, 10);
|
|
318
|
+
|
|
319
|
+
const multipliers: Record<string, number> = {
|
|
320
|
+
s: 1000,
|
|
321
|
+
m: 60 * 1000,
|
|
322
|
+
h: 60 * 60 * 1000,
|
|
323
|
+
d: 24 * 60 * 60 * 1000,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return value * multipliers[unit];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function filterLogsBySince(logs: Array<{ time: string }>, sinceMs: number): Array<{ time: string }> {
|
|
330
|
+
const cutoffTime = Date.now() - sinceMs;
|
|
331
|
+
return logs.filter((log) => new Date(log.time).getTime() >= cutoffTime);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
test("parseDuration handles seconds", () => {
|
|
335
|
+
expect(parseDuration("30s")).toBe(30 * 1000);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("parseDuration handles minutes", () => {
|
|
339
|
+
expect(parseDuration("5m")).toBe(5 * 60 * 1000);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("parseDuration handles hours", () => {
|
|
343
|
+
expect(parseDuration("2h")).toBe(2 * 60 * 60 * 1000);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("parseDuration handles days", () => {
|
|
347
|
+
expect(parseDuration("1d")).toBe(24 * 60 * 60 * 1000);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("parseDuration returns null for invalid format", () => {
|
|
351
|
+
expect(parseDuration("invalid")).toBeNull();
|
|
352
|
+
expect(parseDuration("30x")).toBeNull();
|
|
353
|
+
expect(parseDuration("30")).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("filterLogsBySince filters old logs", () => {
|
|
357
|
+
const now = Date.now();
|
|
358
|
+
const logs = [
|
|
359
|
+
{ time: new Date(now - 10000).toISOString() }, // 10s ago
|
|
360
|
+
{ time: new Date(now - 120000).toISOString() }, // 2m ago
|
|
361
|
+
{ time: new Date(now - 1000).toISOString() }, // 1s ago
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const result = filterLogsBySince(logs, 60000); // Last 1m
|
|
365
|
+
expect(result).toHaveLength(2); // Only logs within last minute
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("formatLogLine", () => {
|
|
370
|
+
function levelToName(level: number): string {
|
|
371
|
+
if (level >= 60) return "FATAL";
|
|
372
|
+
if (level >= 50) return "ERROR";
|
|
373
|
+
if (level >= 40) return "WARN ";
|
|
374
|
+
if (level >= 30) return "INFO ";
|
|
375
|
+
if (level >= 20) return "DEBUG";
|
|
376
|
+
return "TRACE";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function formatLogLine(log: { level: number; time: string; module: string; msg: string }): string {
|
|
380
|
+
const timestamp = new Date(log.time).toLocaleTimeString();
|
|
381
|
+
const levelName = levelToName(log.level);
|
|
382
|
+
const module = log.module.padEnd(12);
|
|
383
|
+
return `${timestamp} ${levelName} ${module} ${log.msg}`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
test("formats log line with timestamp and level", () => {
|
|
387
|
+
const log = {
|
|
388
|
+
level: 30,
|
|
389
|
+
time: "2024-12-24T16:00:00.000Z",
|
|
390
|
+
module: "compaction",
|
|
391
|
+
msg: "started",
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const result = formatLogLine(log);
|
|
395
|
+
expect(result).toContain("INFO");
|
|
396
|
+
expect(result).toContain("compaction");
|
|
397
|
+
expect(result).toContain("started");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("pads module name for alignment", () => {
|
|
401
|
+
const log1 = formatLogLine({ level: 30, time: "2024-12-24T16:00:00.000Z", module: "a", msg: "test" });
|
|
402
|
+
const log2 = formatLogLine({ level: 30, time: "2024-12-24T16:00:00.000Z", module: "compaction", msg: "test" });
|
|
403
|
+
|
|
404
|
+
// Module names should be padded to 12 chars
|
|
405
|
+
expect(log1).toContain("a test"); // 'a' + 11 spaces
|
|
406
|
+
expect(log2).toContain("compaction test"); // 'compaction' + 3 spaces (10 chars + 2)
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("levelToName maps all levels correctly", () => {
|
|
410
|
+
expect(levelToName(10)).toBe("TRACE");
|
|
411
|
+
expect(levelToName(20)).toBe("DEBUG");
|
|
412
|
+
expect(levelToName(30)).toBe("INFO ");
|
|
413
|
+
expect(levelToName(40)).toBe("WARN ");
|
|
414
|
+
expect(levelToName(50)).toBe("ERROR");
|
|
415
|
+
expect(levelToName(60)).toBe("FATAL");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("readLogFiles", () => {
|
|
420
|
+
test("reads multiple .1log files", () => {
|
|
421
|
+
// Create test log files
|
|
422
|
+
const log1 = join(testDir, "swarm.1log");
|
|
423
|
+
const log2 = join(testDir, "swarm.2log");
|
|
424
|
+
const log3 = join(testDir, "compaction.1log");
|
|
425
|
+
|
|
426
|
+
writeFileSync(log1, '{"level":30,"time":"2024-12-24T16:00:00.000Z","msg":"line1"}\n');
|
|
427
|
+
writeFileSync(log2, '{"level":30,"time":"2024-12-24T16:00:01.000Z","msg":"line2"}\n');
|
|
428
|
+
writeFileSync(log3, '{"level":30,"time":"2024-12-24T16:00:02.000Z","module":"compaction","msg":"line3"}\n');
|
|
429
|
+
|
|
430
|
+
function readLogFiles(dir: string): string[] {
|
|
431
|
+
if (!existsSync(dir)) return [];
|
|
432
|
+
|
|
433
|
+
const files = readdirSync(dir)
|
|
434
|
+
.filter((f) => /\.\d+log$/.test(f))
|
|
435
|
+
.sort() // Sort by filename
|
|
436
|
+
.map((f) => join(dir, f));
|
|
437
|
+
|
|
438
|
+
const lines: string[] = [];
|
|
439
|
+
for (const file of files) {
|
|
440
|
+
const content = readFileSync(file, "utf-8");
|
|
441
|
+
lines.push(...content.split("\n").filter((line) => line.trim()));
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return lines;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const lines = readLogFiles(testDir);
|
|
448
|
+
expect(lines).toHaveLength(3);
|
|
449
|
+
// Files are sorted alphabetically: compaction.1log, swarm.1log, swarm.2log
|
|
450
|
+
expect(lines.some((l) => l.includes("line1"))).toBe(true);
|
|
451
|
+
expect(lines.some((l) => l.includes("line2"))).toBe(true);
|
|
452
|
+
expect(lines.some((l) => l.includes("line3"))).toBe(true);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("returns empty array for non-existent directory", () => {
|
|
456
|
+
function readLogFiles(dir: string): string[] {
|
|
457
|
+
if (!existsSync(dir)) return [];
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const lines = readLogFiles(join(testDir, "nonexistent"));
|
|
462
|
+
expect(lines).toHaveLength(0);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
163
465
|
});
|