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.
Files changed (55) hide show
  1. package/.hive/issues.jsonl +12 -0
  2. package/.hive/memories.jsonl +255 -1
  3. package/.turbo/turbo-build.log +9 -10
  4. package/.turbo/turbo-test.log +343 -337
  5. package/CHANGELOG.md +358 -0
  6. package/README.md +152 -179
  7. package/bin/swarm.test.ts +303 -1
  8. package/bin/swarm.ts +473 -16
  9. package/dist/compaction-hook.d.ts +1 -1
  10. package/dist/compaction-hook.d.ts.map +1 -1
  11. package/dist/index.d.ts +112 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +12380 -131
  14. package/dist/logger.d.ts +34 -0
  15. package/dist/logger.d.ts.map +1 -0
  16. package/dist/observability-tools.d.ts +116 -0
  17. package/dist/observability-tools.d.ts.map +1 -0
  18. package/dist/plugin.js +12254 -119
  19. package/dist/skills.d.ts.map +1 -1
  20. package/dist/swarm-orchestrate.d.ts +105 -0
  21. package/dist/swarm-orchestrate.d.ts.map +1 -1
  22. package/dist/swarm-prompts.d.ts +113 -2
  23. package/dist/swarm-prompts.d.ts.map +1 -1
  24. package/dist/swarm-research.d.ts +127 -0
  25. package/dist/swarm-research.d.ts.map +1 -0
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +73 -1
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/compaction-resumption.eval.ts +289 -0
  30. package/evals/coordinator-behavior.eval.ts +307 -0
  31. package/evals/fixtures/compaction-cases.ts +350 -0
  32. package/evals/scorers/compaction-scorers.ts +305 -0
  33. package/evals/scorers/index.ts +12 -0
  34. package/examples/plugin-wrapper-template.ts +297 -8
  35. package/package.json +6 -2
  36. package/src/compaction-hook.test.ts +617 -1
  37. package/src/compaction-hook.ts +291 -18
  38. package/src/index.ts +54 -1
  39. package/src/logger.test.ts +189 -0
  40. package/src/logger.ts +135 -0
  41. package/src/observability-tools.test.ts +346 -0
  42. package/src/observability-tools.ts +594 -0
  43. package/src/skills.integration.test.ts +137 -1
  44. package/src/skills.test.ts +42 -1
  45. package/src/skills.ts +8 -4
  46. package/src/swarm-orchestrate.test.ts +123 -0
  47. package/src/swarm-orchestrate.ts +183 -0
  48. package/src/swarm-prompts.test.ts +553 -1
  49. package/src/swarm-prompts.ts +406 -4
  50. package/src/swarm-research.integration.test.ts +544 -0
  51. package/src/swarm-research.test.ts +698 -0
  52. package/src/swarm-research.ts +472 -0
  53. package/src/swarm-review.test.ts +177 -0
  54. package/src/swarm-review.ts +12 -47
  55. 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
  });