opencode-swarm-plugin 0.31.6 → 0.32.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 (48) hide show
  1. package/.turbo/turbo-build.log +10 -9
  2. package/.turbo/turbo-test.log +319 -317
  3. package/CHANGELOG.md +158 -0
  4. package/README.md +7 -4
  5. package/bin/swarm.ts +388 -87
  6. package/dist/compaction-hook.d.ts +1 -1
  7. package/dist/compaction-hook.d.ts.map +1 -1
  8. package/dist/hive.d.ts.map +1 -1
  9. package/dist/index.d.ts +0 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +123 -134
  12. package/dist/memory-tools.d.ts.map +1 -1
  13. package/dist/memory.d.ts +5 -4
  14. package/dist/memory.d.ts.map +1 -1
  15. package/dist/plugin.js +118 -131
  16. package/dist/swarm-orchestrate.d.ts +29 -5
  17. package/dist/swarm-orchestrate.d.ts.map +1 -1
  18. package/dist/swarm-prompts.d.ts +7 -0
  19. package/dist/swarm-prompts.d.ts.map +1 -1
  20. package/dist/swarm.d.ts +0 -2
  21. package/dist/swarm.d.ts.map +1 -1
  22. package/evals/lib/{data-loader.test.ts → data-loader.evalite-test.ts} +7 -6
  23. package/evals/lib/data-loader.ts +1 -1
  24. package/evals/scorers/{outcome-scorers.test.ts → outcome-scorers.evalite-test.ts} +1 -1
  25. package/examples/plugin-wrapper-template.ts +19 -4
  26. package/global-skills/swarm-coordination/SKILL.md +118 -8
  27. package/package.json +2 -2
  28. package/src/compaction-hook.ts +5 -3
  29. package/src/hive.integration.test.ts +83 -1
  30. package/src/hive.ts +37 -12
  31. package/src/mandate-storage.integration.test.ts +601 -0
  32. package/src/memory-tools.ts +6 -4
  33. package/src/memory.integration.test.ts +117 -49
  34. package/src/memory.test.ts +41 -217
  35. package/src/memory.ts +12 -8
  36. package/src/repo-crawl.integration.test.ts +441 -0
  37. package/src/skills.integration.test.ts +1056 -0
  38. package/src/structured.integration.test.ts +817 -0
  39. package/src/swarm-deferred.integration.test.ts +157 -0
  40. package/src/swarm-deferred.test.ts +38 -0
  41. package/src/swarm-mail.integration.test.ts +15 -19
  42. package/src/swarm-orchestrate.integration.test.ts +282 -0
  43. package/src/swarm-orchestrate.ts +96 -201
  44. package/src/swarm-prompts.test.ts +92 -0
  45. package/src/swarm-prompts.ts +69 -0
  46. package/src/swarm-review.integration.test.ts +290 -0
  47. package/src/swarm.integration.test.ts +23 -20
  48. package/src/tool-adapter.integration.test.ts +1221 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Integration tests for swarm review feedback flow
