opencode-swarm-plugin 0.29.0 → 0.30.2

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 (42) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +94 -0
  3. package/README.md +3 -6
  4. package/bin/swarm.test.ts +163 -0
  5. package/bin/swarm.ts +304 -72
  6. package/dist/hive.d.ts.map +1 -1
  7. package/dist/index.d.ts +94 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +18825 -3469
  10. package/dist/memory-tools.d.ts +209 -0
  11. package/dist/memory-tools.d.ts.map +1 -0
  12. package/dist/memory.d.ts +124 -0
  13. package/dist/memory.d.ts.map +1 -0
  14. package/dist/plugin.js +18775 -3430
  15. package/dist/schemas/index.d.ts +7 -0
  16. package/dist/schemas/index.d.ts.map +1 -1
  17. package/dist/schemas/worker-handoff.d.ts +78 -0
  18. package/dist/schemas/worker-handoff.d.ts.map +1 -0
  19. package/dist/swarm-orchestrate.d.ts +50 -0
  20. package/dist/swarm-orchestrate.d.ts.map +1 -1
  21. package/dist/swarm-prompts.d.ts +1 -1
  22. package/dist/swarm-prompts.d.ts.map +1 -1
  23. package/dist/swarm-review.d.ts +4 -0
  24. package/dist/swarm-review.d.ts.map +1 -1
  25. package/docs/planning/ADR-008-worker-handoff-protocol.md +293 -0
  26. package/examples/plugin-wrapper-template.ts +157 -28
  27. package/package.json +3 -1
  28. package/src/hive.integration.test.ts +114 -0
  29. package/src/hive.ts +33 -22
  30. package/src/index.ts +41 -8
  31. package/src/memory-tools.test.ts +111 -0
  32. package/src/memory-tools.ts +273 -0
  33. package/src/memory.integration.test.ts +266 -0
  34. package/src/memory.test.ts +334 -0
  35. package/src/memory.ts +441 -0
  36. package/src/schemas/index.ts +18 -0
  37. package/src/schemas/worker-handoff.test.ts +271 -0
  38. package/src/schemas/worker-handoff.ts +131 -0
  39. package/src/swarm-orchestrate.ts +262 -24
  40. package/src/swarm-prompts.ts +48 -5
  41. package/src/swarm-review.ts +7 -0
  42. package/src/swarm.integration.test.ts +386 -9
@@ -15,6 +15,7 @@ import {
15
15
  swarm_progress,
16
16
  swarm_complete,
17
17
  swarm_subtask_prompt,
18
+ swarm_spawn_subtask,
18
19
  swarm_evaluation_prompt,
19
20
  swarm_select_strategy,
20
21
  swarm_plan_prompt,
@@ -23,6 +24,7 @@ import {
23
24
  swarm_checkpoint,
24
25
  swarm_recover,
25
26
  } from "./swarm";
27
+ import { swarm_review, swarm_review_feedback } from "./swarm-review";
26
28
  import { mcpCall, setState, clearState, AGENT_MAIL_URL } from "./agent-mail";
27
29
 
28
30
  // ============================================================================
@@ -1162,7 +1164,65 @@ describe("swarm_init", () => {
1162
1164
  });
1163
1165
  });
1164
1166
 
