opencode-swarm-plugin 0.36.0 → 0.36.1

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 (49) hide show
  1. package/.hive/issues.jsonl +4 -4
  2. package/.hive/memories.jsonl +274 -1
  3. package/.turbo/turbo-build.log +4 -4
  4. package/.turbo/turbo-test.log +307 -307
  5. package/CHANGELOG.md +71 -0
  6. package/bin/swarm.ts +234 -179
  7. package/dist/compaction-hook.d.ts +54 -4
  8. package/dist/compaction-hook.d.ts.map +1 -1
  9. package/dist/eval-capture.d.ts +122 -17
  10. package/dist/eval-capture.d.ts.map +1 -1
  11. package/dist/index.d.ts +1 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1278 -619
  14. package/dist/planning-guardrails.d.ts +121 -0
  15. package/dist/planning-guardrails.d.ts.map +1 -1
  16. package/dist/plugin.d.ts +9 -9
  17. package/dist/plugin.d.ts.map +1 -1
  18. package/dist/plugin.js +1283 -329
  19. package/dist/schemas/task.d.ts +0 -1
  20. package/dist/schemas/task.d.ts.map +1 -1
  21. package/dist/swarm-decompose.d.ts +0 -8
  22. package/dist/swarm-decompose.d.ts.map +1 -1
  23. package/dist/swarm-orchestrate.d.ts.map +1 -1
  24. package/dist/swarm-prompts.d.ts +0 -4
  25. package/dist/swarm-prompts.d.ts.map +1 -1
  26. package/dist/swarm-review.d.ts.map +1 -1
  27. package/dist/swarm.d.ts +0 -6
  28. package/dist/swarm.d.ts.map +1 -1
  29. package/evals/README.md +38 -0
  30. package/evals/coordinator-session.eval.ts +154 -0
  31. package/evals/fixtures/coordinator-sessions.ts +328 -0
  32. package/evals/lib/data-loader.ts +69 -0
  33. package/evals/scorers/coordinator-discipline.evalite-test.ts +536 -0
  34. package/evals/scorers/coordinator-discipline.ts +315 -0
  35. package/evals/scorers/index.ts +12 -0
  36. package/examples/plugin-wrapper-template.ts +303 -4
  37. package/package.json +2 -2
  38. package/src/compaction-hook.test.ts +8 -1
  39. package/src/compaction-hook.ts +31 -21
  40. package/src/eval-capture.test.ts +390 -0
  41. package/src/eval-capture.ts +163 -4
  42. package/src/index.ts +68 -1
  43. package/src/planning-guardrails.test.ts +387 -2
  44. package/src/planning-guardrails.ts +289 -0
  45. package/src/plugin.ts +10 -10
  46. package/src/swarm-decompose.ts +20 -0
  47. package/src/swarm-orchestrate.ts +44 -0
  48. package/src/swarm-prompts.ts +20 -0
  49. package/src/swarm-review.ts +41 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "minimatch": "^10.1.1",
42
42
  "pino": "^9.6.0",
43
43
  "pino-roll": "^1.3.0",
44
- "swarm-mail": "1.5.0",
44
+ "swarm-mail": "1.5.1",
45
45
  "yaml": "^2.8.2",
46
46
  "zod": "4.1.8"
47
47
  },