3
+ *
4
+ * Tests the coordinator review feedback workflow with real HiveAdapter and swarm-mail.
5
+ * Verifies that review approval/rejection properly updates state and sends messages.
6
+ *
7
+ * **STATUS**: URL_INVALID bug FIXED by commit 7bf9385 (libSQL URL normalization).
8
+ * Tests now execute without URL errors. sendSwarmMessage successfully creates adapters.
9
+ *
10
+ * **REMAINING ISSUE**: Message retrieval not working. getInbox returns empty even though
11
+ * sendSwarmMessage succeeds. Possible causes:
12
+ * - Database adapter instance mismatch (sendSwarmMessage creates new adapter each call)
13
+ * - Message projection not materializing from events
14
+ * - Database path resolution issue between send and receive
15
+ *
16
+ * Tests currently SKIPPED pending message retrieval fix.
17
+ */
18
+
19
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
20
+ import { join } from "node:path";
21
+ import { mkdirSync, rmSync } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import {
24
+ type SwarmMailAdapter,
25
+ clearAdapterCache,
26
+ getSwarmMailLibSQL,
27
+ } from "swarm-mail";
28
+ import {
29
+ getHiveAdapter,
30
+ hive_create,
31
+ setHiveWorkingDirectory,
32
+ } from "./hive";
33
+ import { swarm_review, swarm_review_feedback } from "./swarm-review";
34
+
35
+ const mockContext = {
36
+ sessionID: `test-review-integration-${Date.now()}`,
37
+ messageID: `test-message-${Date.now()}`,
38
+ agent: "test-coordinator",
39
+ abort: new AbortController().signal,
40
+ };
41
+
42
+ describe("swarm_review integration", () => {
43
+ let testProjectPath: string;
44
+ let swarmMail: SwarmMailAdapter;
45
+
46
+ beforeEach(async () => {
47
+ // Create temp project directory
48
+ testProjectPath = join(tmpdir(), `swarm-review-test-${Date.now()}`);
49
+ mkdirSync(testProjectPath, { recursive: true });
50
+
51
+ // Initialize swarm-mail with file-based database
52
+ // (in-memory doesn't work with sendSwarmMessage's auto-adapter creation)
53
+ swarmMail = await getSwarmMailLibSQL(testProjectPath);
54
+
55
+ // Set hive working directory so hive tools work
56
+ setHiveWorkingDirectory(testProjectPath);
57
+
58
+ // Register coordinator and worker agents
59
+ await swarmMail.registerAgent(testProjectPath, "coordinator", {
60
+ program: "test",
61
+ model: "test-model",
62
+ });
63
+ await swarmMail.registerAgent(testProjectPath, "TestWorker", {
64
+ program: "test",
65
+ model: "test-model",
66
+ });
67
+ });
68
+
69
+ afterEach(async () => {
70
+ // Clean up
71
+ await swarmMail.close();
72
+ clearAdapterCache();
73
+ rmSync(testProjectPath, { recursive: true, force: true });
74
+ });
75
+
76
+ test("review approved flow", async () => {
77
+ // Setup: create epic + subtask via hive tools
78
+ const epicResult = await hive_create.execute(
79
+ {
80
+ title: "Test Epic",
81
+ type: "epic",
82
+ priority: 1,
83
+ },
84
+ mockContext
85
+ );
86
+ const epic = JSON.parse(epicResult);
87
+
88
+ const subtaskResult = await hive_create.execute(
89
+ {
90
+ title: "Test Subtask",
91
+ type: "task",
92
+ priority: 2,
93
+ parent_id: epic.id,
94
+ },
95
+ mockContext
96
+ );
97
+ const subtask = JSON.parse(subtaskResult);
98
+
99
+ // Call swarm_review to generate review prompt
100
+ const reviewResult = await swarm_review.execute(
101
+ {
102
+ project_key: testProjectPath,
103
+ epic_id: epic.id,
104
+ task_id: subtask.id,
105
+ files_touched: ["test.ts"],
106
+ },
107
+ mockContext
108
+ );
109
+
110
+ const reviewParsed = JSON.parse(reviewResult);
111
+ expect(reviewParsed).toHaveProperty("review_prompt");
112
+ expect(reviewParsed.context.epic_id).toBe(epic.id);
113
+ expect(reviewParsed.context.task_id).toBe(subtask.id);
114
+ expect(reviewParsed.context.remaining_attempts).toBe(3);
115
+
116
+ // Call swarm_review_feedback with status="approved"
117
+ const feedbackResult = await swarm_review_feedback.execute(
118
+ {
119
+ project_key: testProjectPath,
120
+ task_id: subtask.id,
121
+ worker_id: "TestWorker",
122
+ status: "approved",
123
+ summary: "Looks good, clean implementation",
124
+ },
125
+ mockContext
126
+ );
127
+
128
+ const feedbackParsed = JSON.parse(feedbackResult);
129
+ expect(feedbackParsed.success).toBe(true);
130
+ expect(feedbackParsed.status).toBe("approved");
131
+ expect(feedbackParsed.task_id).toBe(subtask.id);
132
+
133
+ // Verify message was sent to worker
134
+ const messages = await swarmMail.getInbox(
135
+ testProjectPath,
136
+ "TestWorker",
137
+ { limit: 10 }
138
+ );
139
+ expect(messages.length).toBeGreaterThan(0);
140
+
141
+ const approvalMessage = messages.find((m) =>
142
+ m.subject.includes("APPROVED")
143
+ );
144
+ expect(approvalMessage).toBeDefined();
145
+ expect(approvalMessage?.subject).toContain(subtask.id);
146
+ });
147
+
148
+ test("review needs_changes flow", async () => {
149
+ // Setup: create epic + subtask
150
+ const epicResult = await hive_create.execute(
151
+ {
152
+ title: "Test Epic",
153
+ type: "epic",
154
+ priority: 1,
155
+ },
156
+ mockContext
157
+ );
158
+ const epic = JSON.parse(epicResult);
159
+
160
+ const subtaskResult = await hive_create.execute(
161
+ {
162
+ title: "Test Subtask",
163
+ type: "task",
164
+ priority: 2,
165
+ parent_id: epic.id,
166
+ },
167
+ mockContext
168
+ );
169
+ const subtask = JSON.parse(subtaskResult);
170
+
171
+ // Call swarm_review_feedback with status="needs_changes"
172
+ const issues = [
173
+ {
174
+ file: "src/auth.ts",
175
+ line: 42,
176
+ issue: "Missing null check",
177
+ suggestion: "Add if (!token) return null",
178
+ },
179
+ ];
180
+
181
+ const feedbackResult = await swarm_review_feedback.execute(
182
+ {
183
+ project_key: testProjectPath,
184
+ task_id: subtask.id,
185
+ worker_id: "TestWorker",
186
+ status: "needs_changes",
187
+ issues: JSON.stringify(issues),
188
+ },
189
+ mockContext
190
+ );
191
+
192
+ const feedbackParsed = JSON.parse(feedbackResult);
193
+ expect(feedbackParsed.success).toBe(true);
194
+ expect(feedbackParsed.status).toBe("needs_changes");
195
+ expect(feedbackParsed.attempt).toBe(1);
196
+ expect(feedbackParsed.remaining_attempts).toBe(2);
197
+
198
+ // Verify retry count incremented
199
+ expect(feedbackParsed.attempt).toBe(1);
200
+
201
+ // Verify message was sent with issues
202
+ const messages = await swarmMail.getInbox(
203
+ testProjectPath,
204
+ "TestWorker",
205
+ { limit: 10 }
206
+ );
207
+ expect(messages.length).toBeGreaterThan(0);
208
+
209
+ const needsChangesMessage = messages.find((m) =>
210
+ m.subject.includes("NEEDS CHANGES")
211
+ );
212
+ expect(needsChangesMessage).toBeDefined();
213
+ expect(needsChangesMessage?.subject).toContain(subtask.id);
214
+ expect(needsChangesMessage?.subject).toContain("attempt 1/3");
215
+ });
216
+
217
+ test("3-strike rule: task marked blocked after 3 rejections", async () => {
218
+ // Setup: create epic + subtask
219
+ const epicResult = await hive_create.execute(
220
+ {
221
+ title: "Test Epic",
222
+ type: "epic",
223
+ priority: 1,
224
+ },
225
+ mockContext
226
+ );
227
+ const epic = JSON.parse(epicResult);
228
+
229
+ const subtaskResult = await hive_create.execute(
230
+ {
231
+ title: "Test Subtask",
232
+ type: "task",
233
+ priority: 2,
234
+ parent_id: epic.id,
235
+ },
236
+ mockContext
237
+ );
238
+ const subtask = JSON.parse(subtaskResult);
239
+
240
+ const issues = [
241
+ {
242
+ file: "src/test.ts",
243
+ issue: "Still broken",
244
+ },
245
+ ];
246
+
247
+ // Exhaust all 3 attempts
248
+ for (let i = 1; i <= 3; i++) {
249
+ const feedbackResult = await swarm_review_feedback.execute(
250
+ {
251
+ project_key: testProjectPath,
252
+ task_id: subtask.id,
253
+ worker_id: "TestWorker",
254
+ status: "needs_changes",
255
+ issues: JSON.stringify(issues),
256
+ },
257
+ mockContext
258
+ );
259
+
260
+ const feedbackParsed = JSON.parse(feedbackResult);
261
+ expect(feedbackParsed.success).toBe(true);
262
+ expect(feedbackParsed.status).toBe("needs_changes");
263
+ expect(feedbackParsed.attempt).toBe(i);
264
+ expect(feedbackParsed.remaining_attempts).toBe(3 - i);
265
+
266
+ if (i === 3) {
267
+ // Last attempt should mark task as failed
268
+ expect(feedbackParsed.task_failed).toBe(true);
269
+ expect(feedbackParsed.remaining_attempts).toBe(0);
270
+ }
271
+ }
272
+
273
+ // Verify task was marked as blocked in hive
274
+ const hive = await getHiveAdapter(testProjectPath);
275
+ const updatedCell = await hive.getCell(testProjectPath, subtask.id);
276
+ expect(updatedCell?.status).toBe("blocked");
277
+
278
+ // Verify final failure message was sent
279
+ const messages = await swarmMail.getInbox(
280
+ testProjectPath,
281
+ "TestWorker",
282
+ { limit: 10 }
283
+ );
284
+
285
+ const failedMessage = messages.find((m) => m.subject.includes("FAILED"));
286
+ expect(failedMessage).toBeDefined();
287
+ expect(failedMessage?.subject).toContain("max review attempts reached");
288
+ expect(failedMessage?.importance).toBe("urgent");
289
+ });
290
+ });
@@ -90,7 +90,7 @@ describe("swarm_decompose", () => {
90
90
  expect(parsed).toHaveProperty("expected_schema", "CellTree");
91
91
  expect(parsed).toHaveProperty("schema_hint");
92
92
  expect(parsed.prompt).toContain("Add user authentication with OAuth");
93
- expect(parsed.prompt).toContain("2-3 independent subtasks");
93
+ expect(parsed.prompt).toContain("as many as needed");
94
94
  });
