opencode-swarm-plugin 0.31.7 → 0.33.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 (62) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/.turbo/turbo-test.log +324 -316
  3. package/CHANGELOG.md +394 -0
  4. package/README.md +129 -181
  5. package/bin/swarm.test.ts +31 -0
  6. package/bin/swarm.ts +635 -140
  7. package/dist/compaction-hook.d.ts +1 -1
  8. package/dist/compaction-hook.d.ts.map +1 -1
  9. package/dist/hive.d.ts.map +1 -1
  10. package/dist/index.d.ts +17 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +653 -139
  13. package/dist/memory-tools.d.ts.map +1 -1
  14. package/dist/memory.d.ts +5 -4
  15. package/dist/memory.d.ts.map +1 -1
  16. package/dist/observability-tools.d.ts +116 -0
  17. package/dist/observability-tools.d.ts.map +1 -0
  18. package/dist/plugin.js +648 -136
  19. package/dist/skills.d.ts.map +1 -1
  20. package/dist/swarm-orchestrate.d.ts +29 -5
  21. package/dist/swarm-orchestrate.d.ts.map +1 -1
  22. package/dist/swarm-prompts.d.ts +66 -0
  23. package/dist/swarm-prompts.d.ts.map +1 -1
  24. package/dist/swarm.d.ts +17 -2
  25. package/dist/swarm.d.ts.map +1 -1
  26. package/evals/lib/{data-loader.test.ts → data-loader.evalite-test.ts} +7 -6
  27. package/evals/lib/data-loader.ts +1 -1
  28. package/evals/scorers/{outcome-scorers.test.ts → outcome-scorers.evalite-test.ts} +1 -1
  29. package/examples/plugin-wrapper-template.ts +316 -12
  30. package/global-skills/swarm-coordination/SKILL.md +118 -8
  31. package/package.json +3 -2
  32. package/src/compaction-hook.ts +5 -3
  33. package/src/hive.integration.test.ts +83 -1
  34. package/src/hive.ts +37 -12
  35. package/src/index.ts +25 -1
  36. package/src/mandate-storage.integration.test.ts +601 -0
  37. package/src/memory-tools.ts +6 -4
  38. package/src/memory.integration.test.ts +117 -49
  39. package/src/memory.test.ts +41 -217
  40. package/src/memory.ts +12 -8
  41. package/src/observability-tools.test.ts +346 -0
  42. package/src/observability-tools.ts +594 -0
  43. package/src/repo-crawl.integration.test.ts +441 -0
  44. package/src/skills.integration.test.ts +1192 -0
  45. package/src/skills.test.ts +42 -1
  46. package/src/skills.ts +8 -4
  47. package/src/structured.integration.test.ts +817 -0
  48. package/src/swarm-deferred.integration.test.ts +157 -0
  49. package/src/swarm-deferred.test.ts +38 -0
  50. package/src/swarm-mail.integration.test.ts +15 -19
  51. package/src/swarm-orchestrate.integration.test.ts +282 -0
  52. package/src/swarm-orchestrate.test.ts +123 -0
  53. package/src/swarm-orchestrate.ts +279 -201
  54. package/src/swarm-prompts.test.ts +481 -0
  55. package/src/swarm-prompts.ts +297 -0
  56. package/src/swarm-research.integration.test.ts +544 -0
  57. package/src/swarm-research.test.ts +698 -0
  58. package/src/swarm-research.ts +472 -0
  59. package/src/swarm-review.integration.test.ts +290 -0
  60. package/src/swarm.integration.test.ts +23 -20
  61. package/src/swarm.ts +6 -3
  62. 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
 
@@ -896,6 +905,252 @@ interface SwarmDetection {
896
905
  reasons: string[];
897
906
  }
898
907
 
