opencode-swarm-plugin 0.34.0 → 0.36.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-test.log +333 -333
- package/CHANGELOG.md +97 -0
- package/bin/swarm.test.ts +70 -0
- package/bin/swarm.ts +139 -14
- package/examples/plugin-wrapper-template.ts +447 -33
- package/package.json +1 -1
- package/src/compaction-hook.test.ts +226 -258
- package/src/compaction-hook.ts +361 -16
- package/src/eval-capture.ts +5 -6
- package/src/index.ts +21 -1
- package/src/learning.integration.test.ts +0 -2
- package/src/schemas/task.ts +0 -1
- package/src/swarm-decompose.ts +1 -15
- package/src/swarm-prompts.ts +1 -8
- package/src/swarm.integration.test.ts +0 -40
package/src/compaction-hook.ts
CHANGED
|
@@ -216,6 +216,315 @@ function buildDynamicSwarmState(state: SwarmState): string {
|
|
|
216
216
|
return parts.join("\n");
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// SDK Message Scanning
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Tool part with completed state containing input/output
|
|
225
|
+
*/
|
|
226
|
+
interface ToolPart {
|
|
227
|
+
id: string;
|
|
228
|
+
sessionID: string;
|
|
229
|
+
messageID: string;
|
|
230
|
+
type: "tool";
|
|
231
|
+
callID: string;
|
|
232
|
+
tool: string;
|
|
233
|
+
state: ToolState;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Tool state (completed tools have input/output we need)
|
|
238
|
+
*/
|
|
239
|
+
type ToolState =
|
|
240
|
+
| {
|
|
241
|
+
status: "completed";
|
|
242
|
+
input: { [key: string]: unknown };
|
|
243
|
+
output: string;
|
|
244
|
+
title: string;
|
|
245
|
+
metadata: { [key: string]: unknown };
|
|
246
|
+
time: { start: number; end: number };
|
|
247
|
+
}
|
|
248
|
+
| {
|
|
249
|
+
status: string;
|
|
250
|
+
[key: string]: unknown;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* SDK Client type (minimal interface for scanSessionMessages)
|
|
255
|
+
*
|
|
256
|
+
* The actual SDK client uses a more complex Options-based API:
|
|
257
|
+
* client.session.messages({ path: { id: sessionID }, query: { limit } })
|
|
258
|
+
*
|
|
259
|
+
* We accept `unknown` and handle the type internally to avoid
|
|
260
|
+
* tight coupling to SDK internals.
|
|
261
|
+
*/
|
|
262
|
+
export type OpencodeClient = unknown;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Scanned swarm state extracted from session messages
|
|
266
|
+
*/
|
|
267
|
+
export interface ScannedSwarmState {
|
|
268
|
+
epicId?: string;
|
|
269
|
+
epicTitle?: string;
|
|
270
|
+
projectPath?: string;
|
|
271
|
+
agentName?: string;
|
|
272
|
+
subtasks: Map<
|
|
273
|
+
string,
|
|
274
|
+
{ title: string; status: string; worker?: string; files?: string[] }
|
|
275
|
+
>;
|
|
276
|
+
lastAction?: { tool: string; args: unknown; timestamp: number };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Scan session messages for swarm state using SDK client
|
|
281
|
+
*
|
|
282
|
+
* Extracts swarm coordination state from actual tool calls:
|
|
283
|
+
* - swarm_spawn_subtask → subtask tracking
|
|
284
|
+
* - swarmmail_init → agent name, project path
|
|
285
|
+
* - hive_create_epic → epic ID and title
|
|
286
|
+
* - swarm_status → epic reference
|
|
287
|
+
* - swarm_complete → subtask completion
|
|
288
|
+
*
|
|
289
|
+
* @param client - OpenCode SDK client (undefined if not available)
|
|
290
|
+
* @param sessionID - Session to scan
|
|
291
|
+
* @param limit - Max messages to fetch (default 100)
|
|
292
|
+
* @returns Extracted swarm state
|
|
293
|
+
*/
|
|
294
|
+
export async function scanSessionMessages(
|
|
295
|
+
client: OpencodeClient,
|
|
296
|
+
sessionID: string,
|
|
297
|
+
limit: number = 100,
|
|
298
|
+
): Promise<ScannedSwarmState> {
|
|
299
|
+
const state: ScannedSwarmState = {
|
|
300
|
+
subtasks: new Map(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (!client) {
|
|
304
|
+
return state;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
// SDK client uses Options-based API: { path: { id }, query: { limit } }
|
|
309
|
+
const sdkClient = client as {
|
|
310
|
+
session: {
|
|
311
|
+
messages: (opts: {
|
|
312
|
+
path: { id: string };
|
|
313
|
+
query?: { limit?: number };
|
|
314
|
+
}) => Promise<{ data?: Array<{ info: unknown; parts: ToolPart[] }> }>;
|
|
315
|
+
};
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const response = await sdkClient.session.messages({
|
|
319
|
+
path: { id: sessionID },
|
|
320
|
+
query: { limit },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const messages = response.data || [];
|
|
324
|
+
|
|
325
|
+
for (const message of messages) {
|
|
326
|
+
for (const part of message.parts) {
|
|
327
|
+
if (part.type !== "tool" || part.state.status !== "completed") {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { tool, state: toolState } = part;
|
|
332
|
+
const { input, output, time } = toolState as Extract<
|
|
333
|
+
ToolState,
|
|
334
|
+
{ status: "completed" }
|
|
335
|
+
>;
|
|
336
|
+
|
|
337
|
+
// Track last action
|
|
338
|
+
state.lastAction = {
|
|
339
|
+
tool,
|
|
340
|
+
args: input,
|
|
341
|
+
timestamp: time.end,
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// Extract swarm state based on tool type
|
|
345
|
+
switch (tool) {
|
|
346
|
+
case "hive_create_epic": {
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(output);
|
|
349
|
+
if (parsed.epic?.id) {
|
|
350
|
+
state.epicId = parsed.epic.id;
|
|
351
|
+
}
|
|
352
|
+
if (input.epic_title && typeof input.epic_title === "string") {
|
|
353
|
+
state.epicTitle = input.epic_title;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
// Invalid JSON, skip
|
|
357
|
+
}
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
case "swarmmail_init": {
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(output);
|
|
364
|
+
if (parsed.agent_name) {
|
|
365
|
+
state.agentName = parsed.agent_name;
|
|
366
|
+
}
|
|
367
|
+
if (parsed.project_key) {
|
|
368
|
+
state.projectPath = parsed.project_key;
|
|
369
|
+
}
|
|
370
|
+
} catch {
|
|
371
|
+
// Invalid JSON, skip
|
|
372
|
+
}
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
case "swarm_spawn_subtask": {
|
|
377
|
+
const beadId = input.bead_id as string | undefined;
|
|
378
|
+
const epicId = input.epic_id as string | undefined;
|
|
379
|
+
const title = input.subtask_title as string | undefined;
|
|
380
|
+
const files = input.files as string[] | undefined;
|
|
381
|
+
|
|
382
|
+
if (beadId && title) {
|
|
383
|
+
let worker: string | undefined;
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(output);
|
|
386
|
+
worker = parsed.worker;
|
|
387
|
+
} catch {
|
|
388
|
+
// No worker in output
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
state.subtasks.set(beadId, {
|
|
392
|
+
title,
|
|
393
|
+
status: "spawned",
|
|
394
|
+
worker,
|
|
395
|
+
files,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
if (epicId && !state.epicId) {
|
|
399
|
+
state.epicId = epicId;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case "swarm_complete": {
|
|
406
|
+
const beadId = input.bead_id as string | undefined;
|
|
407
|
+
if (beadId && state.subtasks.has(beadId)) {
|
|
408
|
+
const existing = state.subtasks.get(beadId)!;
|
|
409
|
+
state.subtasks.set(beadId, {
|
|
410
|
+
...existing,
|
|
411
|
+
status: "completed",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case "swarm_status": {
|
|
418
|
+
const epicId = input.epic_id as string | undefined;
|
|
419
|
+
if (epicId && !state.epicId) {
|
|
420
|
+
state.epicId = epicId;
|
|
421
|
+
}
|
|
422
|
+
const projectKey = input.project_key as string | undefined;
|
|
423
|
+
if (projectKey && !state.projectPath) {
|
|
424
|
+
state.projectPath = projectKey;
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
getLog().debug(
|
|
433
|
+
{
|
|
434
|
+
error: error instanceof Error ? error.message : String(error),
|
|
435
|
+
},
|
|
436
|
+
"SDK message scanning failed",
|
|
437
|
+
);
|
|
438
|
+
// SDK not available or error fetching messages - return what we have
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return state;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Build dynamic swarm state from scanned messages (more precise than hive detection)
|
|
446
|
+
*/
|
|
447
|
+
function buildDynamicSwarmStateFromScanned(
|
|
448
|
+
scanned: ScannedSwarmState,
|
|
449
|
+
detected: SwarmState,
|
|
450
|
+
): string {
|
|
451
|
+
const parts: string[] = [];
|
|
452
|
+
|
|
453
|
+
parts.push("## 🐝 Current Swarm State\n");
|
|
454
|
+
|
|
455
|
+
// Prefer scanned data over detected
|
|
456
|
+
const epicId = scanned.epicId || detected.epicId;
|
|
457
|
+
const epicTitle = scanned.epicTitle || detected.epicTitle;
|
|
458
|
+
const projectPath = scanned.projectPath || detected.projectPath;
|
|
459
|
+
|
|
460
|
+
if (epicId) {
|
|
461
|
+
parts.push(`**Epic:** ${epicId}${epicTitle ? ` - ${epicTitle}` : ""}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (scanned.agentName) {
|
|
465
|
+
parts.push(`**Coordinator:** ${scanned.agentName}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
parts.push(`**Project:** ${projectPath}`);
|
|
469
|
+
|
|
470
|
+
// Show detailed subtask info from scanned state
|
|
471
|
+
if (scanned.subtasks.size > 0) {
|
|
472
|
+
parts.push(`\n**Subtasks:**`);
|
|
473
|
+
for (const [id, subtask] of scanned.subtasks) {
|
|
474
|
+
const status = subtask.status === "completed" ? "✓" : `[${subtask.status}]`;
|
|
475
|
+
const worker = subtask.worker ? ` → ${subtask.worker}` : "";
|
|
476
|
+
const files = subtask.files?.length ? ` (${subtask.files.join(", ")})` : "";
|
|
477
|
+
parts.push(` - ${id}: ${subtask.title} ${status}${worker}${files}`);
|
|
478
|
+
}
|
|
479
|
+
} else if (detected.subtasks) {
|
|
480
|
+
// Fall back to counts from hive detection
|
|
481
|
+
const total =
|
|
482
|
+
detected.subtasks.closed +
|
|
483
|
+
detected.subtasks.in_progress +
|
|
484
|
+
detected.subtasks.open +
|
|
485
|
+
detected.subtasks.blocked;
|
|
486
|
+
|
|
487
|
+
if (total > 0) {
|
|
488
|
+
parts.push(`**Subtasks:**`);
|
|
489
|
+
if (detected.subtasks.closed > 0)
|
|
490
|
+
parts.push(` - ${detected.subtasks.closed} closed`);
|
|
491
|
+
if (detected.subtasks.in_progress > 0)
|
|
492
|
+
parts.push(` - ${detected.subtasks.in_progress} in_progress`);
|
|
493
|
+
if (detected.subtasks.open > 0)
|
|
494
|
+
parts.push(` - ${detected.subtasks.open} open`);
|
|
495
|
+
if (detected.subtasks.blocked > 0)
|
|
496
|
+
parts.push(` - ${detected.subtasks.blocked} blocked`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Show last action if available
|
|
501
|
+
if (scanned.lastAction) {
|
|
502
|
+
parts.push(`\n**Last Action:** \`${scanned.lastAction.tool}\``);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (epicId) {
|
|
506
|
+
parts.push(`\n## 🎯 YOU ARE THE COORDINATOR`);
|
|
507
|
+
parts.push(``);
|
|
508
|
+
parts.push(
|
|
509
|
+
`**Primary role:** Orchestrate workers, review their output, unblock dependencies.`,
|
|
510
|
+
);
|
|
511
|
+
parts.push(`**Spawn workers** for implementation tasks - don't do them yourself.`);
|
|
512
|
+
parts.push(``);
|
|
513
|
+
parts.push(`**RESUME STEPS:**`);
|
|
514
|
+
parts.push(
|
|
515
|
+
`1. Check swarm status: \`swarm_status(epic_id="${epicId}", project_key="${projectPath}")\``,
|
|
516
|
+
);
|
|
517
|
+
parts.push(`2. Check inbox for worker messages: \`swarmmail_inbox(limit=5)\``);
|
|
518
|
+
parts.push(
|
|
519
|
+
`3. For in_progress subtasks: Review worker results with \`swarm_review\``,
|
|
520
|
+
);
|
|
521
|
+
parts.push(`4. For open subtasks: Spawn workers with \`swarm_spawn_subtask\``);
|
|
522
|
+
parts.push(`5. For blocked subtasks: Investigate and unblock`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return parts.join("\n");
|
|
526
|
+
}
|
|
527
|
+
|
|
219
528
|
// ============================================================================
|
|
220
529
|
// Swarm Detection
|
|
221
530
|
// ============================================================================
|
|
@@ -481,17 +790,21 @@ async function detectSwarm(): Promise<SwarmDetection> {
|
|
|
481
790
|
* Philosophy: Err on the side of continuation. A false positive costs
|
|
482
791
|
* a bit of context space. A false negative loses the swarm.
|
|
483
792
|
*
|
|
793
|
+
* @param client - Optional OpenCode SDK client for scanning session messages.
|
|
794
|
+
* When provided, extracts PRECISE swarm state from actual tool calls.
|
|
795
|
+
* When undefined, falls back to hive/swarm-mail heuristic detection.
|
|
796
|
+
*
|
|
484
797
|
* @example
|
|
485
798
|
* ```typescript
|
|
486
799
|
* import { createCompactionHook } from "opencode-swarm-plugin";
|
|
487
800
|
*
|
|
488
|
-
* export const SwarmPlugin: Plugin = async () => ({
|
|
801
|
+
* export const SwarmPlugin: Plugin = async (input) => ({
|
|
489
802
|
* tool: { ... },
|
|
490
|
-
* "experimental.session.compacting": createCompactionHook(),
|
|
803
|
+
* "experimental.session.compacting": createCompactionHook(input.client),
|
|
491
804
|
* });
|
|
492
805
|
* ```
|
|
493
806
|
*/
|
|
494
|
-
export function createCompactionHook() {
|
|
807
|
+
export function createCompactionHook(client?: OpencodeClient) {
|
|
495
808
|
return async (
|
|
496
809
|
input: { sessionID: string },
|
|
497
810
|
output: { context: string[] },
|
|
@@ -502,41 +815,73 @@ export function createCompactionHook() {
|
|
|
502
815
|
{
|
|
503
816
|
session_id: input.sessionID,
|
|
504
817
|
trigger: "session_compaction",
|
|
818
|
+
has_sdk_client: !!client,
|
|
505
819
|
},
|
|
506
820
|
"compaction started",
|
|
507
821
|
);
|
|
508
822
|
|
|
509
823
|
try {
|
|
824
|
+
// Scan session messages for precise swarm state (if client available)
|
|
825
|
+
const scannedState = await scanSessionMessages(client, input.sessionID);
|
|
826
|
+
|
|
827
|
+
// Also run heuristic detection from hive/swarm-mail
|
|
510
828
|
const detection = await detectSwarm();
|
|
511
829
|
|
|
830
|
+
// Boost confidence if we found swarm evidence in session messages
|
|
831
|
+
let effectiveConfidence = detection.confidence;
|
|
832
|
+
if (scannedState.epicId || scannedState.subtasks.size > 0) {
|
|
833
|
+
// Session messages show swarm activity - this is HIGH confidence
|
|
834
|
+
if (effectiveConfidence === "none" || effectiveConfidence === "low") {
|
|
835
|
+
effectiveConfidence = "medium";
|
|
836
|
+
detection.reasons.push("swarm tool calls found in session");
|
|
837
|
+
}
|
|
838
|
+
if (scannedState.subtasks.size > 0) {
|
|
839
|
+
effectiveConfidence = "high";
|
|
840
|
+
detection.reasons.push(`${scannedState.subtasks.size} subtasks spawned`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
512
844
|
if (
|
|
513
|
-
|
|
514
|
-
|
|
845
|
+
effectiveConfidence === "high" ||
|
|
846
|
+
effectiveConfidence === "medium"
|
|
515
847
|
) {
|
|
516
848
|
// Definite or probable swarm - inject full context
|
|
517
849
|
const header = `[Swarm detected: ${detection.reasons.join(", ")}]\n\n`;
|
|
518
|
-
|
|
519
|
-
// Build dynamic state section
|
|
850
|
+
|
|
851
|
+
// Build dynamic state section - prefer scanned state (ground truth) over detected
|
|
520
852
|
let dynamicState = "";
|
|
521
|
-
if (
|
|
853
|
+
if (scannedState.epicId || scannedState.subtasks.size > 0) {
|
|
854
|
+
// Use scanned state (more precise)
|
|
855
|
+
dynamicState =
|
|
856
|
+
buildDynamicSwarmStateFromScanned(
|
|
857
|
+
scannedState,
|
|
858
|
+
detection.state || {
|
|
859
|
+
projectPath: scannedState.projectPath || process.cwd(),
|
|
860
|
+
subtasks: { closed: 0, in_progress: 0, open: 0, blocked: 0 },
|
|
861
|
+
},
|
|
862
|
+
) + "\n\n";
|
|
863
|
+
} else if (detection.state && detection.state.epicId) {
|
|
864
|
+
// Fall back to hive-detected state
|
|
522
865
|
dynamicState = buildDynamicSwarmState(detection.state) + "\n\n";
|
|
523
866
|
}
|
|
524
|
-
|
|
867
|
+
|
|
525
868
|
const contextContent = header + dynamicState + SWARM_COMPACTION_CONTEXT;
|
|
526
869
|
output.context.push(contextContent);
|
|
527
870
|
|
|
528
871
|
getLog().info(
|
|
529
872
|
{
|
|
530
|
-
confidence:
|
|
873
|
+
confidence: effectiveConfidence,
|
|
531
874
|
context_length: contextContent.length,
|
|
532
875
|
context_type: "full",
|
|
533
876
|
reasons: detection.reasons,
|
|
534
877
|
has_dynamic_state: !!dynamicState,
|
|
535
|
-
epic_id: detection.state?.epicId,
|
|
878
|
+
epic_id: scannedState.epicId || detection.state?.epicId,
|
|
879
|
+
scanned_subtasks: scannedState.subtasks.size,
|
|
880
|
+
scanned_agent: scannedState.agentName,
|
|
536
881
|
},
|
|
537
882
|
"injected swarm context",
|
|
538
883
|
);
|
|
539
|
-
} else if (
|
|
884
|
+
} else if (effectiveConfidence === "low") {
|
|
540
885
|
// Possible swarm - inject fallback detection prompt
|
|
541
886
|
const header = `[Possible swarm: ${detection.reasons.join(", ")}]\n\n`;
|
|
542
887
|
const contextContent = header + SWARM_DETECTION_FALLBACK;
|
|
@@ -544,7 +889,7 @@ export function createCompactionHook() {
|
|
|
544
889
|
|
|
545
890
|
getLog().info(
|
|
546
891
|
{
|
|
547
|
-
confidence:
|
|
892
|
+
confidence: effectiveConfidence,
|
|
548
893
|
context_length: contextContent.length,
|
|
549
894
|
context_type: "fallback",
|
|
550
895
|
reasons: detection.reasons,
|
|
@@ -554,7 +899,7 @@ export function createCompactionHook() {
|
|
|
554
899
|
} else {
|
|
555
900
|
getLog().debug(
|
|
556
901
|
{
|
|
557
|
-
confidence:
|
|
902
|
+
confidence: effectiveConfidence,
|
|
558
903
|
context_type: "none",
|
|
559
904
|
},
|
|
560
905
|
"no swarm detected, skipping injection",
|
|
@@ -567,8 +912,8 @@ export function createCompactionHook() {
|
|
|
567
912
|
{
|
|
568
913
|
duration_ms: duration,
|
|
569
914
|
success: true,
|
|
570
|
-
detected: detection.detected,
|
|
571
|
-
confidence:
|
|
915
|
+
detected: detection.detected || scannedState.epicId !== undefined,
|
|
916
|
+
confidence: effectiveConfidence,
|
|
572
917
|
context_injected: output.context.length > 0,
|
|
573
918
|
},
|
|
574
919
|
"compaction complete",
|
package/src/eval-capture.ts
CHANGED
|
@@ -63,8 +63,8 @@ export const EvalRecordSchema = z.object({
|
|
|
63
63
|
context: z.string().optional(),
|
|
64
64
|
/** Strategy used for decomposition */
|
|
65
65
|
strategy: z.enum(["file-based", "feature-based", "risk-based", "auto"]),
|
|
66
|
-
/**
|
|
67
|
-
|
|
66
|
+
/** Number of subtasks generated */
|
|
67
|
+
subtask_count: z.number().int().min(1),
|
|
68
68
|
|
|
69
69
|
// OUTPUT (the decomposition)
|
|
70
70
|
/** Epic title */
|
|
@@ -238,7 +238,6 @@ export function captureDecomposition(params: {
|
|
|
238
238
|
task: string;
|
|
239
239
|
context?: string;
|
|
240
240
|
strategy: "file-based" | "feature-based" | "risk-based" | "auto";
|
|
241
|
-
maxSubtasks: number;
|
|
242
241
|
epicTitle: string;
|
|
243
242
|
epicDescription?: string;
|
|
244
243
|
subtasks: Array<{
|
|
@@ -256,7 +255,7 @@ export function captureDecomposition(params: {
|
|
|
256
255
|
task: params.task,
|
|
257
256
|
context: params.context,
|
|
258
257
|
strategy: params.strategy,
|
|
259
|
-
|
|
258
|
+
subtask_count: params.subtasks.length,
|
|
260
259
|
epic_title: params.epicTitle,
|
|
261
260
|
epic_description: params.epicDescription,
|
|
262
261
|
subtasks: params.subtasks,
|
|
@@ -409,7 +408,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
409
408
|
input: { task: string; context?: string };
|
|
410
409
|
expected: {
|
|
411
410
|
minSubtasks: number;
|
|
412
|
-
|
|
411
|
+
subtaskCount: number;
|
|
413
412
|
requiredFiles?: string[];
|
|
414
413
|
overallSuccess?: boolean;
|
|
415
414
|
};
|
|
@@ -426,7 +425,7 @@ export function exportForEvalite(projectPath: string): Array<{
|
|
|
426
425
|
},
|
|
427
426
|
expected: {
|
|
428
427
|
minSubtasks: 2,
|
|
429
|
-
|
|
428
|
+
subtaskCount: record.subtask_count,
|
|
430
429
|
requiredFiles: record.subtasks.flatMap((s) => s.files),
|
|
431
430
|
overallSuccess: record.overall_success,
|
|
432
431
|
},
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
analyzeTodoWrite,
|
|
59
59
|
shouldAnalyzeTool,
|
|
60
60
|
} from "./planning-guardrails";
|
|
61
|
+
import { createCompactionHook } from "./compaction-hook";
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
64
|
* OpenCode Swarm Plugin
|
|
@@ -80,7 +81,7 @@ import {
|
|
|
80
81
|
export const SwarmPlugin: Plugin = async (
|
|
81
82
|
input: PluginInput,
|
|
82
83
|
): Promise<Hooks> => {
|
|
83
|
-
const { $, directory } = input;
|
|
84
|
+
const { $, directory, client } = input;
|
|
84
85
|
|
|
85
86
|
// Set the working directory for hive commands
|
|
86
87
|
// This ensures hive operations run in the project directory, not ~/.config/opencode
|
|
@@ -261,6 +262,25 @@ export const SwarmPlugin: Plugin = async (
|
|
|
261
262
|
// Auto-sync was removed because bd CLI is deprecated
|
|
262
263
|
// The hive_sync tool handles flushing to JSONL and git commit/push
|
|
263
264
|
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compaction hook for swarm context preservation
|
|
268
|
+
*
|
|
269
|
+
* When OpenCode compacts session context, this hook injects swarm state
|
|
270
|
+
* to ensure coordinators can resume orchestration seamlessly.
|
|
271
|
+
*
|
|
272
|
+
* Uses SDK client to scan actual session messages for precise swarm state
|
|
273
|
+
* (epic IDs, subtask status, agent names) rather than relying solely on
|
|
274
|
+
* heuristic detection from hive/swarm-mail.
|
|
275
|
+
*
|
|
276
|
+
* Note: This hook is experimental and may not be in the published Hooks type yet.
|
|
277
|
+
*/
|
|
278
|
+
"experimental.session.compacting": createCompactionHook(client),
|
|
279
|
+
} as Hooks & {
|
|
280
|
+
"experimental.session.compacting"?: (
|
|
281
|
+
input: { sessionID: string },
|
|
282
|
+
output: { context: string[] },
|
|
283
|
+
) => Promise<void>;
|
|
264
284
|
};
|
|
265
285
|
};
|
|
266
286
|
|
|
@@ -976,7 +976,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
976
976
|
const result = await swarm_decompose.execute(
|
|
977
977
|
{
|
|
978
978
|
task: "Add user authentication",
|
|
979
|
-
max_subtasks: 3,
|
|
980
979
|
query_cass: true,
|
|
981
980
|
},
|
|
982
981
|
mockContext,
|
|
@@ -992,7 +991,6 @@ describe("Swarm Tool Integrations", () => {
|
|
|
992
991
|
const result = await swarm_decompose.execute(
|
|
993
992
|
{
|
|
994
993
|
task: "Add user authentication",
|
|
995
|
-
max_subtasks: 3,
|
|
996
994
|
query_cass: false,
|
|
997
995
|
},
|
|
998
996
|
mockContext,
|
package/src/schemas/task.ts
CHANGED
|
@@ -87,7 +87,6 @@ export type TaskDecomposition = z.infer<typeof TaskDecompositionSchema>;
|
|
|
87
87
|
*/
|
|
88
88
|
export const DecomposeArgsSchema = z.object({
|
|
89
89
|
task: z.string().min(1),
|
|
90
|
-
max_subtasks: z.number().int().min(1).default(5),
|
|
91
90
|
context: z.string().optional(),
|
|
92
91
|
});
|
|
93
92
|
export type DecomposeArgs = z.infer<typeof DecomposeArgsSchema>;
|
package/src/swarm-decompose.ts
CHANGED
|
@@ -434,12 +434,6 @@ export const swarm_decompose = tool({
|
|
|
434
434
|
"Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
|
|
435
435
|
args: {
|
|
436
436
|
task: tool.schema.string().min(1).describe("Task description to decompose"),
|
|
437
|
-
max_subtasks: tool.schema
|
|
438
|
-
.number()
|
|
439
|
-
.int()
|
|
440
|
-
.min(1)
|
|
441
|
-
.optional()
|
|
442
|
-
.describe("Suggested max subtasks (optional - LLM decides if not specified)"),
|
|
443
437
|
context: tool.schema
|
|
444
438
|
.string()
|
|
445
439
|
.optional()
|
|
@@ -503,7 +497,6 @@ export const swarm_decompose = tool({
|
|
|
503
497
|
: "## Additional Context\n(none provided)";
|
|
504
498
|
|
|
505
499
|
const prompt = DECOMPOSITION_PROMPT.replace("{task}", args.task)
|
|
506
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString())
|
|
507
500
|
.replace("{context_section}", contextSection);
|
|
508
501
|
|
|
509
502
|
// Return the prompt and schema info for the caller
|
|
@@ -697,12 +690,6 @@ export const swarm_delegate_planning = tool({
|
|
|
697
690
|
.string()
|
|
698
691
|
.optional()
|
|
699
692
|
.describe("Additional context to include"),
|
|
700
|
-
max_subtasks: tool.schema
|
|
701
|
-
.number()
|
|
702
|
-
.int()
|
|
703
|
-
.min(1)
|
|
704
|
-
.optional()
|
|
705
|
-
.describe("Suggested max subtasks (optional - LLM decides if not specified)"),
|
|
706
693
|
strategy: tool.schema
|
|
707
694
|
.enum(["auto", "file-based", "feature-based", "risk-based"])
|
|
708
695
|
.optional()
|
|
@@ -804,8 +791,7 @@ export const swarm_delegate_planning = tool({
|
|
|
804
791
|
.replace("{strategy_guidelines}", strategyGuidelines)
|
|
805
792
|
.replace("{context_section}", contextSection)
|
|
806
793
|
.replace("{cass_history}", cassContext || "")
|
|
807
|
-
.replace("{skills_context}", skillsContext || "")
|
|
808
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
|
|
794
|
+
.replace("{skills_context}", skillsContext || "");
|
|
809
795
|
|
|
810
796
|
// Add strict JSON-only instructions for the subagent
|
|
811
797
|
const subagentInstructions = `
|
package/src/swarm-prompts.ts
CHANGED
|
@@ -1393,12 +1393,6 @@ export const swarm_plan_prompt = tool({
|
|
|
1393
1393
|
.enum(["file-based", "feature-based", "risk-based", "auto"])
|
|
1394
1394
|
.optional()
|
|
1395
1395
|
.describe("Decomposition strategy (default: auto-detect)"),
|
|
1396
|
-
max_subtasks: tool.schema
|
|
1397
|
-
.number()
|
|
1398
|
-
.int()
|
|
1399
|
-
.min(1)
|
|
1400
|
-
.optional()
|
|
1401
|
-
.describe("Suggested max subtasks (optional - LLM decides if not specified)"),
|
|
1402
1396
|
context: tool.schema
|
|
1403
1397
|
.string()
|
|
1404
1398
|
.optional()
|
|
@@ -1482,8 +1476,7 @@ export const swarm_plan_prompt = tool({
|
|
|
1482
1476
|
.replace("{strategy_guidelines}", strategyGuidelines)
|
|
1483
1477
|
.replace("{context_section}", contextSection)
|
|
1484
1478
|
.replace("{cass_history}", "") // Empty for now
|
|
1485
|
-
.replace("{skills_context}", skillsContext || "")
|
|
1486
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
|
|
1479
|
+
.replace("{skills_context}", skillsContext || "");
|
|
1487
1480
|
|
|
1488
1481
|
return JSON.stringify(
|
|
1489
1482
|
{
|