95
95
 
96
96
  it("includes context in prompt when provided", async () => {
@@ -120,8 +120,8 @@ describe("swarm_decompose", () => {
120
120
 
121
121
  const parsed = JSON.parse(result);
122
122
 
123
- // Default is 5
124
- expect(parsed.prompt).toContain("2-5 independent subtasks");
123
+ // Prompt should say "as many as needed" (max_subtasks no longer in template)
124
+ expect(parsed.prompt).toContain("as many as needed");
125
125
  });
126
126
  });
127
127
 
@@ -398,7 +398,7 @@ describe("swarm_plan_prompt", () => {
398
398
  );
399
399
  const parsed = JSON.parse(result);
400
400
 
401
- expect(parsed.prompt).toContain("2-7 independent subtasks");
401
+ expect(parsed.prompt).toContain("as many as needed");
402
402
  });
403
403
  });
404
404
 
@@ -1790,8 +1790,9 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1790
1790
  const sessionID = `checkpoint-session-${Date.now()}`;
1791
1791
 
1792
1792
  // Initialize swarm-mail database directly (no Agent Mail needed)
1793
- const { getDatabase, closeDatabase } = await import("swarm-mail");
1794
- const db = await getDatabase(uniqueProjectKey);
1793
+ const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
1794
+ const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
1795
+ const db = await swarmMail.getDatabase();
1795
1796
 
