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/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();
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Observability Tools Tests
3
+ *
4
+ * TDD: Write tests first, then implement the tools.
5
+ */
6
+
7
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
8
+ import {
9
+ observabilityTools,
10
+ type SwarmAnalyticsArgs,
11
+ type SwarmQueryArgs,
12
+ type SwarmDiagnoseArgs,
13
+ type SwarmInsightsArgs,
14
+ } from "./observability-tools";
15
+ import type { ToolContext } from "@opencode-ai/plugin";
16
+ import {
17
+ closeSwarmMailLibSQL,
18
+ createInMemorySwarmMailLibSQL,
19
+ initSwarmAgent,
20
+ reserveSwarmFiles,
21
+ sendSwarmMessage,
22
+ type SwarmMailAdapter,
23
+ } from "swarm-mail";
24
+
25
+ describe("observability-tools", () => {
26
+ let swarmMail: SwarmMailAdapter;
27
+ const projectPath = "/test/project";
28
+ const mockContext: ToolContext = { sessionID: "test-session" };
29
+
30
+ beforeAll(async () => {
31
+ // Create in-memory database with test data
32
+ swarmMail = await createInMemorySwarmMailLibSQL(projectPath);
33
+
34
+ // Populate with test events using high-level API
35
+ const agentName = "TestAgent";
36
+
37
+ // Register agent
38
+ await initSwarmAgent({
39
+ projectPath,
40
+ agentName,
41
+ taskDescription: "test-task",
42
+ });
43
+
44
+ // Reserve and release files (for lock contention analytics)
45
+ await reserveSwarmFiles({
46
+ projectPath,
47
+ agentName,
48
+ paths: ["src/test.ts"],
49
+ reason: "test-reason",
50
+ });
51
+
52
+ // Send a message (for message latency analytics)
53
+ await sendSwarmMessage({
54
+ projectPath,
55
+ fromAgent: agentName,
56
+ toAgents: ["Agent2"],
57
+ subject: "test-subject",
58
+ body: "test-body",
59
+ });
60
+
61
+ // Note: subtask outcomes are recorded via a different API
62
+ // For now, we'll test with the events we have
63
+ // The important thing is that the tools can execute queries
64
+ });
65
+
66
+ afterAll(async () => {
67
+ await closeSwarmMailLibSQL(projectPath);
68
+ });
69
+
70
+ describe("swarm_analytics", () => {
71
+ const tool = observabilityTools.swarm_analytics;
72
+
73
+ test("is defined with correct schema", () => {
74
+ expect(tool).toBeDefined();
75
+ expect(tool.description).toBeTruthy();
76
+ expect(tool.args).toBeDefined();
77
+ });
78
+
79
+ test("returns failed-decompositions data", async () => {
80
+ const args: SwarmAnalyticsArgs = {
81
+ query: "failed-decompositions",
82
+ };
83
+
84
+ const result = await tool.execute(args, mockContext);
85
+ expect(result).toBeTruthy();
86
+
87
+ const parsed = JSON.parse(result);
88
+ expect(parsed).toHaveProperty("results");
89
+ expect(Array.isArray(parsed.results)).toBe(true);
90
+ // Empty data is fine - we're testing tool execution
91
+ });
92
+
93
+ test("returns strategy-success-rates data", async () => {
94
+ const args: SwarmAnalyticsArgs = {
95
+ query: "strategy-success-rates",
96
+ };
97
+
98
+ const result = await tool.execute(args, mockContext);
99
+ const parsed = JSON.parse(result);
100
+ expect(parsed).toHaveProperty("results");
101
+ expect(Array.isArray(parsed.results)).toBe(true);
102
+ });
103
+
104
+ test("returns agent-activity data", async () => {
105
+ const args: SwarmAnalyticsArgs = {
106
+ query: "agent-activity",
107
+ };
108
+
109
+ const result = await tool.execute(args, mockContext);
110
+ const parsed = JSON.parse(result);
111
+ expect(parsed).toHaveProperty("results");
112
+ expect(Array.isArray(parsed.results)).toBe(true);
113
+ // Should have at least our TestAgent
114
+ expect(parsed.results.length).toBeGreaterThanOrEqual(1);
115
+ });
116
+
117
+ test("supports summary format", async () => {
118
+ const args: SwarmAnalyticsArgs = {
119
+ query: "agent-activity",
120
+ format: "summary",
121
+ };
122
+
123
+ const result = await tool.execute(args, mockContext);
124
+ expect(result).toBeTruthy();
125
+ expect(typeof result).toBe("string");
126
+ // Summary should be concise (<500 chars)
127
+ expect(result.length).toBeLessThan(500);
128
+ });
129
+
130
+ test("supports time filtering with since", async () => {
131
+ const args: SwarmAnalyticsArgs = {
132
+ query: "agent-activity",
133
+ since: "24h",
134
+ };
135
+
136
+ const result = await tool.execute(args, mockContext);
137
+ const parsed = JSON.parse(result);
138
+ expect(parsed).toHaveProperty("results");
139
+ });
140
+
141
+ test("returns error for invalid query type", async () => {
142
+ const args = {
143
+ query: "invalid-query",
144
+ };
145
+
146
+ const result = await tool.execute(args as any, mockContext);
147
+ const parsed = JSON.parse(result);
148
+ expect(parsed).toHaveProperty("error");
149
+ });
150
+ });
151
+
152
+ describe("swarm_query", () => {
153
+ const tool = observabilityTools.swarm_query;
154
+
155
+ test("is defined with correct schema", () => {
156
+ expect(tool).toBeDefined();
157
+ expect(tool.description).toBeTruthy();
158
+ expect(tool.args).toBeDefined();
159
+ });
160
+
161
+ test("executes raw SQL queries", async () => {
162
+ const args: SwarmQueryArgs = {
163
+ sql: "SELECT type, COUNT(*) as count FROM events GROUP BY type",
164
+ };
165
+
166
+ const result = await tool.execute(args, mockContext);
167
+ expect(result).toBeTruthy();
168
+
169
+ const parsed = JSON.parse(result);
170
+ // May have errors in test environment - that's ok
171
+ if (!parsed.error) {
172
+ // Should have count and results even if empty
173
+ expect(parsed).toHaveProperty("count");
174
+ expect(parsed).toHaveProperty("results");
175
+ expect(Array.isArray(parsed.results)).toBe(true);
176
+ }
177
+ });
178
+
179
+ test("limits results to max 50 rows", async () => {
180
+ const args: SwarmQueryArgs = {
181
+ sql: "SELECT * FROM events LIMIT 100", // Try to fetch 100
182
+ };
183
+
184
+ const result = await tool.execute(args, mockContext);
185
+ const parsed = JSON.parse(result);
186
+ // Should be capped at 50 (or less if there's less data)
187
+ // May return error if database issues - that's ok for this test
188
+ if (parsed.error) {
189
+ expect(parsed).toHaveProperty("error");
190
+ } else {
191
+ expect(parsed).toHaveProperty("results");
192
+ expect(parsed.results.length).toBeLessThanOrEqual(50);
193
+ }
194
+ });
195
+
196
+ test("supports table format", async () => {
197
+ const args: SwarmQueryArgs = {
198
+ sql: "SELECT type FROM events LIMIT 3",
199
+ format: "table",
200
+ };
201
+
202
+ const result = await tool.execute(args, mockContext);
203
+ expect(typeof result).toBe("string");
204
+ // Table format returns string (even if "No results" for empty data)
205
+ expect(result.length).toBeGreaterThan(0);
206
+ });
207
+
208
+ test("returns error for invalid SQL", async () => {
209
+ const args: SwarmQueryArgs = {
210
+ sql: "SELECT * FROM nonexistent_table",
211
+ };
212
+
213
+ const result = await tool.execute(args, mockContext);
214
+ const parsed = JSON.parse(result);
215
+ expect(parsed).toHaveProperty("error");
216
+ });
217
+ });
218
+
219
+ describe("swarm_diagnose", () => {
220
+ const tool = observabilityTools.swarm_diagnose;
221
+
222
+ test("is defined with correct schema", () => {
223
+ expect(tool).toBeDefined();
224
+ expect(tool.description).toBeTruthy();
225
+ expect(tool.args).toBeDefined();
226
+ });
227
+
228
+ test("diagnoses issues for a specific epic", async () => {
229
+ const args: SwarmDiagnoseArgs = {
230
+ epic_id: "epic-123",
231
+ include: ["blockers", "errors"],
232
+ };
233
+
234
+ const result = await tool.execute(args, mockContext);
235
+ expect(result).toBeTruthy();
236
+
237
+ const parsed = JSON.parse(result);
238
+ expect(parsed).toHaveProperty("epic_id");
239
+ expect(parsed).toHaveProperty("diagnosis");
240
+ });
241
+
242
+ test("returns structured diagnosis with suggestions", async () => {
243
+ const args: SwarmDiagnoseArgs = {
244
+ bead_id: "task-1",
245
+ };
246
+
247
+ const result = await tool.execute(args, mockContext);
248
+ const parsed = JSON.parse(result);
249
+ expect(parsed).toHaveProperty("diagnosis");
250
+ expect(Array.isArray(parsed.diagnosis)).toBe(true);
251
+ });
252
+
253
+ test("includes timeline when requested", async () => {
254
+ const args: SwarmDiagnoseArgs = {
255
+ bead_id: "task-1",
256
+ include: ["timeline"],
257
+ };
258
+
259
+ const result = await tool.execute(args, mockContext);
260
+ const parsed = JSON.parse(result);
261
+ expect(parsed).toHaveProperty("timeline");
262
+ });
263
+ });
264
+
265
+ describe("swarm_insights", () => {
266
+ const tool = observabilityTools.swarm_insights;
267
+
268
+ test("is defined with correct schema", () => {
269
+ expect(tool).toBeDefined();
270
+ expect(tool.description).toBeTruthy();
271
+ expect(tool.args).toBeDefined();
272
+ });
273
+
274
+ test("generates insights for recent activity", async () => {
275
+ const args: SwarmInsightsArgs = {
276
+ scope: "recent",
277
+ metrics: ["success_rate", "avg_duration"],
278
+ };
279
+
280
+ const result = await tool.execute(args, mockContext);
281
+ expect(result).toBeTruthy();
282
+
283
+ const parsed = JSON.parse(result);
284
+ expect(parsed).toHaveProperty("insights");
285
+ expect(Array.isArray(parsed.insights)).toBe(true);
286
+ });
287
+
288
+ test("generates insights for specific epic", async () => {
289
+ const args: SwarmInsightsArgs = {
290
+ scope: "epic",
291
+ epic_id: "epic-123",
292
+ metrics: ["conflict_rate", "retry_rate"],
293
+ };
294
+
295
+ const result = await tool.execute(args, mockContext);
296
+ const parsed = JSON.parse(result);
297
+ expect(parsed).toHaveProperty("epic_id", "epic-123");
298
+ expect(parsed).toHaveProperty("insights");
299
+ });
300
+
301
+ test("returns error when epic_id missing for epic scope", async () => {
302
+ const args: SwarmInsightsArgs = {
303
+ scope: "epic",
304
+ metrics: ["success_rate"],
305
+ // Missing epic_id
306
+ };
307
+
308
+ const result = await tool.execute(args, mockContext);
309
+ const parsed = JSON.parse(result);
310
+ expect(parsed).toHaveProperty("error");
311
+ });
312
+ });
313
+
314
+ describe("integration with swarm-mail analytics", () => {
315
+ test("all query types are supported", async () => {
316
+ const queryTypes = [
317
+ "failed-decompositions",
318
+ "strategy-success-rates",
319
+ "lock-contention",
320
+ "agent-activity",
321
+ "message-latency",
322
+ "scope-violations",
323
+ "task-duration",
324
+ "checkpoint-frequency",
325
+ "recovery-success",
326
+ "human-feedback",
327
+ ];
328
+
329
+ for (const queryType of queryTypes) {
330
+ const tool = observabilityTools.swarm_analytics;
331
+ const args: SwarmAnalyticsArgs = {
332
+ query: queryType as SwarmAnalyticsArgs["query"],
333
+ };
334
+
335
+ const result = await tool.execute(args, mockContext);
336
+ const parsed = JSON.parse(result);
337
+
338
+ // Should return results property (even if empty array)
339
+ // May have errors in test environment - that's ok
340
+ if (!parsed.error) {
341
+ expect(parsed).toHaveProperty("results");
342
+ }
343
+ }
344
+ });
345
+ });
346
+ });