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.
- package/.turbo/turbo-build.log +4 -4
- package/.turbo/turbo-test.log +324 -316
- package/CHANGELOG.md +394 -0
- package/README.md +129 -181
- package/bin/swarm.test.ts +31 -0
- package/bin/swarm.ts +635 -140
- package/dist/compaction-hook.d.ts +1 -1
- package/dist/compaction-hook.d.ts.map +1 -1
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +653 -139
- package/dist/memory-tools.d.ts.map +1 -1
- package/dist/memory.d.ts +5 -4
- package/dist/memory.d.ts.map +1 -1
- package/dist/observability-tools.d.ts +116 -0
- package/dist/observability-tools.d.ts.map +1 -0
- package/dist/plugin.js +648 -136
- package/dist/skills.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts +29 -5
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts +66 -0
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm.d.ts +17 -2
- package/dist/swarm.d.ts.map +1 -1
- package/evals/lib/{data-loader.test.ts → data-loader.evalite-test.ts} +7 -6
- package/evals/lib/data-loader.ts +1 -1
- package/evals/scorers/{outcome-scorers.test.ts → outcome-scorers.evalite-test.ts} +1 -1
- package/examples/plugin-wrapper-template.ts +316 -12
- package/global-skills/swarm-coordination/SKILL.md +118 -8
- package/package.json +3 -2
- package/src/compaction-hook.ts +5 -3
- package/src/hive.integration.test.ts +83 -1
- package/src/hive.ts +37 -12
- package/src/index.ts +25 -1
- package/src/mandate-storage.integration.test.ts +601 -0
- package/src/memory-tools.ts +6 -4
- package/src/memory.integration.test.ts +117 -49
- package/src/memory.test.ts +41 -217
- package/src/memory.ts +12 -8
- package/src/observability-tools.test.ts +346 -0
- package/src/observability-tools.ts +594 -0
- package/src/repo-crawl.integration.test.ts +441 -0
- package/src/skills.integration.test.ts +1192 -0
- package/src/skills.test.ts +42 -1
- package/src/skills.ts +8 -4
- package/src/structured.integration.test.ts +817 -0
- package/src/swarm-deferred.integration.test.ts +157 -0
- package/src/swarm-deferred.test.ts +38 -0
- package/src/swarm-mail.integration.test.ts +15 -19
- package/src/swarm-orchestrate.integration.test.ts +282 -0
- package/src/swarm-orchestrate.test.ts +123 -0
- package/src/swarm-orchestrate.ts +279 -201
- package/src/swarm-prompts.test.ts +481 -0
- package/src/swarm-prompts.ts +297 -0
- package/src/swarm-research.integration.test.ts +544 -0
- package/src/swarm-research.test.ts +698 -0
- package/src/swarm-research.ts +472 -0
- package/src/swarm-review.integration.test.ts +290 -0
- package/src/swarm.integration.test.ts +23 -20
- package/src/swarm.ts +6 -3
- 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.
|
|
1062
|
-
4.
|
|
1063
|
-
5. **
|
|
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:
|
|
1382
|
+
output: CompactionOutput,
|
|
1121
1383
|
) => Promise<void>;
|
|
1122
1384
|
};
|
|
1123
1385
|
|
|
1124
1386
|
export const SwarmPlugin: Plugin = async (
|
|
1125
|
-
|
|
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 -
|
|
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
|
-
|
|
1192
|
-
output:
|
|
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 -
|
|
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:
|
|
447
|
+
### Phase 6: MANDATORY Review Loop (NON-NEGOTIABLE)
|
|
446
448
|
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
42
|
+
"swarm-mail": "1.4.0",
|
|
43
|
+
"yaml": "^2.8.2",
|
|
43
44
|
"zod": "4.1.8"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
package/src/compaction-hook.ts
CHANGED
|
@@ -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.
|
|
92
|
-
4.
|
|
93
|
-
5. **
|
|
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 =
|
|
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");
|