opencode-swarm-plugin 0.45.0 → 0.45.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/bin/swarm.ts +156 -29
- package/dist/bin/swarm.js +125980 -0
- package/dist/cass-tools.d.ts +75 -0
- package/dist/cass-tools.d.ts.map +1 -0
- package/dist/eval-capture.d.ts +6 -6
- package/dist/hive.d.ts +20 -20
- package/dist/hive.d.ts.map +1 -1
- package/dist/hive.js +79 -0
- package/dist/index.d.ts +91 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +512 -35
- package/dist/memory-tools.d.ts.map +1 -1
- package/dist/plugin.js +511 -35
- package/dist/schemas/cell-events.d.ts +4 -4
- package/dist/schemas/cell.d.ts +10 -10
- package/dist/schemas/swarm-context.d.ts +2 -2
- package/dist/skills.d.ts +8 -8
- package/dist/skills.d.ts.map +1 -1
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.js +242 -29
- package/examples/plugin-wrapper-template.ts +419 -37
- package/package.json +3 -3
|
@@ -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", {
|
|
@@ -2063,6 +2344,97 @@ Extract from session context:
|
|
|
2063
2344
|
**You are not waiting for instructions. You are the coordinator. Coordinate.**
|
|
2064
2345
|
`;
|
|
2065
2346
|
|
|
2347
|
+
/**
|
|
2348
|
+
* Build dynamic swarm state section from snapshot
|
|
2349
|
+
*
|
|
2350
|
+
* This creates a concrete state summary with actual IDs and status
|
|
2351
|
+
* to prepend to the static compaction context.
|
|
2352
|
+
*/
|
|
2353
|
+
function buildDynamicStateFromSnapshot(snapshot: SwarmStateSnapshot): string {
|
|
2354
|
+
if (!snapshot.epic) {
|
|
2355
|
+
return "";
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const parts: string[] = [];
|
|
2359
|
+
|
|
2360
|
+
// Header with epic info
|
|
2361
|
+
parts.push(`## 🐝 Current Swarm State\n`);
|
|
2362
|
+
parts.push(`**Epic:** ${snapshot.epic.id} - ${snapshot.epic.title}`);
|
|
2363
|
+
parts.push(`**Status:** ${snapshot.epic.status}`);
|
|
2364
|
+
parts.push(`**Project:** ${projectDirectory}\n`);
|
|
2365
|
+
|
|
2366
|
+
// Subtask breakdown
|
|
2367
|
+
const subtasks = snapshot.epic.subtasks || [];
|
|
2368
|
+
const completed = subtasks.filter(s => s.status === "closed");
|
|
2369
|
+
const inProgress = subtasks.filter(s => s.status === "in_progress");
|
|
2370
|
+
const blocked = subtasks.filter(s => s.status === "blocked");
|
|
2371
|
+
const pending = subtasks.filter(s => s.status === "open");
|
|
2372
|
+
|
|
2373
|
+
parts.push(`**Progress:** ${completed.length}/${subtasks.length} subtasks complete\n`);
|
|
2374
|
+
|
|
2375
|
+
// Immediate actions with real IDs
|
|
2376
|
+
parts.push(`## 1️⃣ IMMEDIATE ACTIONS (Do These FIRST)\n`);
|
|
2377
|
+
parts.push(`1. \`swarm_status(epic_id="${snapshot.epic.id}", project_key="${projectDirectory}")\` - Get current state`);
|
|
2378
|
+
parts.push(`2. \`swarmmail_inbox(limit=5)\` - Check for worker messages`);
|
|
2379
|
+
|
|
2380
|
+
if (inProgress.length > 0) {
|
|
2381
|
+
parts.push(`3. Review in-progress work when workers complete`);
|
|
2382
|
+
}
|
|
2383
|
+
if (pending.length > 0) {
|
|
2384
|
+
const next = pending[0];
|
|
2385
|
+
parts.push(`4. Spawn next subtask: \`swarm_spawn_subtask(bead_id="${next.id}", ...)\``);
|
|
2386
|
+
}
|
|
2387
|
+
if (blocked.length > 0) {
|
|
2388
|
+
parts.push(`5. Unblock: ${blocked.map(s => s.id).join(", ")}`);
|
|
2389
|
+
}
|
|
2390
|
+
parts.push("");
|
|
2391
|
+
|
|
2392
|
+
// Detailed subtask status
|
|
2393
|
+
if (inProgress.length > 0) {
|
|
2394
|
+
parts.push(`### 🚧 In Progress (${inProgress.length})`);
|
|
2395
|
+
for (const s of inProgress) {
|
|
2396
|
+
const files = s.files?.length ? ` (${s.files.slice(0, 3).join(", ")}${s.files.length > 3 ? "..." : ""})` : "";
|
|
2397
|
+
parts.push(`- ${s.id}: ${s.title}${files}`);
|
|
2398
|
+
}
|
|
2399
|
+
parts.push("");
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
if (blocked.length > 0) {
|
|
2403
|
+
parts.push(`### 🚫 Blocked (${blocked.length})`);
|
|
2404
|
+
for (const s of blocked) {
|
|
2405
|
+
parts.push(`- ${s.id}: ${s.title}`);
|
|
2406
|
+
}
|
|
2407
|
+
parts.push("");
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
if (pending.length > 0) {
|
|
2411
|
+
parts.push(`### ⏳ Ready to Spawn (${pending.length})`);
|
|
2412
|
+
for (const s of pending.slice(0, 5)) { // Show first 5
|
|
2413
|
+
const files = s.files?.length ? ` (${s.files.slice(0, 2).join(", ")}${s.files.length > 2 ? "..." : ""})` : "";
|
|
2414
|
+
parts.push(`- ${s.id}: ${s.title}${files}`);
|
|
2415
|
+
}
|
|
2416
|
+
if (pending.length > 5) {
|
|
2417
|
+
parts.push(`- ... and ${pending.length - 5} more`);
|
|
2418
|
+
}
|
|
2419
|
+
parts.push("");
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
if (completed.length > 0) {
|
|
2423
|
+
parts.push(`### ✅ Completed (${completed.length})`);
|
|
2424
|
+
for (const s of completed.slice(-3)) { // Show last 3
|
|
2425
|
+
parts.push(`- ${s.id}: ${s.title} ✓`);
|
|
2426
|
+
}
|
|
2427
|
+
if (completed.length > 3) {
|
|
2428
|
+
parts.push(`- ... and ${completed.length - 3} more`);
|
|
2429
|
+
}
|
|
2430
|
+
parts.push("");
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
parts.push("---\n");
|
|
2434
|
+
|
|
2435
|
+
return parts.join("\n");
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2066
2438
|
/**
|
|
2067
2439
|
* Fallback detection prompt - tells the compactor what to look for
|
|
2068
2440
|
*
|
|
@@ -2329,6 +2701,9 @@ const SwarmPlugin: Plugin = async (
|
|
|
2329
2701
|
has_projection: !!sessionScan.projection?.isSwarm,
|
|
2330
2702
|
});
|
|
2331
2703
|
|
|
2704
|
+
// Hoist snapshot outside try block so it's available in fallback path
|
|
2705
|
+
let snapshot: SwarmStateSnapshot | undefined;
|
|
2706
|
+
|
|
2332
2707
|
try {
|
|
2333
2708
|
// =======================================================================
|
|
2334
2709
|
// PREFER PROJECTION (ground truth from events) OVER HIVE QUERY
|
|
@@ -2336,8 +2711,6 @@ const SwarmPlugin: Plugin = async (
|
|
|
2336
2711
|
// The projection is derived from session events - it's the source of truth.
|
|
2337
2712
|
// Hive query may show all cells closed even if swarm was active.
|
|
2338
2713
|
|
|
2339
|
-
let snapshot: SwarmStateSnapshot;
|
|
2340
|
-
|
|
2341
2714
|
if (sessionScan.projection?.isSwarm) {
|
|
2342
2715
|
// Use projection as primary source - convert to snapshot format
|
|
2343
2716
|
const proj = sessionScan.projection;
|
|
@@ -2520,9 +2893,12 @@ const SwarmPlugin: Plugin = async (
|
|
|
2520
2893
|
});
|
|
2521
2894
|
}
|
|
2522
2895
|
|
|
2523
|
-
// Level 3: Fall back to static context
|
|
2896
|
+
// Level 3: Fall back to static context WITH dynamic state from snapshot
|
|
2524
2897
|
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
2525
|
-
|
|
2898
|
+
|
|
2899
|
+
// Build dynamic state section if we have snapshot data
|
|
2900
|
+
const dynamicState = snapshot ? buildDynamicStateFromSnapshot(snapshot) : "";
|
|
2901
|
+
const staticContent = header + dynamicState + SWARM_COMPACTION_CONTEXT;
|
|
2526
2902
|
output.context.push(staticContent);
|
|
2527
2903
|
|
|
2528
2904
|
// =======================================================================
|
|
@@ -2530,13 +2906,16 @@ const SwarmPlugin: Plugin = async (
|
|
|
2530
2906
|
// =======================================================================
|
|
2531
2907
|
await captureCompaction(
|
|
2532
2908
|
input.sessionID,
|
|
2533
|
-
"unknown",
|
|
2909
|
+
snapshot?.epic?.id || "unknown",
|
|
2534
2910
|
"context_injected",
|
|
2535
2911
|
{
|
|
2536
2912
|
full_content: staticContent,
|
|
2537
2913
|
content_length: staticContent.length,
|
|
2538
2914
|
injection_method: "output.context.push",
|
|
2539
|
-
context_type: "
|
|
2915
|
+
context_type: "static_with_dynamic_state",
|
|
2916
|
+
has_dynamic_state: !!dynamicState,
|
|
2917
|
+
epic_id: snapshot?.epic?.id,
|
|
2918
|
+
subtask_count: snapshot?.epic?.subtasks?.length ?? 0,
|
|
2540
2919
|
},
|
|
2541
2920
|
);
|
|
2542
2921
|
|
|
@@ -2545,9 +2924,12 @@ const SwarmPlugin: Plugin = async (
|
|
|
2545
2924
|
session_id: input.sessionID,
|
|
2546
2925
|
total_duration_ms: totalDuration,
|
|
2547
2926
|
confidence: detection.confidence,
|
|
2548
|
-
context_type: "static_swarm_context",
|
|
2927
|
+
context_type: dynamicState ? "static_with_dynamic_state" : "static_swarm_context",
|
|
2549
2928
|
content_length: staticContent.length,
|
|
2550
2929
|
context_count_after: output.context.length,
|
|
2930
|
+
has_dynamic_state: !!dynamicState,
|
|
2931
|
+
epic_id: snapshot?.epic?.id,
|
|
2932
|
+
subtask_count: snapshot?.epic?.subtasks?.length ?? 0,
|
|
2551
2933
|
});
|
|
2552
2934
|
} else if (detection.confidence === "low") {
|
|
2553
2935
|
// Level 4: Possible swarm - inject fallback detection prompt
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm-plugin",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.2",
|
|
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
|
".": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"minimatch": "^10.1.1",
|
|
64
64
|
"pino": "^9.6.0",
|
|
65
65
|
"pino-roll": "^1.3.0",
|
|
66
|
-
"swarm-mail": "1.6.
|
|
66
|
+
"swarm-mail": "1.6.2",
|
|
67
67
|
"yaml": "^2.8.2",
|
|
68
68
|
"zod": "4.1.8"
|
|
69
69
|
},
|