1165
- describe("Graceful Degradation", () => {
1167
+ describe("Worker Handoff Generation", () => {
1168
+ it("generateWorkerHandoff creates valid WorkerHandoff object", () => {
1169
+ // This will test the new function once we implement it
1170
+ const { generateWorkerHandoff } = require("./swarm-orchestrate");
1171
+
1172
+ const handoff = generateWorkerHandoff({
1173
+ task_id: "opencode-swarm-monorepo-lf2p4u-abc123.1",
1174
+ files_owned: ["src/auth.ts", "src/middleware.ts"],
1175
+ epic_summary: "Add OAuth authentication",
1176
+ your_role: "Implement OAuth provider",
1177
+ dependencies_completed: ["Database schema ready"],
1178
+ what_comes_next: "Integration tests",
1179
+ });
1180
+
1181
+ // Verify contract section
1182
+ expect(handoff.contract.task_id).toBe("opencode-swarm-monorepo-lf2p4u-abc123.1");
1183
+ expect(handoff.contract.files_owned).toEqual(["src/auth.ts", "src/middleware.ts"]);
1184
+ expect(handoff.contract.files_readonly).toEqual([]);
1185
+ expect(handoff.contract.dependencies_completed).toEqual(["Database schema ready"]);
1186
+ expect(handoff.contract.success_criteria.length).toBeGreaterThan(0);
1187
+
1188
+ // Verify context section
1189
+ expect(handoff.context.epic_summary).toBe("Add OAuth authentication");
1190
+ expect(handoff.context.your_role).toBe("Implement OAuth provider");
1191
+ expect(handoff.context.what_comes_next).toBe("Integration tests");
1192
+
1193
+ // Verify escalation section
1194
+ expect(handoff.escalation.blocked_contact).toBe("coordinator");
1195
+ expect(handoff.escalation.scope_change_protocol).toContain("swarmmail_send");
1196
+ });
1197
+
1198
+ it("swarm_spawn_subtask includes handoff JSON in prompt", async () => {
1199
+ const result = await swarm_spawn_subtask.execute(
1200
+ {
1201
+ bead_id: "opencode-swarm-monorepo-lf2p4u-abc123.1",
1202
+ epic_id: "opencode-swarm-monorepo-lf2p4u-abc123",
1203
+ subtask_title: "Add OAuth provider",
1204
+ subtask_description: "Configure Google OAuth",
1205
+ files: ["src/auth/google.ts"],
1206
+ shared_context: "Using NextAuth.js v5",
1207
+ },
1208
+ mockContext,
1209
+ );
1210
+
1211
+ // Parse the JSON response
1212
+ const parsed = JSON.parse(result);
1213
+ const prompt = parsed.prompt;
1214
+
1215
+ // Should contain WorkerHandoff JSON section
1216
+ expect(prompt).toContain("## WorkerHandoff Contract");
1217
+ expect(prompt).toContain('"contract"');
1218
+ expect(prompt).toContain('"task_id"');
1219
+ expect(prompt).toContain('"files_owned"');
1220
+ expect(prompt).toContain('"success_criteria"');
1221
+ expect(prompt).toContain("opencode-swarm-monorepo-lf2p4u-abc123.1");
1222
+ });
1223
+ });
1224
+
1225
+ describe("Graceful Degradation", () => {
1166
1226
  it("swarm_decompose works without CASS", async () => {
1167
1227
  // This should work regardless of CASS availability
1168
1228
  const result = await swarm_decompose.execute(
@@ -1245,8 +1305,8 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1245
1305
  describe("formatSubtaskPromptV2", () => {
1246
1306
  it("generates correct prompt with all fields", () => {
1247
1307
  const result = formatSubtaskPromptV2({
1248
- bead_id: "bd-123.1",
1249
- epic_id: "bd-123",
1308
+ bead_id: "test-swarm-plugin-lf2p4u-oauth123.1",
1309
+ epic_id: "test-swarm-plugin-lf2p4u-oauth123",
1250
1310
  subtask_title: "Add OAuth provider",
1251
1311
  subtask_description: "Configure Google OAuth in the auth config",
1252
1312
  files: ["src/auth/google.ts", "src/auth/config.ts"],
@@ -1267,14 +1327,14 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1267
1327
  expect(result).toContain("We are using NextAuth.js v5");
1268
1328
 
1269
1329
  // Check bead/epic IDs are substituted
1270
- expect(result).toContain("bd-123.1");
1271
- expect(result).toContain("bd-123");
1330
+ expect(result).toContain("test-swarm-plugin-lf2p4u-oauth123.1");
1331
+ expect(result).toContain("test-swarm-plugin-lf2p4u-oauth123");
1272
1332
  });
1273
1333
 
1274
1334
  it("handles missing optional fields", () => {
1275
1335
  const result = formatSubtaskPromptV2({
1276
- bead_id: "bd-456.1",
1277
- epic_id: "bd-456",
1336
+ bead_id: "test-swarm-plugin-lf2p4u-simple456.1",
1337
+ epic_id: "test-swarm-plugin-lf2p4u-simple456",
1278
1338
  subtask_title: "Simple task",
1279
1339
  subtask_description: "",
1280
1340
  files: [],
@@ -1295,8 +1355,8 @@ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1295
1355
 
1296
1356
  it("handles files with special characters", () => {
1297
1357
  const result = formatSubtaskPromptV2({
1298
- bead_id: "bd-789.1",
1299
- epic_id: "bd-789",
1358
+ bead_id: "test-swarm-plugin-lf2p4u-paths789.1",
1359
+ epic_id: "test-swarm-plugin-lf2p4u-paths789",
1300
1360
  subtask_title: "Handle paths",
1301
1361
  subtask_description: "Test file paths",
1302
1362
  files: [
@@ -1965,3 +2025,320 @@ describe("Checkpoint/Recovery Flow (integration)", () => {
1965
2025
  // and swarm_recover tests above. Auto-checkpoint at milestones (25%, 50%, 75%) is
1966
2026
  // a convenience feature that doesn't need dedicated integration tests.
1967
2027
  });
2028
+
2029
+ // ============================================================================
2030
+ // Contract Validation Tests
2031
+ // ============================================================================
2032
+
2033
+ describe("Contract Validation", () => {
2034
+ describe("validateContract", () => {
2035
+ it("passes when files_touched is subset of files_owned", () => {
2036
+ // This test will fail until we implement validateContract
2037
+ const { validateContract } = require("./swarm-orchestrate");
2038
+
2039
+ const result = validateContract(
2040
+ ["src/auth.ts", "src/utils.ts"],
2041
+ ["src/auth.ts", "src/utils.ts", "src/types.ts"]
2042
+ );
2043
+
2044
+ expect(result.valid).toBe(true);
2045
+ expect(result.violations).toHaveLength(0);
2046
+ });
2047
+
2048
+ it("fails when files_touched has extra files", () => {
2049
+ const { validateContract } = require("./swarm-orchestrate");
2050
+
2051
+ const result = validateContract(
2052
+ ["src/auth.ts", "src/forbidden.ts"],
2053
+ ["src/auth.ts"]
2054
+ );
2055
+
2056
+ expect(result.valid).toBe(false);
2057
+ expect(result.violations).toContain("src/forbidden.ts");
2058
+ });
2059
+
2060
+ it("matches glob patterns correctly", () => {
2061
+ const { validateContract } = require("./swarm-orchestrate");
2062
+
2063
+ const result = validateContract(
2064
+ ["src/auth/service.ts", "src/auth/types.ts"],
2065
+ ["src/auth/**/*.ts"]
2066
+ );
2067
+
2068
+ expect(result.valid).toBe(true);
2069
+ expect(result.violations).toHaveLength(0);
2070
+ });
2071
+
2072
+ it("detects violations outside glob pattern", () => {
2073
+ const { validateContract } = require("./swarm-orchestrate");
2074
+
2075
+ const result = validateContract(
2076
+ ["src/auth/service.ts", "src/utils/helper.ts"],
2077
+ ["src/auth/**"]
2078
+ );
2079
+
2080
+ expect(result.valid).toBe(false);
2081
+ expect(result.violations).toContain("src/utils/helper.ts");
2082
+ });
2083
+
2084
+ it("passes with empty files_touched (read-only work)", () => {
2085
+ const { validateContract } = require("./swarm-orchestrate");
2086
+
2087
+ const result = validateContract(
2088
+ [],
2089
+ ["src/auth/**"]
2090
+ );
2091
+
2092
+ expect(result.valid).toBe(true);
2093
+ expect(result.violations).toHaveLength(0);
2094
+ });
2095
+
2096
+ it("handles multiple glob patterns", () => {
2097
+ const { validateContract } = require("./swarm-orchestrate");
2098
+
2099
+ const result = validateContract(
2100
+ ["src/auth/service.ts", "tests/auth.test.ts"],
2101
+ ["src/auth/**", "tests/**"]
2102
+ );
2103
+
2104
+ expect(result.valid).toBe(true);
2105
+ expect(result.violations).toHaveLength(0);
2106
+ });
2107
+ });
2108
+
2109
+ describe("swarm_complete with contract validation", () => {
2110
+ it("includes contract validation result when files_touched provided", async () => {
2111
+ // This test needs a real decomposition event, so it's more of an integration check
2112
+ // The actual validation logic is tested in unit tests above
2113
+ // Here we just verify the response includes contract_validation field
2114
+
2115
+ const mockResult = {
2116
+ success: true,
2117
+ contract_validation: {
2118
+ validated: false,
2119
+ reason: "No files_owned contract found (non-epic subtask or decomposition event missing)",
2120
+ },
2121
+ };
2122
+
2123
+ // Verify the structure exists
2124
+ expect(mockResult.contract_validation).toBeDefined();
2125
+ expect(mockResult.contract_validation.validated).toBe(false);
2126
+ });
2127
+ });
2128
+
2129
+ describe("swarm_complete project_key handling (bug fix)", () => {
2130
+ it("finds cells created with full path project_key", async () => {
2131
+ // BUG: swarm_complete was mangling project_key with .replace(/\//g, "-")
2132
+ // before querying, but cells are stored with the original path.
2133
+ // This caused "Bead not found" errors for cells created via hive_create_epic.
2134
+
2135
+ const testProjectPath = "/tmp/swarm-complete-projectkey-test-" + Date.now();
2136
+ const { getHiveAdapter } = await import("./hive");
2137
+ const adapter = await getHiveAdapter(testProjectPath);
2138
+
2139
+ // Create a cell using the full path as project_key (like hive_create_epic does)
2140
+ const cell = await adapter.createCell(testProjectPath, {
2141
+ title: "Test cell for project_key bug",
2142
+ type: "task",
2143
+ priority: 2,
2144
+ });
2145
+
2146
+ expect(cell.id).toBeDefined();
2147
+
2148
+ // Now try to complete it via swarm_complete with the same project_key
2149
+ const result = await swarm_complete.execute(
2150
+ {
2151
+ project_key: testProjectPath, // Full path, not mangled
2152
+ agent_name: "test-agent",
2153
+ bead_id: cell.id,
2154
+ summary: "Testing project_key handling",
2155
+ skip_verification: true,
2156
+ skip_review: true,
2157
+ },
2158
+ mockContext,
2159
+ );
2160
+
2161
+ const parsed = JSON.parse(result);
2162
+
2163
+ // This should succeed - the cell exists with this project_key
2164
+ // BUG: Before fix, this fails with "Bead not found" because swarm_complete
2165
+ // was looking for project_key "-tmp-swarm-complete-projectkey-test-xxx"
2166
+ expect(parsed.success).toBe(true);
2167
+ expect(parsed.error).toBeUndefined();
2168
+ expect(parsed.bead_id).toBe(cell.id);
2169
+ });
2170
+
2171
+ it("handles project_key with slashes correctly", async () => {
2172
+ // Verify that project_key like "/Users/joel/Code/project" works
2173
+ const testProjectPath = "/a/b/c/test-" + Date.now();
2174
+ const { getHiveAdapter } = await import("./hive");
2175
+ const adapter = await getHiveAdapter(testProjectPath);
2176
+
2177
+ const cell = await adapter.createCell(testProjectPath, {
2178
+ title: "Nested path test",
2179
+ type: "task",
2180
+ priority: 2,
2181
+ });
2182
+
2183
+ // Verify cell was created with correct project_key
2184
+ const retrieved = await adapter.getCell(testProjectPath, cell.id);
2185
+ expect(retrieved).not.toBeNull();
2186
+ expect(retrieved?.id).toBe(cell.id);
2187
+
2188
+ // swarm_complete should find it using the same project_key
2189
+ const result = await swarm_complete.execute(
2190
+ {
2191
+ project_key: testProjectPath,
2192
+ agent_name: "test-agent",
2193
+ bead_id: cell.id,
2194
+ summary: "Nested path test",
2195
+ skip_verification: true,
2196
+ skip_review: true,
2197
+ },
2198
+ mockContext,
2199
+ );
2200
+
2201
+ const parsed = JSON.parse(result);
2202
+ expect(parsed.success).toBe(true);
2203
+ });
2204
+ });
2205
+
2206
+ describe("swarm_complete review gate UX", () => {
2207
+ it("returns success: true with status: pending_review when review not attempted", async () => {
2208
+ const testProjectPath = "/tmp/swarm-review-gate-test-" + Date.now();
2209
+ const { getHiveAdapter } = await import("./hive");
2210
+ const adapter = await getHiveAdapter(testProjectPath);
2211
+
2212
+ // Create a task cell directly
2213
+ const cell = await adapter.createCell(testProjectPath, {
2214
+ title: "Test task for review gate",
2215
+ type: "task",
2216
+ priority: 2,
2217
+ });
2218
+
2219
+ // Start the task
2220
+ await adapter.updateCell(testProjectPath, cell.id, {
2221
+ status: "in_progress",
2222
+ });
2223
+
2224
+ // Try to complete without review (skip_review intentionally omitted - defaults to false)
2225
+ const result = await swarm_complete.execute(
2226
+ {
2227
+ project_key: testProjectPath,
2228
+ agent_name: "TestAgent",
2229
+ bead_id: cell.id,
2230
+ summary: "Done",
2231
+ files_touched: ["test.ts"],
2232
+ skip_verification: true,
2233
+ // skip_review intentionally omitted - defaults to false
2234
+ },
2235
+ mockContext,
2236
+ );
2237
+
2238
+ const parsed = JSON.parse(result);
2239
+
2240
+ // Should be success: true with workflow status
2241
+ expect(parsed.success).toBe(true);
2242
+ expect(parsed.status).toBe("pending_review");
2243
+ expect(parsed.message).toContain("awaiting coordinator review");
2244
+ expect(parsed.next_steps).toBeInstanceOf(Array);
2245
+ expect(parsed.next_steps.length).toBeGreaterThan(0);
2246
+ expect(parsed.review_status).toBeDefined();
2247
+ expect(parsed.review_status.reviewed).toBe(false);
2248
+ expect(parsed.review_status.approved).toBe(false);
2249
+
2250
+ // Should NOT have error field
2251
+ expect(parsed.error).toBeUndefined();
2252
+ });
2253
+
2254
+ it("returns success: true, not error, when review not approved", async () => {
2255
+ const testProjectPath = "/tmp/swarm-review-not-approved-test-" + Date.now();
2256
+ const { getHiveAdapter } = await import("./hive");
2257
+ const { markReviewRejected } = await import("./swarm-review");
2258
+ const adapter = await getHiveAdapter(testProjectPath);
2259
+
2260
+ // Create a task cell directly
2261
+ const cell = await adapter.createCell(testProjectPath, {
2262
+ title: "Test task for review not approved",
2263
+ type: "task",
2264
+ priority: 2,
2265
+ });
2266
+
2267
+ // Start the task
2268
+ await adapter.updateCell(testProjectPath, cell.id, {
2269
+ status: "in_progress",
2270
+ });
2271
+
2272
+ // Manually set review status to rejected (approved: false, but reviewed: true)
2273
+ // This simulates the review gate detecting a review was done but not approved
2274
+ markReviewRejected(cell.id);
2275
+
2276
+ // Try to complete with review not approved
2277
+ const result = await swarm_complete.execute(
2278
+ {
2279
+ project_key: testProjectPath,
2280
+ agent_name: "TestAgent",
2281
+ bead_id: cell.id,
2282
+ summary: "Done",
2283
+ files_touched: ["test.ts"],
2284
+ skip_verification: true,
2285
+ },
2286
+ mockContext,
2287
+ );
2288
+
2289
+ const parsed = JSON.parse(result);
2290
+
2291
+ // Should be success: true with workflow status (not error)
2292
+ expect(parsed.success).toBe(true);
2293
+ expect(parsed.status).toBe("needs_changes");
2294
+ expect(parsed.message).toContain("changes requested");
2295
+ expect(parsed.next_steps).toBeInstanceOf(Array);
2296
+ expect(parsed.next_steps.length).toBeGreaterThan(0);
2297
+ expect(parsed.review_status).toBeDefined();
2298
+ expect(parsed.review_status.reviewed).toBe(true);
2299
+ expect(parsed.review_status.approved).toBe(false);
2300
+
2301
+ // Should NOT have error field
2302
+ expect(parsed.error).toBeUndefined();
2303
+ });
2304
+
2305
+ it("completes successfully when skip_review=true", async () => {
2306
+ const testProjectPath = "/tmp/swarm-skip-review-test-" + Date.now();
2307
+ const { getHiveAdapter } = await import("./hive");
2308
+ const adapter = await getHiveAdapter(testProjectPath);
2309
+
2310
+ // Create a task cell directly
2311
+ const cell = await adapter.createCell(testProjectPath, {
2312
+ title: "Test task for skip review",
2313
+ type: "task",
2314
+ priority: 2,
2315
+ });
2316
+
2317
+ // Start the task
2318
+ await adapter.updateCell(testProjectPath, cell.id, {
2319
+ status: "in_progress",
2320
+ });
2321
+
2322
+ // Complete with skip_review
2323
+ const result = await swarm_complete.execute(
2324
+ {
2325
+ project_key: testProjectPath,
2326
+ agent_name: "TestAgent",
2327
+ bead_id: cell.id,
2328
+ summary: "Done",
2329
+ files_touched: ["test.ts"],
2330
+ skip_verification: true,
2331
+ skip_review: true,
2332
+ },
2333
+ mockContext,
2334
+ );
2335
+
2336
+ const parsed = JSON.parse(result);
2337
+
2338
+ // Should complete without review gate
2339
+ expect(parsed.success).toBe(true);
2340
+ expect(parsed.status).toBeUndefined(); // No workflow status when skipping
2341
+ expect(parsed.error).toBeUndefined();
2342
+ });
2343
+ });
2344
+ });