opencode-swarm-plugin 0.31.7 → 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 +134 -0
  4. package/README.md +7 -4
  5. package/bin/swarm.ts +388 -128
  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
@@ -11,6 +11,7 @@
11
11
  * - OPENCODE_SESSION_ID: Passed to CLI for session state persistence
12
12
  * - OPENCODE_MESSAGE_ID: Passed to CLI for context
13
13
  * - OPENCODE_AGENT: Passed to CLI for context
14
+ * - SWARM_PROJECT_DIR: Project directory (critical for database path)
14
15
  */
15
16
  import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
16
17
  import { tool } from "@opencode-ai/plugin";
@@ -18,6 +19,10 @@ import { spawn } from "child_process";
18
19
 
19
20
  const SWARM_CLI = "swarm";
20
21
 
22
+ // Module-level project directory - set during plugin initialization
23
+ // This is CRITICAL: without it, the CLI uses process.cwd() which may be wrong
24
+ let projectDirectory: string = process.cwd();
25
+
21
26
  // =============================================================================
22
27
  // CLI Execution Helper
23
28
  // =============================================================================
@@ -27,6 +32,8 @@ const SWARM_CLI = "swarm";
27
32
  *
28
33
  * Spawns `swarm tool <name> --json '<args>'` and returns the result.
29
34
  * Passes session context via environment variables.
35
+ *
36
+ * IMPORTANT: Runs in projectDirectory (set by OpenCode) not process.cwd()
30
37
  */
