opencode-swarm-plugin 0.45.0 → 0.45.1
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/bin/swarm.ts +156 -29
- package/dist/bin/swarm.js +125212 -0
- package/examples/plugin-wrapper-template.ts +311 -30
- package/package.json +2 -2
|
@@ -1,16 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
* ║ ║
|
|
4
|
+
* ║ 🐝 OPENCODE SWARM PLUGIN WRAPPER 🐝 ║
|
|
5
|
+
* ║ ║
|
|
6
|
+
* ║ This file lives at: ~/.config/opencode/plugin/swarm.ts ║
|
|
7
|
+
* ║ Generated by: swarm setup ║
|
|
8
|
+
* ║ ║
|
|
9
|
+
* ╠═══════════════════════════════════════════════════════════════════════════╣
|
|
10
|
+
* ║ ║
|
|
11
|
+
* ║ ⚠️ CRITICAL: THIS FILE MUST BE 100% SELF-CONTAINED ⚠️ ║
|
|
12
|
+
* ║ ║
|
|
13
|
+
* ║ ❌ NEVER import from "opencode-swarm-plugin" npm package ║
|
|
14
|
+
* ║ ❌ NEVER import from any package with transitive deps (evalite, etc) ║
|
|
15
|
+
* ║ ❌ NEVER add dependencies that aren't provided by OpenCode ║
|
|
16
|
+
* ║ ║
|
|
17
|
+
* ║ ✅ ONLY import from: @opencode-ai/plugin, @opencode-ai/sdk, node:* ║
|
|
18
|
+
* ║ ✅ Shell out to `swarm` CLI for all tool execution ║
|
|
19
|
+
* ║ ✅ Inline any logic that would otherwise require imports ║
|
|
20
|
+
* ║ ║
|
|
21
|
+
* ║ WHY? The npm package has dependencies (evalite, etc) that aren't ║
|
|
22
|
+
* ║ available in OpenCode's plugin context. Importing causes: ║
|
|
23
|
+
* ║ "Cannot find module 'evalite/runner'" → trace trap → OpenCode crash ║
|
|
24
|
+
* ║ ║
|
|
25
|
+
* ║ PATTERN: Plugin wrapper is DUMB. CLI is SMART. ║
|
|
26
|
+
* ║ - Wrapper: thin shell, no logic, just bridges to CLI ║
|
|
27
|
+
* ║ - CLI: all the smarts, all the deps, runs in its own context ║
|
|
28
|
+
* ║ ║
|
|
29
|
+
* ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
3
30
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* All tool logic lives in the npm package - this just bridges to it.
|
|
9
|
-
*
|
|
10
|
-
* Environment variables:
|
|
11
|
-
* - OPENCODE_SESSION_ID: Passed to CLI for session state persistence
|
|
12
|
-
* - OPENCODE_MESSAGE_ID: Passed to CLI for context
|
|
13
|
-
* - OPENCODE_AGENT: Passed to CLI for context
|
|
31
|
+
* Environment variables passed to CLI:
|
|
32
|
+
* - OPENCODE_SESSION_ID: Session state persistence
|
|
33
|
+
* - OPENCODE_MESSAGE_ID: Message context
|
|
34
|
+
* - OPENCODE_AGENT: Agent context
|
|
14
35
|
* - SWARM_PROJECT_DIR: Project directory (critical for database path)
|
|
15
36
|
*/
|
|
16
37
|
import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
@@ -21,15 +42,267 @@ import { appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
|
21
42
|
import { join } from "node:path";
|
|
22
43
|
import { homedir } from "node:os";
|
|
23
44
|
|
|
24
|
-
//
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Swarm Signature Detection (INLINED - do not import from opencode-swarm-plugin)
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Subtask lifecycle status derived from events
|
|
51
|
+
*/
|
|
52
|
+
type SubtaskStatus = "created" | "spawned" | "in_progress" | "completed" | "closed";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Subtask state projected from events
|
|
56
|
+
*/
|
|
57
|
+
interface SubtaskState {
|
|
58
|
+
id: string;
|
|
59
|
+
title: string;
|
|
60
|
+
status: SubtaskStatus;
|
|
61
|
+
files: string[];
|
|
62
|
+
worker?: string;
|
|
63
|
+
spawnedAt?: number;
|
|
64
|
+
completedAt?: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Epic state projected from events
|
|
69
|
+
*/
|
|
70
|
+
interface EpicState {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
status: "open" | "in_progress" | "closed";
|
|
74
|
+
createdAt: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Complete swarm state projected from session events
|
|
79
|
+
*/
|
|
80
|
+
interface SwarmProjection {
|
|
81
|
+
isSwarm: boolean;
|
|
82
|
+
epic?: EpicState;
|
|
83
|
+
subtasks: Map<string, SubtaskState>;
|
|
84
|
+
projectPath?: string;
|
|
85
|
+
coordinatorName?: string;
|
|
86
|
+
lastEventAt?: number;
|
|
87
|
+
counts: {
|
|
88
|
+
total: number;
|
|
89
|
+
created: number;
|
|
90
|
+
spawned: number;
|
|
91
|
+
inProgress: number;
|
|
92
|
+
completed: number;
|
|
93
|
+
closed: number;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tool call event extracted from session messages
|
|
99
|
+
*/
|
|
100
|
+
interface ToolCallEvent {
|
|
101
|
+
tool: string;
|
|
102
|
+
input: Record<string, unknown>;
|
|
103
|
+
output: string;
|
|
104
|
+
timestamp: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Parse epic ID from hive_create_epic output */
|
|
108
|
+
function parseEpicId(output: string): string | undefined {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(output);
|
|
111
|
+
return parsed.epic?.id || parsed.id;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Parse subtask IDs from hive_create_epic output */
|
|
118
|
+
function parseSubtaskIds(output: string): string[] {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(output);
|
|
121
|
+
const subtasks = parsed.subtasks || parsed.epic?.subtasks || [];
|
|
122
|
+
return subtasks
|
|
123
|
+
.map((s: unknown) => {
|
|
124
|
+
if (typeof s === "object" && s !== null && "id" in s) {
|
|
125
|
+
return (s as { id: string }).id;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
})
|
|
129
|
+
.filter((id: unknown): id is string => typeof id === "string");
|
|
130
|
+
} catch {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Project swarm state from session tool call events
|
|
137
|
+
*/
|
|
138
|
+
function projectSwarmState(events: ToolCallEvent[]): SwarmProjection {
|
|
139
|
+
const state: SwarmProjection = {
|
|
140
|
+
isSwarm: false,
|
|
141
|
+
subtasks: new Map(),
|
|
142
|
+
counts: { total: 0, created: 0, spawned: 0, inProgress: 0, completed: 0, closed: 0 },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
let hasEpic = false;
|
|
146
|
+
let hasSpawn = false;
|
|
147
|
+
|
|
148
|
+
for (const event of events) {
|
|
149
|
+
state.lastEventAt = event.timestamp;
|
|
150
|
+
|
|
151
|
+
switch (event.tool) {
|
|
152
|
+
case "hive_create_epic": {
|
|
153
|
+
const epicId = parseEpicId(event.output);
|
|
154
|
+
const epicTitle = typeof event.input.epic_title === "string" ? event.input.epic_title : undefined;
|
|
155
|
+
|
|
156
|
+
if (epicId) {
|
|
157
|
+
state.epic = { id: epicId, title: epicTitle || "Unknown Epic", status: "open", createdAt: event.timestamp };
|
|
158
|
+
hasEpic = true;
|
|
159
|
+
|
|
160
|
+
const subtasks = event.input.subtasks;
|
|
161
|
+
if (Array.isArray(subtasks)) {
|
|
162
|
+
for (const subtask of subtasks) {
|
|
163
|
+
if (typeof subtask === "object" && subtask !== null) {
|
|
164
|
+
state.counts.created++;
|
|
165
|
+
state.counts.total++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const subtaskIds = parseSubtaskIds(event.output);
|
|
171
|
+
for (const id of subtaskIds) {
|
|
172
|
+
if (!state.subtasks.has(id)) {
|
|
173
|
+
state.subtasks.set(id, { id, title: "Unknown", status: "created", files: [] });
|
|
174
|
+
state.counts.total++;
|
|
175
|
+
state.counts.created++;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
case "swarm_spawn_subtask": {
|
|
183
|
+
const beadId = typeof event.input.bead_id === "string" ? event.input.bead_id : undefined;
|
|
184
|
+
const title = typeof event.input.subtask_title === "string" ? event.input.subtask_title : "Unknown";
|
|
185
|
+
const files = Array.isArray(event.input.files) ? (event.input.files as string[]) : [];
|
|
186
|
+
|
|
187
|
+
if (beadId) {
|
|
188
|
+
hasSpawn = true;
|
|
189
|
+
const existing = state.subtasks.get(beadId);
|
|
190
|
+
if (existing) {
|
|
191
|
+
if (existing.status === "created") { state.counts.created--; state.counts.spawned++; }
|
|
192
|
+
existing.status = "spawned";
|
|
193
|
+
existing.title = title;
|
|
194
|
+
existing.files = files;
|
|
195
|
+
existing.spawnedAt = event.timestamp;
|
|
196
|
+
} else {
|
|
197
|
+
state.subtasks.set(beadId, { id: beadId, title, status: "spawned", files, spawnedAt: event.timestamp });
|
|
198
|
+
state.counts.total++;
|
|
199
|
+
state.counts.spawned++;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const epicId = typeof event.input.epic_id === "string" ? event.input.epic_id : undefined;
|
|
203
|
+
if (epicId && !state.epic) {
|
|
204
|
+
state.epic = { id: epicId, title: "Unknown Epic", status: "in_progress", createdAt: event.timestamp };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case "hive_start": {
|
|
211
|
+
const id = typeof event.input.id === "string" ? event.input.id : undefined;
|
|
212
|
+
if (id) {
|
|
213
|
+
const subtask = state.subtasks.get(id);
|
|
214
|
+
if (subtask && subtask.status !== "completed" && subtask.status !== "closed") {
|
|
215
|
+
if (subtask.status === "created") state.counts.created--;
|
|
216
|
+
else if (subtask.status === "spawned") state.counts.spawned--;
|
|
217
|
+
subtask.status = "in_progress";
|
|
218
|
+
state.counts.inProgress++;
|
|
219
|
+
}
|
|
220
|
+
if (state.epic && state.epic.id === id) state.epic.status = "in_progress";
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case "swarm_complete": {
|
|
226
|
+
const beadId = typeof event.input.bead_id === "string" ? event.input.bead_id : undefined;
|
|
227
|
+
if (beadId) {
|
|
228
|
+
const subtask = state.subtasks.get(beadId);
|
|
229
|
+
if (subtask && subtask.status !== "closed") {
|
|
230
|
+
if (subtask.status === "created") state.counts.created--;
|
|
231
|
+
else if (subtask.status === "spawned") state.counts.spawned--;
|
|
232
|
+
else if (subtask.status === "in_progress") state.counts.inProgress--;
|
|
233
|
+
subtask.status = "completed";
|
|
234
|
+
subtask.completedAt = event.timestamp;
|
|
235
|
+
state.counts.completed++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
case "hive_close": {
|
|
242
|
+
const id = typeof event.input.id === "string" ? event.input.id : undefined;
|
|
243
|
+
if (id) {
|
|
244
|
+
const subtask = state.subtasks.get(id);
|
|
245
|
+
if (subtask) {
|
|
246
|
+
if (subtask.status === "created") state.counts.created--;
|
|
247
|
+
else if (subtask.status === "spawned") state.counts.spawned--;
|
|
248
|
+
else if (subtask.status === "in_progress") state.counts.inProgress--;
|
|
249
|
+
else if (subtask.status === "completed") state.counts.completed--;
|
|
250
|
+
subtask.status = "closed";
|
|
251
|
+
state.counts.closed++;
|
|
252
|
+
}
|
|
253
|
+
if (state.epic && state.epic.id === id) state.epic.status = "closed";
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case "swarmmail_init": {
|
|
259
|
+
try {
|
|
260
|
+
const parsed = JSON.parse(event.output);
|
|
261
|
+
if (parsed.agent_name) state.coordinatorName = parsed.agent_name;
|
|
262
|
+
if (parsed.project_key) state.projectPath = parsed.project_key;
|
|
263
|
+
} catch { /* skip */ }
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
state.isSwarm = hasEpic && hasSpawn;
|
|
270
|
+
return state;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Quick check for swarm signature without full projection */
|
|
274
|
+
function hasSwarmSignature(events: ToolCallEvent[]): boolean {
|
|
275
|
+
let hasEpic = false;
|
|
276
|
+
let hasSpawn = false;
|
|
277
|
+
for (const event of events) {
|
|
278
|
+
if (event.tool === "hive_create_epic") hasEpic = true;
|
|
279
|
+
else if (event.tool === "swarm_spawn_subtask") hasSpawn = true;
|
|
280
|
+
if (hasEpic && hasSpawn) return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Check if swarm is still active (has pending work) */
|
|
286
|
+
function isSwarmActive(projection: SwarmProjection): boolean {
|
|
287
|
+
if (!projection.isSwarm) return false;
|
|
288
|
+
return projection.counts.created > 0 || projection.counts.spawned > 0 ||
|
|
289
|
+
projection.counts.inProgress > 0 || projection.counts.completed > 0;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Get human-readable swarm status summary */
|
|
293
|
+
function getSwarmSummary(projection: SwarmProjection): string {
|
|
294
|
+
if (!projection.isSwarm) return "No swarm detected";
|
|
295
|
+
const { counts, epic } = projection;
|
|
296
|
+
const parts: string[] = [];
|
|
297
|
+
if (epic) parts.push(`Epic: ${epic.id} - ${epic.title} [${epic.status}]`);
|
|
298
|
+
parts.push(`Subtasks: ${counts.total} total (${counts.spawned} spawned, ${counts.inProgress} in_progress, ${counts.completed} completed, ${counts.closed} closed)`);
|
|
299
|
+
parts.push(isSwarmActive(projection) ? "Status: ACTIVE - has pending work" : "Status: COMPLETE - all work closed");
|
|
300
|
+
return parts.join("\n");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =============================================================================
|
|
304
|
+
// Constants
|
|
305
|
+
// =============================================================================
|
|
33
306
|
|
|
34
307
|
const SWARM_CLI = "swarm";
|
|
35
308
|
|
|
@@ -76,10 +349,10 @@ function logCompaction(
|
|
|
76
349
|
}
|
|
77
350
|
|
|
78
351
|
/**
|
|
79
|
-
* Capture compaction event for evals
|
|
352
|
+
* Capture compaction event for evals via CLI
|
|
80
353
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
354
|
+
* Shells out to `swarm capture` command to avoid import issues.
|
|
355
|
+
* The CLI handles all the logic - plugin wrapper stays dumb.
|
|
83
356
|
*
|
|
84
357
|
* @param sessionID - Session ID
|
|
85
358
|
* @param epicID - Epic ID (or "unknown" if not detected)
|
|
@@ -93,14 +366,22 @@ async function captureCompaction(
|
|
|
93
366
|
payload: any,
|
|
94
367
|
): Promise<void> {
|
|
95
368
|
try {
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
payload,
|
|
369
|
+
// Shell out to CLI - no imports needed, version always matches
|
|
370
|
+
const args = [
|
|
371
|
+
"capture",
|
|
372
|
+
"--session", sessionID,
|
|
373
|
+
"--epic", epicID,
|
|
374
|
+
"--type", compactionType,
|
|
375
|
+
"--payload", JSON.stringify(payload),
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
const proc = spawn(SWARM_CLI, args, {
|
|
379
|
+
env: { ...process.env, SWARM_PROJECT_DIR: projectDirectory },
|
|
380
|
+
stdio: ["ignore", "ignore", "ignore"], // Fire and forget
|
|
103
381
|
});
|
|
382
|
+
|
|
383
|
+
// Don't wait - capture is non-blocking
|
|
384
|
+
proc.unref();
|
|
104
385
|
} catch (err) {
|
|
105
386
|
// Non-fatal - capture failures shouldn't break compaction
|
|
106
387
|
logCompaction("warn", "compaction_capture_failed", {
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.1",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
|
-
"swarm": "./bin/swarm.
|
|
9
|
+
"swarm": "./dist/bin/swarm.js"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|