@@ -64,7 +64,14 @@ describe("Compaction Hook", () => {
64
64
  describe("SWARM_COMPACTION_CONTEXT", () => {
65
65
  it("contains coordinator instructions", () => {
66
66
  expect(SWARM_COMPACTION_CONTEXT).toContain("COORDINATOR");
67
- expect(SWARM_COMPACTION_CONTEXT).toContain("Keep Cooking");
67
+ expect(SWARM_COMPACTION_CONTEXT).toContain("You Are The COORDINATOR");
68
+ });
69
+
70
+ it("contains prohibition-first anti-patterns", () => {
71
+ expect(SWARM_COMPACTION_CONTEXT).toContain("NEVER");
72
+ expect(SWARM_COMPACTION_CONTEXT).toContain("edit");
73
+ expect(SWARM_COMPACTION_CONTEXT).toContain("write");
74
+ expect(SWARM_COMPACTION_CONTEXT).toContain("SPAWN A WORKER");
68
75
  });
69
76
 
70
77
  it("contains resume instructions", () => {
@@ -68,11 +68,31 @@ function getLog() {
68
68
  * This is NOT about preserving state for a human - it's about the swarm continuing
69
69
  * autonomously after context compression.
70
70
  */
71
- export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE - Keep Cooking
71
+ export const SWARM_COMPACTION_CONTEXT = `## 🐝 SWARM ACTIVE - You Are The COORDINATOR
72
72
 
73
- You are the **COORDINATOR** of an active swarm. Context was compacted but the swarm is still running.
73
+ Context was compacted but the swarm is still running. You are the **COORDINATOR**.
74
74
 
75
- **YOUR JOB:** Keep orchestrating. Spawn agents. Monitor progress. Unblock work. Ship it.
75
+ ### NEVER DO THESE (Coordinator Anti-Patterns)
76
+
77
+ **CRITICAL: Coordinators NEVER do implementation work. ALWAYS spawn workers.**
78
+
79
+ - ❌ **NEVER** use \`edit\` or \`write\` tools - SPAWN A WORKER
80
+ - ❌ **NEVER** run tests with \`bash\` - SPAWN A WORKER
81
+ - ❌ **NEVER** implement features yourself - SPAWN A WORKER
82
+ - ❌ **NEVER** "just do it myself to save time" - NO. SPAWN A WORKER.
83
+ - ❌ **NEVER** reserve files with \`swarmmail_reserve\` - Workers reserve files
84
+
85
+ **If you catch yourself about to edit a file, STOP. Use \`swarm_spawn_subtask\` instead.**
86
+
87
+ ### ✅ ALWAYS DO THESE (Coordinator Checklist)
88
+
89
+ On resume, execute this checklist IN ORDER:
90
+
91
+ 1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
92
+ 2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
93
+ 3. For completed work: \`swarm_review\` → \`swarm_review_feedback\`
94
+ 4. For open subtasks: \`swarm_spawn_subtask\` (NOT "do it yourself")
95
+ 5. For blocked work: Investigate, unblock, reassign
76
96
 
77
97
  ### Preserve in Summary
78
98
 
@@ -89,41 +109,31 @@ Extract from session context:
89
109
  \`\`\`
90
110
  ## 🐝 Swarm State
91
111
 
92
- **Epic:** <bd-xxx> - <title>
112
+ **Epic:** <cell-xxx> - <title>
93
113
  **Project:** <path>
94
114
  **Progress:** X/Y subtasks complete
95
115
 
96
116
  **Active:**
97
- - <bd-xxx>: <title> [in_progress] → <agent> working on <files>
117
+ - <cell-xxx>: <title> [in_progress] → <agent> working on <files>
98
118
 
99
119
  **Blocked:**
100
- - <bd-xxx>: <title> - BLOCKED: <reason>
120
+ - <cell-xxx>: <title> - BLOCKED: <reason>
101
121
 
102
122
  **Completed:**
103
- - <bd-xxx>: <title> ✓
123
+ - <cell-xxx>: <title> ✓
104
124
 
105
125
  **Ready to Spawn:**
106
- - <bd-xxx>: <title> (files: <...>)
126
+ - <cell-xxx>: <title> (files: <...>)
107
127
  \`\`\`
108
128
 
109
- ### On Resume - IMMEDIATELY
110
-
111
- 1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
112
- 2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
113
- 3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
114
- 4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
115
- 5. **Spawn ready subtasks** - Don't wait, fire them off
116
- 6. **Unblock blocked work** - Resolve dependencies, reassign if needed
117
- 7. **Collect completed work** - Close done subtasks, verify quality
118
-
119
- ### Keep the Swarm Cooking
129
+ ### Your Role
120
130
 
121
131
  - **Spawn aggressively** - If a subtask is ready and unblocked, spawn an agent
122
132
  - **Monitor actively** - Check status, read messages, respond to blockers
133
+ - **Review work** - Use \`swarm_review\` and \`swarm_review_feedback\` for completed work
123
134
  - **Close the loop** - When all subtasks done, verify and close the epic
124
- - **Don't stop** - The swarm runs until the epic is closed
125
135
 
126
- **You are not waiting for instructions. You are the coordinator. Coordinate.**
136
+ **You are the COORDINATOR. You orchestrate. You do NOT implement. Spawn workers.**
127
137
  `;
128
138
 
129
139
  /**
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Tests for eval-capture coordinator event schemas and session capture
3
+ */
4
+ import { type Mock, afterEach, beforeEach, describe, expect, test } from "bun:test";
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ import {
9
+ type CoordinatorEvent,
10
+ CoordinatorEventSchema,
11
+ type CoordinatorSession,
12
+ CoordinatorSessionSchema,
13
+ captureCoordinatorEvent,
14
+ saveSession,
15
+ } from "./eval-capture.js";
16
+
17
+ describe("CoordinatorEvent schemas", () => {
18
+ describe("DECISION events", () => {
19
+ test("validates strategy_selected event", () => {
20
+ const event: CoordinatorEvent = {
21
+ session_id: "test-session",
22
+ epic_id: "bd-123",
23
+ timestamp: new Date().toISOString(),
24
+ event_type: "DECISION",
25
+ decision_type: "strategy_selected",
26
+ payload: {
27
+ strategy: "file-based",
28
+ reasoning: "Files are well isolated",
29
+ },
30
+ };
31
+
32
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
33
+ });
34
+
35
+ test("validates worker_spawned event", () => {
36
+ const event: CoordinatorEvent = {
37
+ session_id: "test-session",
38
+ epic_id: "bd-123",
39
+ timestamp: new Date().toISOString(),
40
+ event_type: "DECISION",
41
+ decision_type: "worker_spawned",
42
+ payload: {
43
+ worker_id: "GreenStorm",
44
+ subtask_id: "bd-123.1",
45
+ files: ["src/test.ts"],
46
+ },
47
+ };
48
+
49
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
50
+ });
51
+
52
+ test("validates review_completed event", () => {
53
+ const event: CoordinatorEvent = {
54
+ session_id: "test-session",
55
+ epic_id: "bd-123",
56
+ timestamp: new Date().toISOString(),
57
+ event_type: "DECISION",
58
+ decision_type: "review_completed",
59
+ payload: {
60
+ subtask_id: "bd-123.1",
61
+ approved: true,
62
+ issues_found: 0,
63
+ },
64
+ };
65
+
66
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
67
+ });
68
+
69
+ test("validates decomposition_complete event", () => {
70
+ const event: CoordinatorEvent = {
71
+ session_id: "test-session",
72
+ epic_id: "bd-123",
73
+ timestamp: new Date().toISOString(),
74
+ event_type: "DECISION",
75
+ decision_type: "decomposition_complete",
76
+ payload: {
77
+ subtask_count: 3,
78
+ strategy: "feature-based",
79
+ },
80
+ };
81
+
82
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
83
+ });
84
+ });
85
+
86
+ describe("VIOLATION events", () => {
87
+ test("validates coordinator_edited_file event", () => {
88
+ const event: CoordinatorEvent = {
89
+ session_id: "test-session",
90
+ epic_id: "bd-123",
91
+ timestamp: new Date().toISOString(),
92
+ event_type: "VIOLATION",
93
+ violation_type: "coordinator_edited_file",
94
+ payload: {
95
+ file: "src/bad.ts",
96
+ operation: "edit",
97
+ },
98
+ };
99
+
100
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
101
+ });
102
+
103
+ test("validates coordinator_ran_tests event", () => {
104
+ const event: CoordinatorEvent = {
105
+ session_id: "test-session",
106
+ epic_id: "bd-123",
107
+ timestamp: new Date().toISOString(),
108
+ event_type: "VIOLATION",
109
+ violation_type: "coordinator_ran_tests",
110
+ payload: {
111
+ command: "bun test",
112
+ },
113
+ };
114
+
115
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
116
+ });
117
+
118
+ test("validates coordinator_reserved_files event", () => {
119
+ const event: CoordinatorEvent = {
120
+ session_id: "test-session",
121
+ epic_id: "bd-123",
122
+ timestamp: new Date().toISOString(),
123
+ event_type: "VIOLATION",
124
+ violation_type: "coordinator_reserved_files",
125
+ payload: {
126
+ files: ["src/auth.ts"],
127
+ },
128
+ };
129
+
130
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
131
+ });
132
+
133
+ test("validates no_worker_spawned event", () => {
134
+ const event: CoordinatorEvent = {
135
+ session_id: "test-session",
136
+ epic_id: "bd-123",
137
+ timestamp: new Date().toISOString(),
138
+ event_type: "VIOLATION",
139
+ violation_type: "no_worker_spawned",
140
+ payload: {
141
+ subtask_id: "bd-123.1",
142
+ reason: "Did work directly",
143
+ },
144
+ };
145
+
146
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
147
+ });
148
+ });
149
+
150
+ describe("OUTCOME events", () => {
151
+ test("validates subtask_success event", () => {
152
+ const event: CoordinatorEvent = {
153
+ session_id: "test-session",
154
+ epic_id: "bd-123",
155
+ timestamp: new Date().toISOString(),
156
+ event_type: "OUTCOME",
157
+ outcome_type: "subtask_success",
158
+ payload: {
159
+ subtask_id: "bd-123.1",
160
+ duration_ms: 45000,
161
+ files_touched: ["src/auth.ts"],
162
+ },
163
+ };
164
+
165
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
166
+ });
167
+
168
+ test("validates subtask_retry event", () => {
169
+ const event: CoordinatorEvent = {
170
+ session_id: "test-session",
171
+ epic_id: "bd-123",
172
+ timestamp: new Date().toISOString(),
173
+ event_type: "OUTCOME",
174
+ outcome_type: "subtask_retry",
175
+ payload: {
176
+ subtask_id: "bd-123.1",
177
+ retry_count: 2,
178
+ reason: "Review rejected",
179
+ },
180
+ };
181
+
182
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
183
+ });
184
+
185
+ test("validates subtask_failed event", () => {
186
+ const event: CoordinatorEvent = {
187
+ session_id: "test-session",
188
+ epic_id: "bd-123",
189
+ timestamp: new Date().toISOString(),
190
+ event_type: "OUTCOME",
191
+ outcome_type: "subtask_failed",
192
+ payload: {
193
+ subtask_id: "bd-123.1",
194
+ error: "Type error in auth.ts",
195
+ },
196
+ };
197
+
198
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
199
+ });
200
+
201
+ test("validates epic_complete event", () => {
202
+ const event: CoordinatorEvent = {
203
+ session_id: "test-session",
204
+ epic_id: "bd-123",
205
+ timestamp: new Date().toISOString(),
206
+ event_type: "OUTCOME",
207
+ outcome_type: "epic_complete",
208
+ payload: {
209
+ success: true,
210
+ total_duration_ms: 180000,
211
+ subtasks_completed: 3,
212
+ },
213
+ };
214
+
215
+ expect(() => CoordinatorEventSchema.parse(event)).not.toThrow();
216
+ });
217
+ });
218
+ });
219
+
220
+ describe("CoordinatorSession schema", () => {
221
+ test("validates complete session", () => {
222
+ const session: CoordinatorSession = {
223
+ session_id: "test-session",
224
+ epic_id: "bd-123",
225
+ start_time: new Date().toISOString(),
226
+ end_time: new Date().toISOString(),
227
+ events: [
228
+ {
229
+ session_id: "test-session",
230
+ epic_id: "bd-123",
231
+ timestamp: new Date().toISOString(),
232
+ event_type: "DECISION",
233
+ decision_type: "strategy_selected",
234
+ payload: { strategy: "file-based" },
235
+ },
236
+ ],
237
+ };
238
+
239
+ expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
240
+ });
241
+
242
+ test("validates session without end_time", () => {
243
+ const session: Partial<CoordinatorSession> = {
244
+ session_id: "test-session",
245
+ epic_id: "bd-123",
246
+ start_time: new Date().toISOString(),
247
+ events: [],
248
+ };
249
+
250
+ expect(() => CoordinatorSessionSchema.parse(session)).not.toThrow();
251
+ });
252
+ });
253
+
254
+ describe("captureCoordinatorEvent", () => {
255
+ let sessionDir: string;
256
+ let sessionId: string;
257
+
258
+ beforeEach(() => {
259
+ sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
260
+ sessionId = `test-${Date.now()}`;
261
+ });
262
+
263
+ afterEach(() => {
264
+ // Clean up test session file
265
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
266
+ if (fs.existsSync(sessionPath)) {
267
+ fs.unlinkSync(sessionPath);
268
+ }
269
+ });
270
+
271
+ test("creates session directory if not exists", () => {
272
+ const event: CoordinatorEvent = {
273
+ session_id: sessionId,
274
+ epic_id: "bd-123",
275
+ timestamp: new Date().toISOString(),
276
+ event_type: "DECISION",
277
+ decision_type: "strategy_selected",
278
+ payload: { strategy: "file-based" },
279
+ };
280
+
281
+ captureCoordinatorEvent(event);
282
+
283
+ expect(fs.existsSync(sessionDir)).toBe(true);
284
+ });
285
+
286
+ test("appends event to session file", () => {
287
+ const event: CoordinatorEvent = {
288
+ session_id: sessionId,
289
+ epic_id: "bd-123",
290
+ timestamp: new Date().toISOString(),
291
+ event_type: "DECISION",
292
+ decision_type: "strategy_selected",
293
+ payload: { strategy: "file-based" },
294
+ };
295
+
296
+ captureCoordinatorEvent(event);
297
+
298
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
299
+ expect(fs.existsSync(sessionPath)).toBe(true);
300
+
301
+ const content = fs.readFileSync(sessionPath, "utf-8");
302
+ const lines = content.trim().split("\n");
303
+ expect(lines).toHaveLength(1);
304
+
305
+ const parsed = JSON.parse(lines[0]);
306
+ expect(parsed.session_id).toBe(sessionId);
307
+ expect(parsed.event_type).toBe("DECISION");
308
+ });
309
+
310
+ test("appends multiple events to same session", () => {
311
+ const event1: CoordinatorEvent = {
312
+ session_id: sessionId,
313
+ epic_id: "bd-123",
314
+ timestamp: new Date().toISOString(),
315
+ event_type: "DECISION",
316
+ decision_type: "strategy_selected",
317
+ payload: { strategy: "file-based" },
318
+ };
319
+
320
+ const event2: CoordinatorEvent = {
321
+ session_id: sessionId,
322
+ epic_id: "bd-123",
323
+ timestamp: new Date().toISOString(),
324
+ event_type: "VIOLATION",
325
+ violation_type: "coordinator_edited_file",
326
+ payload: { file: "src/bad.ts" },
327
+ };
328
+
329
+ captureCoordinatorEvent(event1);
330
+ captureCoordinatorEvent(event2);
331
+
332
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
333
+ const content = fs.readFileSync(sessionPath, "utf-8");
334
+ const lines = content.trim().split("\n");
335
+ expect(lines).toHaveLength(2);
336
+ });
337
+ });
338
+
339
+ describe("saveSession", () => {
340
+ let sessionDir: string;
341
+ let sessionId: string;
342
+
343
+ beforeEach(() => {
344
+ sessionDir = path.join(os.homedir(), ".config", "swarm-tools", "sessions");
345
+ sessionId = `test-${Date.now()}`;
346
+ });
347
+
348
+ afterEach(() => {
349
+ // Clean up test session file
350
+ const sessionPath = path.join(sessionDir, `${sessionId}.jsonl`);
351
+ if (fs.existsSync(sessionPath)) {
352
+ fs.unlinkSync(sessionPath);
353
+ }
354
+ });
355
+
356
+ test("wraps events in session structure", () => {
357
+ // Capture some events
358
+ const event1: CoordinatorEvent = {
359
+ session_id: sessionId,
360
+ epic_id: "bd-123",
361
+ timestamp: new Date().toISOString(),
362
+ event_type: "DECISION",
363
+ decision_type: "strategy_selected",
364
+ payload: { strategy: "file-based" },
365
+ };
366
+
367
+ captureCoordinatorEvent(event1);
368
+
369
+ // Save session
370
+ const session = saveSession({
371
+ session_id: sessionId,
372
+ epic_id: "bd-123",
373
+ });
374
+
375
+ expect(session).toBeDefined();
376
+ expect(session.session_id).toBe(sessionId);
377
+ expect(session.events).toHaveLength(1);
378
+ expect(session.start_time).toBeDefined();
379
+ expect(session.end_time).toBeDefined();
380
+ });
381
+
382
+ test("returns null if session file does not exist", () => {
383
+ const session = saveSession({
384
+ session_id: "nonexistent",
385
+ epic_id: "bd-999",
386
+ });
387
+
388
+ expect(session).toBeNull();
389
+ });
390
+ });