31
38
  async function execTool(
32
39
  name: string,
@@ -40,12 +47,14 @@ async function execTool(
40
47
  : ["tool", name];
41
48
 
42
49
  const proc = spawn(SWARM_CLI, cliArgs, {
50
+ cwd: projectDirectory, // Run in project directory, not plugin directory
43
51
  stdio: ["ignore", "pipe", "pipe"],
44
52
  env: {
45
53
  ...process.env,
46
54
  OPENCODE_SESSION_ID: ctx.sessionID,
47
55
  OPENCODE_MESSAGE_ID: ctx.messageID,
48
56
  OPENCODE_AGENT: ctx.agent,
57
+ SWARM_PROJECT_DIR: projectDirectory, // Also pass as env var
49
58
  },
50
59
  });
51
60
 
@@ -1058,9 +1067,11 @@ Extract from session context:
1058
1067
 
1059
1068
  1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
1060
1069
  2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
1061
- 3. **Spawn ready subtasks** - Don't wait, fire them off
1062
- 4. **Unblock blocked work** - Resolve dependencies, reassign if needed
1063
- 5. **Collect completed work** - Close done subtasks, verify quality
1070
+ 3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
1071
+ 4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
1072
+ 5. **Spawn ready subtasks** - Don't wait, fire them off
1073
+ 6. **Unblock blocked work** - Resolve dependencies, reassign if needed
1074
+ 7. **Collect completed work** - Close done subtasks, verify quality
1064
1075
 
1065
1076
  ### Keep the Swarm Cooking
1066
1077
 
@@ -1122,8 +1133,12 @@ type ExtendedHooks = Hooks & {
1122
1133
  };
1123
1134
 
1124
1135
  export const SwarmPlugin: Plugin = async (
1125
- _input: PluginInput,
1136
+ input: PluginInput,
1126
1137
  ): Promise<ExtendedHooks> => {
1138
+ // CRITICAL: Set project directory from OpenCode input
1139
+ // Without this, CLI uses wrong database path
1140
+ projectDirectory = input.directory;
1141
+
1127
1142
  return {
1128
1143
  tool: {
1129
1144
  // Beads
@@ -13,6 +13,8 @@ tools:
13
13
  - swarm_complete
14
14
  - swarm_status
15
15
  - swarm_progress
16
+ - swarm_review
17
+ - swarm_review_feedback
16
18
  - hive_create_epic
17
19
  - hive_query
18
20
  - swarmmail_init
@@ -442,19 +444,120 @@ for (const subtask of subtasks) {
442
444
  }
443
445
  ```
444
446
 
445
- ### Phase 6: Monitor & Intervene
447
+ ### Phase 6: MANDATORY Review Loop (NON-NEGOTIABLE)
446
448
 
447
- ```typescript
448
- // Check progress
449
- const status = await swarm_status({ epic_id, project_key });
449
+ **⚠️ AFTER EVERY Worker Returns, You MUST Complete This Checklist:**
450
450
 
451
- // Check for messages from workers
452
- const inbox = await swarmmail_inbox({ limit: 5 });
451
+ This is the **quality gate** that prevents shipping broken code. DO NOT skip this.
453
452
 
454
- // Read specific message if needed
453
+ ```typescript
454
+ // ============================================================
455
+ // Step 1: Check Swarm Mail (Worker may have sent messages)
456
+ // ============================================================
457
+ const inbox = await swarmmail_inbox({ limit: 5 });
455
458
  const message = await swarmmail_read_message({ message_id: N });
456
459
 
457
- // Intervene if needed (see Intervention Patterns)
460
+ // ============================================================
461
+ // Step 2: Review the Work (Generate review prompt with diff)
462
+ // ============================================================
463
+ const reviewPrompt = await swarm_review({
464
+ project_key: "/abs/path/to/project",
465
+ epic_id: "epic-id",
466
+ task_id: "subtask-id",
467
+ files_touched: ["src/auth/service.ts", "src/auth/service.test.ts"]
468
+ });
469
+
470
+ // This generates a review prompt that includes:
471
+ // - Epic context (what we're trying to achieve)
472
+ // - Subtask requirements
473
+ // - Git diff of changes
474
+ // - Dependency status (what came before, what comes next)
475
+
476
+ // ============================================================
477
+ // Step 3: Evaluate Against Criteria
478
+ // ============================================================
479
+ // Ask yourself:
480
+ // - Does the work fulfill the subtask requirements?
481
+ // - Does it serve the overall epic goal?
482
+ // - Does it enable downstream tasks?
483
+ // - Type safety, no obvious bugs?
484
+
485
+ // ============================================================
486
+ // Step 4: Send Feedback (Approve or Request Changes)
487
+ // ============================================================
488
+ await swarm_review_feedback({
489
+ project_key: "/abs/path/to/project",
490
+ task_id: "subtask-id",
491
+ worker_id: "WorkerName",
492
+ status: "approved", // or "needs_changes"
493
+ summary: "LGTM - auth service looks solid",
494
+ issues: "[]" // or "[{file, line, issue, suggestion}]"
495
+ });
496
+
497
+ // ============================================================
498
+ // Step 5: ONLY THEN Continue
499
+ // ============================================================
500
+ // If approved:
501
+ // - Close the cell
502
+ // - Spawn next worker (if dependencies allow)
503
+ // - Update swarm status
504
+ //
505
+ // If needs_changes:
506
+ // - Worker gets feedback
507
+ // - Worker retries (max 3 attempts)
508
+ // - Review again when worker re-submits
509
+ //
510
+ // If 3 failures:
511
+ // - Mark task blocked
512
+ // - Escalate to human (architectural problem, not "try harder")
513
+ ```
514
+
515
+ **❌ Anti-Pattern (Skipping Review):**
516
+
517
+ ```typescript
518
+ // Worker completes
519
+ swarm_complete({ ... });
520
+
521
+ // Coordinator immediately spawns next worker
522
+ // ⚠️ WRONG - No quality gate!
523
+ Task({ subagent_type: "swarm/worker", prompt: nextWorkerPrompt });
524
+ ```
525
+
526
+ **✅ Correct Pattern (Review Before Proceeding):**
527
+
528
+ ```typescript
529
+ // Worker completes
530
+ swarm_complete({ ... });
531
+
532
+ // Coordinator REVIEWS first
533
+ swarm_review({ ... });
534
+ // ... evaluates changes ...
535
+ swarm_review_feedback({ status: "approved" });
536
+
537
+ // ONLY THEN spawn next worker
538
+ Task({ subagent_type: "swarm/worker", prompt: nextWorkerPrompt });
539
+ ```
540
+
541
+ **Review Workflow (3-Strike Rule):**
542
+
543
+ 1. Worker calls `swarm_complete` → Coordinator notified
544
+ 2. Coordinator runs `swarm_review` → Gets diff + epic context
545
+ 3. Coordinator evaluates against epic goals
546
+ 4. If good: `swarm_review_feedback(status="approved")` → Task closed
547
+ 5. If issues: `swarm_review_feedback(status="needs_changes", issues=[...])` → Worker fixes
548
+ 6. After 3 rejections → Task marked blocked (architectural problem, not "try harder")
549
+
550
+ **Review Criteria:**
551
+ - Does work fulfill subtask requirements?
552
+ - Does it serve the overall epic goal?
553
+ - Does it enable downstream tasks?
554
+ - Type safety, no obvious bugs?
555
+
556
+ **Monitoring & Intervention:**
557
+
558
+ ```typescript
559
+ // Check overall swarm status
560
+ const status = await swarm_status({ epic_id, project_key });
458
561
  ```
459
562
 
460
563
  ### Phase 7: Aggregate & Complete
@@ -778,6 +881,13 @@ One blocker affects multiple subtasks.
778
881
  | `swarmmail_ack` | Acknowledge message |
779
882
  | `swarmmail_health` | Check database health |
780
883
 
884
+ ## Swarm Review Quick Reference
885
+
886
+ | Tool | Purpose |
887
+ | ------------------------ | ------------------------------------------ |
888
+ | `swarm_review` | Generate review prompt with epic context + diff |
889
+ | `swarm_review_feedback` | Send approval/rejection to worker (3-strike rule) |
890
+
781
891
  ## Full Swarm Flow
782
892
 
783
893
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.31.7",
3
+ "version": "0.32.0",
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",
@@ -39,7 +39,7 @@
39
39
  "gray-matter": "^4.0.3",
40
40
  "ioredis": "^5.4.1",
41
41
  "minimatch": "^10.1.1",
42
- "swarm-mail": "1.2.2",
42
+ "swarm-mail": "1.3.0",
43
43
  "zod": "4.1.8"
44
44
  },
45
45
  "devDependencies": {
@@ -88,9 +88,11 @@ Extract from session context:
88
88
 
89
89
  1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
90
90
  2. \`swarmmail_inbox(limit=5)\` - Check for agent messages
91
- 3. **Spawn ready subtasks** - Don't wait, fire them off
92
- 4. **Unblock blocked work** - Resolve dependencies, reassign if needed
93
- 5. **Collect completed work** - Close done subtasks, verify quality
91
+ 3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
92
+ 4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
93
+ 5. **Spawn ready subtasks** - Don't wait, fire them off
94
+ 6. **Unblock blocked work** - Resolve dependencies, reassign if needed
95
+ 7. **Collect completed work** - Close done subtasks, verify quality
94
96
 
95
97
  ### Keep the Swarm Cooking
96
98
 
@@ -7,6 +7,8 @@
7
7
  * Run with: bun test src/hive.integration.test.ts
8
8
  */
9
9
  import { describe, it, expect, beforeAll, beforeEach, afterAll } from "vitest";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
10
12
  import {
11
13
  hive_create,
12
14
  hive_create_epic,
@@ -56,7 +58,7 @@ const createdBeadIds: string[] = [];
56
58
  /**
57
59
  * Test project key - use temp directory to isolate tests
58
60
  */
59
- const TEST_PROJECT_KEY = `/tmp/beads-integration-test-${Date.now()}`;
61
+ const TEST_PROJECT_KEY = join(tmpdir(), `beads-integration-test-${Date.now()}`);
60
62
 
61
63
  /**
62
64
  * Adapter instance for verification
@@ -1353,6 +1355,86 @@ describe("beads integration", () => {
1353
1355
  });
1354
1356
 
1355
1357
  describe("hive_sync", () => {
1358
+ it("succeeds with unstaged changes outside .hive/ (stash-before-pull)", async () => {
1359
+ const { mkdirSync, rmSync, writeFileSync, existsSync } = await import("node:fs");
1360
+ const { join } = await import("node:path");
1361
+ const { tmpdir } = await import("node:os");
1362
+ const { execSync } = await import("node:child_process");
1363
+
1364
+ // Create a temp git repository with a remote (to trigger pull)
1365
+ const tempProject = join(tmpdir(), `hive-sync-stash-test-${Date.now()}`);
1366
+ const remoteProject = join(tmpdir(), `hive-sync-remote-${Date.now()}`);
1367
+
1368
+ // Create "remote" bare repo
1369
+ mkdirSync(remoteProject, { recursive: true });
1370
+ execSync("git init --bare", { cwd: remoteProject });
1371
+
1372
+ // Create local repo
1373
+ mkdirSync(tempProject, { recursive: true });
1374
+ execSync("git init", { cwd: tempProject });
1375
+ execSync('git config user.email "test@example.com"', { cwd: tempProject });
1376
+ execSync('git config user.name "Test User"', { cwd: tempProject });
1377
+ execSync(`git remote add origin ${remoteProject}`, { cwd: tempProject });
1378
+
1379
+ // Create .hive directory and a source file
1380
+ const hiveDir = join(tempProject, ".hive");
1381
+ mkdirSync(hiveDir, { recursive: true });
1382
+ writeFileSync(join(hiveDir, "issues.jsonl"), "");
1383
+ writeFileSync(join(tempProject, "src.ts"), "// initial");
1384
+
1385
+ // Initial commit and push
1386
+ execSync("git add .", { cwd: tempProject });
1387
+ execSync('git commit -m "initial commit"', { cwd: tempProject });
1388
+ execSync("git push -u origin main", { cwd: tempProject });
1389
+
1390
+ // Now create unstaged changes OUTSIDE .hive/
1391
+ writeFileSync(join(tempProject, "src.ts"), "// modified but not staged");
1392
+
1393
+ // Set working directory for hive commands
1394
+ const originalDir = getHiveWorkingDirectory();
1395
+ setHiveWorkingDirectory(tempProject);
1396
+
1397
+ try {
1398
+ // Create a cell (this will mark it dirty and flush will write to JSONL)
1399
+ await hive_create.execute(
1400
+ { title: "Stash test cell", type: "task" },
1401
+ mockContext,
1402
+ );
1403
+
1404
+ // Sync WITH auto_pull=true (this is where the bug manifests)
1405
+ // Before fix: fails with "cannot pull with rebase: You have unstaged changes"
1406
+ // After fix: stashes, pulls, pops, succeeds
1407
+ const result = await hive_sync.execute(
1408
+ { auto_pull: true },
1409
+ mockContext,
1410
+ );
1411
+
1412
+ // Should succeed
1413
+ expect(result).toContain("successfully");
1414
+
1415
+ // Verify .hive changes were committed
1416
+ const hiveStatus = execSync("git status --porcelain .hive/", {
1417
+ cwd: tempProject,
1418
+ encoding: "utf-8",
1419
+ });
1420
+ expect(hiveStatus.trim()).toBe("");
1421
+
1422
+ // Verify unstaged changes are still there (stash was popped)
1423
+ const srcStatus = execSync("git status --porcelain src.ts", {
1424
+ cwd: tempProject,
1425
+ encoding: "utf-8",
1426
+ });
1427
+ expect(srcStatus.trim()).toContain("M src.ts");
1428
+ } finally {
1429
+ // Restore original working directory
1430
+ setHiveWorkingDirectory(originalDir);
1431
+
1432
+ // Cleanup
1433
+ rmSync(tempProject, { recursive: true, force: true });
1434
+ rmSync(remoteProject, { recursive: true, force: true });
1435
+ }
1436
+ });
1437
+
1356
1438
  it("commits .hive changes before pulling (regression test for unstaged changes error)", async () => {
1357
1439
  const { mkdirSync, rmSync, writeFileSync, existsSync } = await import("node:fs");
1358
1440
  const { join } = await import("node:path");
package/src/hive.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  syncMemories,
23
23
  type HiveAdapter,
24
24
  type Cell as AdapterCell,
25
- getSwarmMail,
25
+ getSwarmMailLibSQL,
26
26
  resolvePartialId,
27
27
  } from "swarm-mail";
28
28
  import { existsSync, readFileSync } from "node:fs";
@@ -508,7 +508,7 @@ export async function getHiveAdapter(projectKey: string): Promise<HiveAdapter> {
508
508
  return adapterCache.get(projectKey)!;
509
509
  }
510
510
 
511
- const swarmMail = await getSwarmMail(projectKey);
511
+ const swarmMail = await getSwarmMailLibSQL(projectKey);
512
512
  const db = await swarmMail.getDatabase();
513
513
  const adapter = createHiveAdapter(db, projectKey);
514
514
 
@@ -1158,7 +1158,7 @@ export const hive_sync = tool({
1158
1158
  );
1159
1159
 
1160
1160
  // 2b. Sync memories to JSONL
1161
- const swarmMail = await getSwarmMail(projectKey);
1161
+ const swarmMail = await getSwarmMailLibSQL(projectKey);
1162
1162
  const db = await swarmMail.getDatabase();
1163
1163
  const hivePath = join(projectKey, ".hive");
1164
1164
  let memoriesSynced = 0;
@@ -1217,18 +1217,43 @@ export const hive_sync = tool({
1217
1217
  const hasRemote = remoteCheckResult.stdout.trim() !== "";
1218
1218
 
1219
1219
  if (hasRemote) {
1220
- const pullResult = await withTimeout(
1221
- runGitCommand(["pull", "--rebase"]),
1222
- TIMEOUT_MS,
1223
- "git pull --rebase",
1224
- );
1220
+ // Check for unstaged changes that would block pull --rebase
1221
+ const statusResult = await runGitCommand(["status", "--porcelain"]);
1222
+ const hasUnstagedChanges = statusResult.stdout.trim() !== "";
1223
+ let didStash = false;
1224
+
1225
+ if (hasUnstagedChanges) {
1226
+ // Stash all changes (including untracked) before pull
1227
+ const stashResult = await runGitCommand(["stash", "push", "-u", "-m", "hive_sync: auto-stash before pull"]);
1228
+ if (stashResult.exitCode === 0) {
1229
+ didStash = true;
1230
+ }
1231
+ // If stash fails (e.g., nothing to stash), continue anyway
1232
+ }
1225
1233
 
1226
- if (pullResult.exitCode !== 0) {
1227
- throw new HiveError(
1228
- `Failed to pull: ${pullResult.stderr}`,
1234
+ try {
1235
+ const pullResult = await withTimeout(
1236
+ runGitCommand(["pull", "--rebase"]),
1237
+ TIMEOUT_MS,
1229
1238
  "git pull --rebase",
1230
- pullResult.exitCode,
1231
1239
  );
1240
+
1241
+ if (pullResult.exitCode !== 0) {
1242
+ throw new HiveError(
1243
+ `Failed to pull: ${pullResult.stderr}`,
1244
+ "git pull --rebase",
1245
+ pullResult.exitCode,
1246
+ );
1247
+ }
1248
+ } finally {
1249
+ // Pop stash if we stashed
1250
+ if (didStash) {
1251
+ const popResult = await runGitCommand(["stash", "pop"]);
1252
+ if (popResult.exitCode !== 0) {
1253
+ // Stash pop failed - likely a conflict. Log warning but don't fail sync.
1254
+ console.warn(`[hive_sync] Warning: stash pop failed. Your changes are in 'git stash list'. Error: ${popResult.stderr}`);
1255
+ }
1256
+ }
1232
1257
  }
1233
1258
  }
1234
1259
  }