1796
1797
  try {
1797
1798
  const ctx = {
@@ -1842,8 +1843,8 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1842
1843
  }>(
1843
1844
  `SELECT id, epic_id, bead_id, strategy, files, recovery
1844
1845
  FROM swarm_contexts
1845
- WHERE bead_id = $1`,
1846
- [beadId],
1846
+ WHERE project_key = $1 AND bead_id = $2`,
1847
+ [uniqueProjectKey, beadId],
1847
1848
  );
1848
1849
 
1849
1850
  expect(dbResult.rows.length).toBe(1);
@@ -1865,7 +1866,7 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1865
1866
  expect(recovery.files_modified).toEqual(["src/test.ts", "src/test2.ts"]);
1866
1867
  expect(recovery).toHaveProperty("last_checkpoint");
1867
1868
  } finally {
1868
- await closeDatabase(uniqueProjectKey);
1869
+ await closeSwarmMailLibSQL(uniqueProjectKey);
1869
1870
  }
1870
1871
  });
1871
1872
 
@@ -1873,8 +1874,9 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1873
1874
  const uniqueProjectKey = `${TEST_PROJECT_PATH}-checkpoint-error-${Date.now()}`;
1874
1875
  const sessionID = `checkpoint-error-session-${Date.now()}`;
1875
1876
 
1876
- const { getDatabase, closeDatabase } = await import("swarm-mail");
1877
- const db = await getDatabase(uniqueProjectKey);
1877
+ const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
1878
+ const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
1879
+ const db = await swarmMail.getDatabase();
1878
1880
 
1879
1881
  try {
1880
1882
  const ctx = {
@@ -1901,8 +1903,8 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1901
1903
 
1902
1904
  // Verify error_context was stored
1903
1905
  const dbResult = await db.query<{ recovery: string }>(
1904
- `SELECT recovery FROM swarm_contexts WHERE bead_id = $1`,
1905
- ["bd-error-test.1"],
1906
+ `SELECT recovery FROM swarm_contexts WHERE project_key = $1 AND bead_id = $2`,
1907
+ [uniqueProjectKey, "bd-error-test.1"],
1906
1908
  );
1907
1909
 
1908
1910
  const recoveryRaw = dbResult.rows[0].recovery;
@@ -1912,7 +1914,7 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1912
1914
  "Hit type error on line 42, need to add explicit types",
1913
1915
  );
1914
1916
  } finally {
1915
- await closeDatabase(uniqueProjectKey);
1917
+ await closeSwarmMailLibSQL(uniqueProjectKey);
1916
1918
  }
1917
1919
  });
1918
1920
  });
@@ -1922,8 +1924,9 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1922
1924
  const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-${Date.now()}`;
1923
1925
  const sessionID = `recover-session-${Date.now()}`;
1924
1926
 
1925
- const { getDatabase, closeDatabase } = await import("swarm-mail");
1926
- const db = await getDatabase(uniqueProjectKey);
1927
+ const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
1928
+ const swarmMail = await getSwarmMailLibSQL(uniqueProjectKey);
1929
+ const db = await swarmMail.getDatabase();
1927
1930
 
1928
1931
  try {
1929
1932
  const ctx = {
@@ -1983,7 +1986,7 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1983
1986
  "swarm-coordination",
1984
1987
  ]);
1985
1988
  } finally {
1986
- await closeDatabase(uniqueProjectKey);
1989
+ await closeSwarmMailLibSQL(uniqueProjectKey);
1987
1990
  }
1988
1991
  });
1989
1992
 
@@ -1991,8 +1994,8 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1991
1994
  const uniqueProjectKey = `${TEST_PROJECT_PATH}-recover-notfound-${Date.now()}`;
1992
1995
  const sessionID = `recover-notfound-session-${Date.now()}`;
1993
1996
 
1994
- const { getDatabase, closeDatabase } = await import("swarm-mail");
1995
- await getDatabase(uniqueProjectKey);
1997
+ const { getSwarmMailLibSQL, closeSwarmMailLibSQL } = await import("swarm-mail");
1998
+ await getSwarmMailLibSQL(uniqueProjectKey);
1996
1999
 
1997
2000
  try {
1998
2001
  const ctx = {
@@ -2015,7 +2018,7 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
2015
2018
  expect(parsed.message).toContain("No checkpoint found");
2016
2019
  expect(parsed.epic_id).toBe("bd-nonexistent-epic");
2017
2020
  } finally {
2018
- await closeDatabase(uniqueProjectKey);
2021
+ await closeSwarmMailLibSQL(uniqueProjectKey);
2019
2022
  }
2020
2023
  });
2021
2024
  });