908
+ /**
909
+ * Structured state snapshot for LLM-powered compaction
910
+ *
911
+ * This is passed to the lite model to generate a continuation prompt
912
+ * with concrete data instead of just instructions.
913
+ */
914
+ interface SwarmStateSnapshot {
915
+ sessionID: string;
916
+ detection: {
917
+ confidence: "high" | "medium" | "low" | "none";
918
+ reasons: string[];
919
+ };
920
+ epic?: {
921
+ id: string;
922
+ title: string;
923
+ status: string;
924
+ subtasks: Array<{
925
+ id: string;
926
+ title: string;
927
+ status: "open" | "in_progress" | "blocked" | "closed";
928
+ files: string[];
929
+ assignedTo?: string;
930
+ }>;
931
+ };
932
+ messages: Array<{
933
+ from: string;
934
+ to: string[];
935
+ subject: string;
936
+ body: string;
937
+ timestamp: number;
938
+ importance?: string;
939
+ }>;
940
+ reservations: Array<{
941
+ agent: string;
942
+ paths: string[];
943
+ exclusive: boolean;
944
+ expiresAt: number;
945
+ }>;
946
+ }
947
+
948
+ /**
949
+ * Query actual swarm state using spawn (like detectSwarm does)
950
+ *
951
+ * Returns structured snapshot of current state for LLM compaction.
952
+ * Shells out to swarm CLI to get real data.
953
+ */
954
+ async function querySwarmState(sessionID: string): Promise<SwarmStateSnapshot> {
955
+ try {
956
+ // Query cells via swarm CLI
957
+ const cellsResult = await new Promise<{ exitCode: number; stdout: string }>(
958
+ (resolve) => {
959
+ const proc = spawn(SWARM_CLI, ["tool", "hive_query"], {
960
+ cwd: projectDirectory,
961
+ stdio: ["ignore", "pipe", "pipe"],
962
+ });
963
+ let stdout = "";
964
+ proc.stdout.on("data", (d) => {
965
+ stdout += d;
966
+ });
967
+ proc.on("close", (exitCode) =>
968
+ resolve({ exitCode: exitCode ?? 1, stdout }),
969
+ );
970
+ },
971
+ );
972
+
973
+ const cells =
974
+ cellsResult.exitCode === 0 ? JSON.parse(cellsResult.stdout) : [];
975
+
976
+ // Find active epic (first unclosed epic with subtasks)
977
+ const openEpics = cells.filter(
978
+ (c: { type?: string; status: string }) =>
979
+ c.type === "epic" && c.status !== "closed",
980
+ );
981
+ const epic = openEpics[0];
982
+
983
+ // Get subtasks if we have an epic
984
+ const subtasks =
985
+ epic && epic.id
986
+ ? cells.filter(
987
+ (c: { parent_id?: string }) => c.parent_id === epic.id,
988
+ )
989
+ : [];
990
+
991
+ // TODO: Query swarm mail for messages and reservations
992
+ // For MVP, use empty arrays - the fallback chain handles this
993
+ const messages: SwarmStateSnapshot["messages"] = [];
994
+ const reservations: SwarmStateSnapshot["reservations"] = [];
995
+
996
+ // Run detection for confidence
997
+ const detection = await detectSwarm();
998
+
999
+ return {
1000
+ sessionID,
1001
+ detection: {
1002
+ confidence: detection.confidence,
1003
+ reasons: detection.reasons,
1004
+ },
1005
+ epic: epic
1006
+ ? {
1007
+ id: epic.id,
1008
+ title: epic.title,
1009
+ status: epic.status,
1010
+ subtasks: subtasks.map((s: {
1011
+ id: string;
1012
+ title: string;
1013
+ status: string;
1014
+ files?: string[];
1015
+ }) => ({
1016
+ id: s.id,
1017
+ title: s.title,
1018
+ status: s.status as "open" | "in_progress" | "blocked" | "closed",
1019
+ files: s.files || [],
1020
+ })),
1021
+ }
1022
+ : undefined,
1023
+ messages,
1024
+ reservations,
1025
+ };
1026
+ } catch (err) {
1027
+ // If query fails, return minimal snapshot
1028
+ const detection = await detectSwarm();
1029
+ return {
1030
+ sessionID,
1031
+ detection: {
1032
+ confidence: detection.confidence,
1033
+ reasons: detection.reasons,
1034
+ },
1035
+ messages: [],
1036
+ reservations: [],
1037
+ };
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Generate compaction prompt using LLM
1043
+ *
1044
+ * Shells out to `opencode run -m <liteModel>` with structured state.
1045
+ * Returns markdown continuation prompt or null on failure.
1046
+ *
1047
+ * Timeout: 30 seconds
1048
+ */
1049
+ async function generateCompactionPrompt(
1050
+ snapshot: SwarmStateSnapshot,
1051
+ ): Promise<string | null> {
1052
+ try {
1053
+ const liteModel =
1054
+ process.env.OPENCODE_LITE_MODEL || "claude-3-5-haiku-20241022";
1055
+
1056
+ const promptText = `You are generating a continuation prompt for a compacted swarm coordination session.
1057
+
1058
+ Analyze this swarm state and generate a structured markdown prompt that will be given to the resumed session:
1059
+
1060
+ ${JSON.stringify(snapshot, null, 2)}
1061
+
1062
+ Generate a prompt following this structure:
1063
+
1064
+ # 🐝 Swarm Continuation - [Epic Title or "Unknown"]
1065
+
1066
+ You are resuming coordination of an active swarm that was interrupted by context compaction.
1067
+
1068
+ ## Epic State
1069
+
1070
+ **ID:** [epic ID or "Unknown"]
1071
+ **Title:** [epic title or "No active epic"]
1072
+ **Status:** [X/Y subtasks complete]
1073
+ **Project:** ${projectDirectory}
1074
+
1075
+ ## Subtask Status
1076
+
1077
+ ### ✅ Completed (N)
1078
+ [List completed subtasks with IDs]
1079
+
1080
+ ### 🚧 In Progress (N)
1081
+ [List in-progress subtasks with IDs, files, agents if known]
1082
+
1083
+ ### 🚫 Blocked (N)
1084
+ [List blocked subtasks]
1085
+
1086
+ ### ⏳ Pending (N)
1087
+ [List pending subtasks]
1088
+
1089
+ ## Next Actions (IMMEDIATE)
1090
+
1091
+ [List 3-5 concrete actions with actual commands, using real IDs from the state]
1092
+
1093
+ ## Coordinator Reminders
1094
+
1095
+ - **You are the coordinator** - Don't wait for instructions, orchestrate
1096
+ - **Monitor actively** - Check messages every ~10 minutes
1097
+ - **Unblock aggressively** - Resolve dependencies immediately
1098
+ - **Review thoroughly** - 3-strike rule enforced
1099
+ - **Ship it** - When all subtasks done, close the epic
1100
+
1101
+ Keep the prompt concise but actionable. Use actual data from the snapshot, not placeholders.`;
1102
+
1103
+ const result = await new Promise<{ exitCode: number; stdout: string; stderr: string }>(
1104
+ (resolve, reject) => {
1105
+ const proc = spawn("opencode", ["run", "-m", liteModel, "--", promptText], {
1106
+ cwd: projectDirectory,
1107
+ stdio: ["ignore", "pipe", "pipe"],
1108
+ timeout: 30000, // 30 second timeout
1109
+ });
1110
+
1111
+ let stdout = "";
1112
+ let stderr = "";
1113
+
1114
+ proc.stdout.on("data", (d) => {
1115
+ stdout += d;
1116
+ });
1117
+ proc.stderr.on("data", (d) => {
1118
+ stderr += d;
1119
+ });
1120
+
1121
+ proc.on("close", (exitCode) => {
1122
+ resolve({ exitCode: exitCode ?? 1, stdout, stderr });
1123
+ });
1124
+
1125
+ proc.on("error", (err) => {
1126
+ reject(err);
1127
+ });
1128
+
1129
+ // Timeout handling
1130
+ setTimeout(() => {
1131
+ proc.kill("SIGTERM");
1132
+ reject(new Error("LLM compaction timeout (30s)"));
1133
+ }, 30000);
1134
+ },
1135
+ );
1136
+
1137
+ if (result.exitCode !== 0) {
1138
+ console.error(
1139
+ "[Swarm Compaction] opencode run failed:",
1140
+ result.stderr,
1141
+ );
1142
+ return null;
1143
+ }
1144
+
1145
+ // Extract the prompt from stdout (LLM may wrap in markdown)
1146
+ const prompt = result.stdout.trim();
1147
+ return prompt.length > 0 ? prompt : null;
1148
+ } catch (err) {
1149
+ console.error("[Swarm Compaction] LLM generation failed:", err);
1150
+ return null;
1151
+ }
1152
+ }
1153
+
899
1154
  /**
900
1155
  * Check for swarm sign - evidence a swarm passed through
901
1156
  *
@@ -1058,9 +1313,11 @@ Extract from session context:
1058
1313
 
1059
1314
  1. \`swarm_status(epic_id="<epic>", project_key="<path>")\` - Get current state
1060
1315
  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
1316
+ 3. \`swarm_review(project_key, epic_id, task_id, files_touched)\` - Review any completed work
1317
+ 4. \`swarm_review_feedback(project_key, task_id, worker_id, status, issues)\` - Approve or request changes
1318
+ 5. **Spawn ready subtasks** - Don't wait, fire them off
1319
+ 6. **Unblock blocked work** - Resolve dependencies, reassign if needed
1320
+ 7. **Collect completed work** - Close done subtasks, verify quality
1064
1321
 
1065
1322
  ### Keep the Swarm Cooking
1066
1323
 
@@ -1113,17 +1370,26 @@ Include this in your summary:
1113
1370
  "This is an active swarm. Check swarm_status and swarmmail_inbox immediately."
1114
1371
  `;
1115
1372
 
1116
- // Extended hooks type to include experimental compaction hook
1373
+ // Extended hooks type to include experimental compaction hook with new prompt API
1374
+ type CompactionOutput = {
1375
+ context: string[];
1376
+ prompt?: string; // NEW API from OpenCode PR #5907
1377
+ };
1378
+
1117
1379
  type ExtendedHooks = Hooks & {
1118
1380
  "experimental.session.compacting"?: (
1119
1381
  input: { sessionID: string },
1120
- output: { context: string[] },
1382
+ output: CompactionOutput,
1121
1383
  ) => Promise<void>;
1122
1384
  };
1123
1385
 
1124
1386
  export const SwarmPlugin: Plugin = async (
1125
- _input: PluginInput,
1387
+ input: PluginInput,
1126
1388
  ): Promise<ExtendedHooks> => {
1389
+ // CRITICAL: Set project directory from OpenCode input
1390
+ // Without this, CLI uses wrong database path
1391
+ projectDirectory = input.directory;
1392
+
1127
1393
  return {
1128
1394
  tool: {
1129
1395
  // Beads
@@ -1186,23 +1452,61 @@ export const SwarmPlugin: Plugin = async (
1186
1452
  skills_execute,
1187
1453
  },
1188
1454
 
1189
- // Swarm-aware compaction hook - injects context based on detection confidence
1455
+ // Swarm-aware compaction hook with LLM-powered continuation prompts
1456
+ // Three-level fallback chain: LLM → static context → detection fallback → none
1190
1457
  "experimental.session.compacting": async (
1191
- _input: { sessionID: string },
1192
- output: { context: string[] },
1458
+ input: { sessionID: string },
1459
+ output: CompactionOutput,
1193
1460
  ) => {
1194
1461
  const detection = await detectSwarm();
1195
1462
 
1196
1463
  if (detection.confidence === "high" || detection.confidence === "medium") {
1197
- // Definite or probable swarm - inject full context
1464
+ // Definite or probable swarm - try LLM-powered compaction
1465
+ try {
1466
+ // Level 1: Query actual state
1467
+ const snapshot = await querySwarmState(input.sessionID);
1468
+
1469
+ // Level 2: Generate prompt with LLM
1470
+ const llmPrompt = await generateCompactionPrompt(snapshot);
1471
+
1472
+ if (llmPrompt) {
1473
+ // SUCCESS: Use LLM-generated prompt
1474
+ const header = `[Swarm compaction: LLM-generated, ${detection.reasons.join(", ")}]\n\n`;
1475
+
1476
+ // Progressive enhancement: use new API if available
1477
+ if ("prompt" in output) {
1478
+ output.prompt = header + llmPrompt;
1479
+ } else {
1480
+ output.context.push(header + llmPrompt);
1481
+ }
1482
+
1483
+ console.log(
1484
+ "[Swarm Compaction] Using LLM-generated continuation prompt",
1485
+ );
1486
+ return;
1487
+ }
1488
+
1489
+ // LLM failed, fall through to static prompt
1490
+ console.log(
1491
+ "[Swarm Compaction] LLM generation returned null, using static prompt",
1492
+ );
1493
+ } catch (err) {
1494
+ // LLM failed, fall through to static prompt
1495
+ console.error(
1496
+ "[Swarm Compaction] LLM generation failed, using static prompt:",
1497
+ err,
1498
+ );
1499
+ }
1500
+
1501
+ // Level 3: Fall back to static context
1198
1502
  const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
1199
1503
  output.context.push(header + SWARM_COMPACTION_CONTEXT);
1200
1504
  } else if (detection.confidence === "low") {
1201
- // Possible swarm - inject fallback detection prompt
1505
+ // Level 4: Possible swarm - inject fallback detection prompt
1202
1506
  const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
1203
1507
  output.context.push(header + SWARM_DETECTION_FALLBACK);
1204
1508
  }
1205
- // confidence === "none" - no injection, probably not a swarm
1509
+ // Level 5: confidence === "none" - no injection, probably not a swarm
1206
1510
  },
1207
1511
  };
1208
1512
  };
@@ -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.33.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,8 @@
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.4.0",
43
+ "yaml": "^2.8.2",
43
44
  "zod": "4.1.8"
44
45
  },
45
46
  "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");