opencode-swarm-plugin 0.18.0 → 0.19.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.
@@ -0,0 +1,912 @@
1
+ /**
2
+ * Swarm Decompose Module - Task decomposition and validation
3
+ *
4
+ * Handles breaking tasks into parallelizable subtasks with file assignments,
5
+ * validates decomposition structure, and detects conflicts.
6
+ *
7
+ * Key responsibilities:
8
+ * - Decomposition prompt generation
9
+ * - BeadTree validation
10
+ * - File conflict detection
11
+ * - Instruction conflict detection
12
+ * - Delegation to planner subagents
13
+ */
14
+
15
+ import { tool } from "@opencode-ai/plugin";
16
+ import { z } from "zod";
17
+ import { BeadTreeSchema } from "./schemas";
18
+ import {
19
+ POSITIVE_MARKERS,
20
+ NEGATIVE_MARKERS,
21
+ type DecompositionStrategy,
22
+ } from "./swarm-strategies";
23
+
24
+ // ============================================================================
25
+ // Decomposition Prompt (temporary - will be moved to swarm-prompts.ts)
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Prompt for decomposing a task into parallelizable subtasks.
30
+ *
31
+ * Used by swarm_decompose to instruct the agent on how to break down work.
32
+ * The agent responds with a BeadTree that gets validated.
33
+ */
34
+ const DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
35
+
36
+ ## Task
37
+ {task}
38
+
39
+ {context_section}
40
+
41
+ ## MANDATORY: Beads Issue Tracking
42
+
43
+ **Every subtask MUST become a bead.** This is non-negotiable.
44
+
45
+ After decomposition, the coordinator will:
46
+ 1. Create an epic bead for the overall task
47
+ 2. Create child beads for each subtask
48
+ 3. Track progress through bead status updates
49
+ 4. Close beads with summaries when complete
50
+
51
+ Agents MUST update their bead status as they work. No silent progress.
52
+
53
+ ## Requirements
54
+
55
+ 1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
56
+ 2. **Assign files** - each subtask must specify which files it will modify
57
+ 3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
58
+ 4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
59
+ 5. **Estimate complexity** - 1 (trivial) to 5 (complex)
60
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
61
+
62
+ ## Response Format
63
+
64
+ Respond with a JSON object matching this schema:
65
+
66
+ \`\`\`typescript
67
+ {
68
+ epic: {
69
+ title: string, // Epic title for the beads tracker
70
+ description?: string // Brief description of the overall goal
71
+ },
72
+ subtasks: [
73
+ {
74
+ title: string, // What this subtask accomplishes
75
+ description?: string, // Detailed instructions for the agent
76
+ files: string[], // Files this subtask will modify (globs allowed)
77
+ dependencies: number[], // Indices of subtasks this depends on (0-indexed)
78
+ estimated_complexity: 1-5 // Effort estimate
79
+ },
80
+ // ... more subtasks
81
+ ]
82
+ }
83
+ \`\`\`
84
+
85
+ ## Guidelines
86
+
87
+ - **Plan aggressively** - when in doubt, split further. 3 small tasks > 1 medium task
88
+ - **Prefer smaller, focused subtasks** over large complex ones
89
+ - **Include test files** in the same subtask as the code they test
90
+ - **Consider shared types** - if multiple files share types, handle that first
91
+ - **Think about imports** - changes to exported APIs affect downstream files
92
+ - **Explicit > implicit** - spell out what each subtask should do, don't assume
93
+
94
+ ## File Assignment Examples
95
+
96
+ - Schema change: \`["src/schemas/user.ts", "src/schemas/index.ts"]\`
97
+ - Component + test: \`["src/components/Button.tsx", "src/components/Button.test.tsx"]\`
98
+ - API route: \`["src/app/api/users/route.ts"]\`
99
+
100
+ Now decompose the task:`;
101
+
102
+ /**
103
+ * Strategy-specific decomposition prompt template
104
+ */
105
+ const STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
106
+
107
+ ## Task
108
+ {task}
109
+
110
+ {strategy_guidelines}
111
+
112
+ {context_section}
113
+
114
+ {cass_history}
115
+
116
+ {skills_context}
117
+
118
+ ## MANDATORY: Beads Issue Tracking
119
+
120
+ **Every subtask MUST become a bead.** This is non-negotiable.
121
+
122
+ After decomposition, the coordinator will:
123
+ 1. Create an epic bead for the overall task
124
+ 2. Create child beads for each subtask
125
+ 3. Track progress through bead status updates
126
+ 4. Close beads with summaries when complete
127
+
128
+ Agents MUST update their bead status as they work. No silent progress.
129
+
130
+ ## Requirements
131
+
132
+ 1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
133
+ 2. **Assign files** - each subtask must specify which files it will modify
134
+ 3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
135
+ 4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
136
+ 5. **Estimate complexity** - 1 (trivial) to 5 (complex)
137
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
138
+
139
+ ## Response Format
140
+
141
+ Respond with a JSON object matching this schema:
142
+
143
+ \`\`\`typescript
144
+ {
145
+ epic: {
146
+ title: string, // Epic title for the beads tracker
147
+ description?: string // Brief description of the overall goal
148
+ },
149
+ subtasks: [
150
+ {
151
+ title: string, // What this subtask accomplishes
152
+ description?: string, // Detailed instructions for the agent
153
+ files: string[], // Files this subtask will modify (globs allowed)
154
+ dependencies: number[], // Indices of subtasks this depends on (0-indexed)
155
+ estimated_complexity: 1-5 // Effort estimate
156
+ },
157
+ // ... more subtasks
158
+ ]
159
+ }
160
+ \`\`\`
161
+
162
+ Now decompose the task:`;
163
+
164
+ // ============================================================================
165
+ // Conflict Detection
166
+ // ============================================================================
167
+
168
+ /**
169
+ * A detected conflict between subtask instructions
170
+ */
171
+ export interface InstructionConflict {
172
+ subtask_a: number;
173
+ subtask_b: number;
174
+ directive_a: string;
175
+ directive_b: string;
176
+ conflict_type: "positive_negative" | "contradictory";
177
+ description: string;
178
+ }
179
+
180
+ /**
181
+ * Extract directives from text based on marker words
182
+ */
183
+ function extractDirectives(text: string): {
184
+ positive: string[];
185
+ negative: string[];
186
+ } {
187
+ const sentences = text.split(/[.!?\n]+/).map((s) => s.trim().toLowerCase());
188
+ const positive: string[] = [];
189
+ const negative: string[] = [];
190
+
191
+ for (const sentence of sentences) {
192
+ if (!sentence) continue;
193
+
194
+ const hasPositive = POSITIVE_MARKERS.some((m) => sentence.includes(m));
195
+ const hasNegative = NEGATIVE_MARKERS.some((m) => sentence.includes(m));
196
+
197
+ if (hasPositive && !hasNegative) {
198
+ positive.push(sentence);
199
+ } else if (hasNegative) {
200
+ negative.push(sentence);
201
+ }
202
+ }
203
+
204
+ return { positive, negative };
205
+ }
206
+
207
+ /**
208
+ * Check if two directives conflict
209
+ *
210
+ * Simple heuristic: look for common subjects with opposite polarity
211
+ */
212
+ function directivesConflict(positive: string, negative: string): boolean {
213
+ // Extract key nouns/concepts (simple word overlap check)
214
+ const positiveWords = new Set(
215
+ positive.split(/\s+/).filter((w) => w.length > 3),
216
+ );
217
+ const negativeWords = negative.split(/\s+/).filter((w) => w.length > 3);
218
+
219
+ // If they share significant words, they might conflict
220
+ const overlap = negativeWords.filter((w) => positiveWords.has(w));
221
+ return overlap.length >= 2;
222
+ }
223
+
224
+ /**
225
+ * Detect conflicts between subtask instructions
226
+ *
227
+ * Looks for cases where one subtask says "always use X" and another says "avoid X".
228
+ *
229
+ * @param subtasks - Array of subtask descriptions
230
+ * @returns Array of detected conflicts
231
+ *
232
+ * @see https://github.com/Dicklesworthstone/cass_memory_system/blob/main/src/curate.ts#L36-L89
233
+ */
234
+ export function detectInstructionConflicts(
235
+ subtasks: Array<{ title: string; description?: string }>,
236
+ ): InstructionConflict[] {
237
+ const conflicts: InstructionConflict[] = [];
238
+
239
+ // Extract directives from each subtask
240
+ const subtaskDirectives = subtasks.map((s, i) => ({
241
+ index: i,
242
+ title: s.title,
243
+ ...extractDirectives(`${s.title} ${s.description || ""}`),
244
+ }));
245
+
246
+ // Compare each pair of subtasks
247
+ for (let i = 0; i < subtaskDirectives.length; i++) {
248
+ for (let j = i + 1; j < subtaskDirectives.length; j++) {
249
+ const a = subtaskDirectives[i];
250
+ const b = subtaskDirectives[j];
251
+
252
+ // Check if A's positive conflicts with B's negative
253
+ for (const posA of a.positive) {
254
+ for (const negB of b.negative) {
255
+ if (directivesConflict(posA, negB)) {
256
+ conflicts.push({
257
+ subtask_a: i,
258
+ subtask_b: j,
259
+ directive_a: posA,
260
+ directive_b: negB,
261
+ conflict_type: "positive_negative",
262
+ description: `Subtask ${i} says "${posA}" but subtask ${j} says "${negB}"`,
263
+ });
264
+ }
265
+ }
266
+ }
267
+
268
+ // Check if B's positive conflicts with A's negative
269
+ for (const posB of b.positive) {
270
+ for (const negA of a.negative) {
271
+ if (directivesConflict(posB, negA)) {
272
+ conflicts.push({
273
+ subtask_a: j,
274
+ subtask_b: i,
275
+ directive_a: posB,
276
+ directive_b: negA,
277
+ conflict_type: "positive_negative",
278
+ description: `Subtask ${j} says "${posB}" but subtask ${i} says "${negA}"`,
279
+ });
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
286
+ return conflicts;
287
+ }
288
+
289
+ /**
290
+ * Detect file conflicts in a bead tree
291
+ *
292
+ * @param subtasks - Array of subtasks with file assignments
293
+ * @returns Array of files that appear in multiple subtasks
294
+ */
295
+ export function detectFileConflicts(
296
+ subtasks: Array<{ files: string[] }>,
297
+ ): string[] {
298
+ const allFiles = new Map<string, number>();
299
+ const conflicts: string[] = [];
300
+
301
+ for (const subtask of subtasks) {
302
+ for (const file of subtask.files) {
303
+ const count = allFiles.get(file) || 0;
304
+ allFiles.set(file, count + 1);
305
+ if (count === 1) {
306
+ // Second occurrence - it's a conflict
307
+ conflicts.push(file);
308
+ }
309
+ }
310
+ }
311
+
312
+ return conflicts;
313
+ }
314
+
315
+ // ============================================================================
316
+ // CASS History Integration
317
+ // ============================================================================
318
+
319
+ /**
320
+ * CASS search result from similar past tasks
321
+ */
322
+ interface CassSearchResult {
323
+ query: string;
324
+ results: Array<{
325
+ source_path: string;
326
+ line: number;
327
+ agent: string;
328
+ preview: string;
329
+ score: number;
330
+ }>;
331
+ }
332
+
333
+ /**
334
+ * CASS query result with status
335
+ */
336
+ type CassQueryResult =
337
+ | { status: "unavailable" }
338
+ | { status: "failed"; error?: string }
339
+ | { status: "empty"; query: string }
340
+ | { status: "success"; data: CassSearchResult };
341
+
342
+ /**
343
+ * Query CASS for similar past tasks
344
+ *
345
+ * @param task - Task description to search for
346
+ * @param limit - Maximum results to return
347
+ * @returns Structured result with status indicator
348
+ */
349
+ async function queryCassHistory(
350
+ task: string,
351
+ limit: number = 3,
352
+ ): Promise<CassQueryResult> {
353
+ // Check if CASS is available
354
+ try {
355
+ const result = await Bun.$`cass search ${task} --limit ${limit} --json`
356
+ .quiet()
357
+ .nothrow();
358
+
359
+ if (result.exitCode !== 0) {
360
+ const error = result.stderr.toString();
361
+ console.warn(
362
+ `[swarm] CASS search failed (exit ${result.exitCode}):`,
363
+ error,
364
+ );
365
+ return { status: "failed", error };
366
+ }
367
+
368
+ const output = result.stdout.toString();
369
+ if (!output.trim()) {
370
+ return { status: "empty", query: task };
371
+ }
372
+
373
+ try {
374
+ const parsed = JSON.parse(output);
375
+ const searchResult: CassSearchResult = {
376
+ query: task,
377
+ results: Array.isArray(parsed) ? parsed : parsed.results || [],
378
+ };
379
+
380
+ if (searchResult.results.length === 0) {
381
+ return { status: "empty", query: task };
382
+ }
383
+
384
+ return { status: "success", data: searchResult };
385
+ } catch (error) {
386
+ console.warn(`[swarm] Failed to parse CASS output:`, error);
387
+ return { status: "failed", error: String(error) };
388
+ }
389
+ } catch (error) {
390
+ console.error(`[swarm] CASS query error:`, error);
391
+ return { status: "unavailable" };
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Format CASS history for inclusion in decomposition prompt
397
+ */
398
+ function formatCassHistoryForPrompt(history: CassSearchResult): string {
399
+ if (history.results.length === 0) {
400
+ return "";
401
+ }
402
+
403
+ const lines = [
404
+ "## Similar Past Tasks",
405
+ "",
406
+ "These similar tasks were found in agent history:",
407
+ "",
408
+ ...history.results.slice(0, 3).map((r, i) => {
409
+ const preview = r.preview.slice(0, 200).replace(/\n/g, " ");
410
+ return `${i + 1}. [${r.agent}] ${preview}...`;
411
+ }),
412
+ "",
413
+ "Consider patterns that worked in these past tasks.",
414
+ "",
415
+ ];
416
+
417
+ return lines.join("\n");
418
+ }
419
+
420
+ // ============================================================================
421
+ // Tool Definitions
422
+ // ============================================================================
423
+
424
+ /**
425
+ * Decompose a task into a bead tree
426
+ *
427
+ * This is a PROMPT tool - it returns a prompt for the agent to respond to.
428
+ * The agent's response (JSON) should be validated with BeadTreeSchema.
429
+ *
430
+ * Optionally queries CASS for similar past tasks to inform decomposition.
431
+ */
432
+ export const swarm_decompose = tool({
433
+ description:
434
+ "Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
435
+ args: {
436
+ task: tool.schema.string().min(1).describe("Task description to decompose"),
437
+ max_subtasks: tool.schema
438
+ .number()
439
+ .int()
440
+ .min(2)
441
+ .max(10)
442
+ .default(5)
443
+ .describe("Maximum number of subtasks (default: 5)"),
444
+ context: tool.schema
445
+ .string()
446
+ .optional()
447
+ .describe("Additional context (codebase info, constraints, etc.)"),
448
+ query_cass: tool.schema
449
+ .boolean()
450
+ .optional()
451
+ .describe("Query CASS for similar past tasks (default: true)"),
452
+ cass_limit: tool.schema
453
+ .number()
454
+ .int()
455
+ .min(1)
456
+ .max(10)
457
+ .optional()
458
+ .describe("Max CASS results to include (default: 3)"),
459
+ },
460
+ async execute(args) {
461
+ // Import needed modules
462
+ const { formatMemoryQueryForDecomposition } = await import("./learning");
463
+
464
+ // Query CASS for similar past tasks
465
+ let cassContext = "";
466
+ let cassResultInfo: {
467
+ queried: boolean;
468
+ results_found?: number;
469
+ included_in_context?: boolean;
470
+ reason?: string;
471
+ };
472
+
473
+ if (args.query_cass !== false) {
474
+ const cassResult = await queryCassHistory(
475
+ args.task,
476
+ args.cass_limit ?? 3,
477
+ );
478
+ if (cassResult.status === "success") {
479
+ cassContext = formatCassHistoryForPrompt(cassResult.data);
480
+ cassResultInfo = {
481
+ queried: true,
482
+ results_found: cassResult.data.results.length,
483
+ included_in_context: true,
484
+ };
485
+ } else {
486
+ cassResultInfo = {
487
+ queried: true,
488
+ results_found: 0,
489
+ included_in_context: false,
490
+ reason: cassResult.status,
491
+ };
492
+ }
493
+ } else {
494
+ cassResultInfo = { queried: false, reason: "disabled" };
495
+ }
496
+
497
+ // Combine user context with CASS history
498
+ const fullContext = [args.context, cassContext]
499
+ .filter(Boolean)
500
+ .join("\n\n");
501
+
502
+ // Format the decomposition prompt
503
+ const contextSection = fullContext
504
+ ? `## Additional Context\n${fullContext}`
505
+ : "## Additional Context\n(none provided)";
506
+
507
+ const prompt = DECOMPOSITION_PROMPT.replace("{task}", args.task)
508
+ .replace("{max_subtasks}", (args.max_subtasks ?? 5).toString())
509
+ .replace("{context_section}", contextSection);
510
+
511
+ // Return the prompt and schema info for the caller
512
+ return JSON.stringify(
513
+ {
514
+ prompt,
515
+ expected_schema: "BeadTree",
516
+ schema_hint: {
517
+ epic: { title: "string", description: "string?" },
518
+ subtasks: [
519
+ {
520
+ title: "string",
521
+ description: "string?",
522
+ files: "string[]",
523
+ dependencies: "number[]",
524
+ estimated_complexity: "1-5",
525
+ },
526
+ ],
527
+ },
528
+ validation_note:
529
+ "Parse agent response as JSON and validate with BeadTreeSchema from schemas/bead.ts",
530
+ cass_history: cassResultInfo,
531
+ // Add semantic-memory query instruction
532
+ memory_query: formatMemoryQueryForDecomposition(args.task, 3),
533
+ },
534
+ null,
535
+ 2,
536
+ );
537
+ },
538
+ });
539
+
540
+ /**
541
+ * Validate a decomposition response from an agent
542
+ *
543
+ * Use this after the agent responds to swarm:decompose to validate the structure.
544
+ */
545
+ export const swarm_validate_decomposition = tool({
546
+ description: "Validate a decomposition response against BeadTreeSchema",
547
+ args: {
548
+ response: tool.schema
549
+ .string()
550
+ .describe("JSON response from agent (BeadTree format)"),
551
+ },
552
+ async execute(args) {
553
+ try {
554
+ const parsed = JSON.parse(args.response);
555
+ const validated = BeadTreeSchema.parse(parsed);
556
+
557
+ // Additional validation: check for file conflicts
558
+ const conflicts = detectFileConflicts(validated.subtasks);
559
+
560
+ if (conflicts.length > 0) {
561
+ return JSON.stringify(
562
+ {
563
+ valid: false,
564
+ error: `File conflicts detected: ${conflicts.join(", ")}`,
565
+ hint: "Each file can only be assigned to one subtask",
566
+ },
567
+ null,
568
+ 2,
569
+ );
570
+ }
571
+
572
+ // Check dependency indices are valid
573
+ for (let i = 0; i < validated.subtasks.length; i++) {
574
+ const deps = validated.subtasks[i].dependencies;
575
+ for (const dep of deps) {
576
+ // Check bounds first
577
+ if (dep < 0 || dep >= validated.subtasks.length) {
578
+ return JSON.stringify(
579
+ {
580
+ valid: false,
581
+ error: `Invalid dependency: subtask ${i} depends on ${dep}, but only ${validated.subtasks.length} subtasks exist (indices 0-${validated.subtasks.length - 1})`,
582
+ hint: "Dependency index is out of bounds",
583
+ },
584
+ null,
585
+ 2,
586
+ );
587
+ }
588
+ // Check forward references
589
+ if (dep >= i) {
590
+ return JSON.stringify(
591
+ {
592
+ valid: false,
593
+ error: `Invalid dependency: subtask ${i} depends on ${dep}, but dependencies must be earlier in the array`,
594
+ hint: "Reorder subtasks so dependencies come before dependents",
595
+ },
596
+ null,
597
+ 2,
598
+ );
599
+ }
600
+ }
601
+ }
602
+
603
+ // Check for instruction conflicts between subtasks
604
+ const instructionConflicts = detectInstructionConflicts(
605
+ validated.subtasks,
606
+ );
607
+
608
+ return JSON.stringify(
609
+ {
610
+ valid: true,
611
+ bead_tree: validated,
612
+ stats: {
613
+ subtask_count: validated.subtasks.length,
614
+ total_files: new Set(validated.subtasks.flatMap((s) => s.files))
615
+ .size,
616
+ total_complexity: validated.subtasks.reduce(
617
+ (sum, s) => sum + s.estimated_complexity,
618
+ 0,
619
+ ),
620
+ },
621
+ // Include conflicts as warnings (not blocking)
622
+ warnings:
623
+ instructionConflicts.length > 0
624
+ ? {
625
+ instruction_conflicts: instructionConflicts,
626
+ hint: "Review these potential conflicts between subtask instructions",
627
+ }
628
+ : undefined,
629
+ },
630
+ null,
631
+ 2,
632
+ );
633
+ } catch (error) {
634
+ if (error instanceof z.ZodError) {
635
+ return JSON.stringify(
636
+ {
637
+ valid: false,
638
+ error: "Schema validation failed",
639
+ details: error.issues,
640
+ },
641
+ null,
642
+ 2,
643
+ );
644
+ }
645
+ if (error instanceof SyntaxError) {
646
+ return JSON.stringify(
647
+ {
648
+ valid: false,
649
+ error: "Invalid JSON",
650
+ details: error.message,
651
+ },
652
+ null,
653
+ 2,
654
+ );
655
+ }
656
+ throw error;
657
+ }
658
+ },
659
+ });
660
+
661
+ /**
662
+ * Delegate task decomposition to a swarm/planner subagent
663
+ *
664
+ * Returns a prompt for spawning a planner agent that will handle all decomposition
665
+ * reasoning. This keeps the coordinator context lean by offloading:
666
+ * - Strategy selection
667
+ * - CASS queries
668
+ * - Skills discovery
669
+ * - File analysis
670
+ * - BeadTree generation
671
+ *
672
+ * The planner returns ONLY structured BeadTree JSON, which the coordinator
673
+ * validates and uses to create beads.
674
+ *
675
+ * @example
676
+ * ```typescript
677
+ * // Coordinator workflow:
678
+ * const delegateResult = await swarm_delegate_planning({
679
+ * task: "Add user authentication",
680
+ * context: "Next.js 14 app",
681
+ * });
682
+ *
683
+ * // Parse the result
684
+ * const { prompt, subagent_type } = JSON.parse(delegateResult);
685
+ *
686
+ * // Spawn subagent using Task tool
687
+ * const plannerResponse = await Task(prompt, subagent_type);
688
+ *
689
+ * // Validate the response
690
+ * await swarm_validate_decomposition({ response: plannerResponse });
691
+ * ```
692
+ */
693
+ export const swarm_delegate_planning = tool({
694
+ description:
695
+ "Delegate task decomposition to a swarm/planner subagent. Returns a prompt to spawn the planner. Use this to keep coordinator context lean - all planning reasoning happens in the subagent.",
696
+ args: {
697
+ task: tool.schema.string().min(1).describe("The task to decompose"),
698
+ context: tool.schema
699
+ .string()
700
+ .optional()
701
+ .describe("Additional context to include"),
702
+ max_subtasks: tool.schema
703
+ .number()
704
+ .int()
705
+ .min(2)
706
+ .max(10)
707
+ .optional()
708
+ .default(5)
709
+ .describe("Maximum number of subtasks (default: 5)"),
710
+ strategy: tool.schema
711
+ .enum(["auto", "file-based", "feature-based", "risk-based"])
712
+ .optional()
713
+ .default("auto")
714
+ .describe("Decomposition strategy (default: auto-detect)"),
715
+ query_cass: tool.schema
716
+ .boolean()
717
+ .optional()
718
+ .default(true)
719
+ .describe("Query CASS for similar past tasks (default: true)"),
720
+ },
721
+ async execute(args) {
722
+ // Import needed modules
723
+ const { selectStrategy, formatStrategyGuidelines } =
724
+ await import("./swarm-strategies");
725
+ const { formatMemoryQueryForDecomposition } = await import("./learning");
726
+ const { listSkills, getSkillsContextForSwarm, findRelevantSkills } =
727
+ await import("./skills");
728
+
729
+ // Select strategy
730
+ let selectedStrategy: Exclude<DecompositionStrategy, "auto">;
731
+ let strategyReasoning: string;
732
+
733
+ if (args.strategy && args.strategy !== "auto") {
734
+ selectedStrategy = args.strategy;
735
+ strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
736
+ } else {
737
+ const selection = selectStrategy(args.task);
738
+ selectedStrategy = selection.strategy;
739
+ strategyReasoning = selection.reasoning;
740
+ }
741
+
742
+ // Query CASS for similar past tasks
743
+ let cassContext = "";
744
+ let cassResultInfo: {
745
+ queried: boolean;
746
+ results_found?: number;
747
+ included_in_context?: boolean;
748
+ reason?: string;
749
+ };
750
+
751
+ if (args.query_cass !== false) {
752
+ const cassResult = await queryCassHistory(args.task, 3);
753
+ if (cassResult.status === "success") {
754
+ cassContext = formatCassHistoryForPrompt(cassResult.data);
755
+ cassResultInfo = {
756
+ queried: true,
757
+ results_found: cassResult.data.results.length,
758
+ included_in_context: true,
759
+ };
760
+ } else {
761
+ cassResultInfo = {
762
+ queried: true,
763
+ results_found: 0,
764
+ included_in_context: false,
765
+ reason: cassResult.status,
766
+ };
767
+ }
768
+ } else {
769
+ cassResultInfo = { queried: false, reason: "disabled" };
770
+ }
771
+
772
+ // Fetch skills context
773
+ let skillsContext = "";
774
+ let skillsInfo: { included: boolean; count?: number; relevant?: string[] } =
775
+ {
776
+ included: false,
777
+ };
778
+
779
+ const allSkills = await listSkills();
780
+ if (allSkills.length > 0) {
781
+ skillsContext = await getSkillsContextForSwarm();
782
+ const relevantSkills = await findRelevantSkills(args.task);
783
+ skillsInfo = {
784
+ included: true,
785
+ count: allSkills.length,
786
+ relevant: relevantSkills,
787
+ };
788
+
789
+ // Add suggestion for relevant skills
790
+ if (relevantSkills.length > 0) {
791
+ skillsContext += `\n\n**Suggested skills for this task**: ${relevantSkills.join(", ")}`;
792
+ }
793
+ }
794
+
795
+ // Format strategy guidelines
796
+ const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
797
+
798
+ // Combine user context
799
+ const contextSection = args.context
800
+ ? `## Additional Context\n${args.context}`
801
+ : "## Additional Context\n(none provided)";
802
+
803
+ // Build the planning prompt with clear instructions for JSON-only output
804
+ const planningPrompt = STRATEGY_DECOMPOSITION_PROMPT.replace(
805
+ "{task}",
806
+ args.task,
807
+ )
808
+ .replace("{strategy_guidelines}", strategyGuidelines)
809
+ .replace("{context_section}", contextSection)
810
+ .replace("{cass_history}", cassContext || "")
811
+ .replace("{skills_context}", skillsContext || "")
812
+ .replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
813
+
814
+ // Add strict JSON-only instructions for the subagent
815
+ const subagentInstructions = `
816
+ ## CRITICAL: Output Format
817
+
818
+ You are a planner subagent. Your ONLY output must be valid JSON matching the BeadTree schema.
819
+
820
+ DO NOT include:
821
+ - Explanatory text before or after the JSON
822
+ - Markdown code fences (\`\`\`json)
823
+ - Commentary or reasoning
824
+
825
+ OUTPUT ONLY the raw JSON object.
826
+
827
+ ## Example Output
828
+
829
+ {
830
+ "epic": {
831
+ "title": "Add user authentication",
832
+ "description": "Implement OAuth-based authentication system"
833
+ },
834
+ "subtasks": [
835
+ {
836
+ "title": "Set up OAuth provider",
837
+ "description": "Configure OAuth client credentials and redirect URLs",
838
+ "files": ["src/auth/oauth.ts", "src/config/auth.ts"],
839
+ "dependencies": [],
840
+ "estimated_complexity": 2
841
+ },
842
+ {
843
+ "title": "Create auth routes",
844
+ "description": "Implement login, logout, and callback routes",
845
+ "files": ["src/app/api/auth/[...nextauth]/route.ts"],
846
+ "dependencies": [0],
847
+ "estimated_complexity": 3
848
+ }
849
+ ]
850
+ }
851
+
852
+ Now generate the BeadTree for the given task.`;
853
+
854
+ const fullPrompt = `${planningPrompt}\n\n${subagentInstructions}`;
855
+
856
+ // Return structured output for coordinator
857
+ return JSON.stringify(
858
+ {
859
+ prompt: fullPrompt,
860
+ subagent_type: "swarm/planner",
861
+ description: "Task decomposition planning",
862
+ strategy: {
863
+ selected: selectedStrategy,
864
+ reasoning: strategyReasoning,
865
+ },
866
+ expected_output: "BeadTree JSON (raw JSON, no markdown)",
867
+ next_steps: [
868
+ "1. Spawn subagent with Task tool using returned prompt",
869
+ "2. Parse subagent response as JSON",
870
+ "3. Validate with swarm_validate_decomposition",
871
+ "4. Create beads with beads_create_epic",
872
+ ],
873
+ cass_history: cassResultInfo,
874
+ skills: skillsInfo,
875
+ // Add semantic-memory query instruction
876
+ memory_query: formatMemoryQueryForDecomposition(args.task, 3),
877
+ },
878
+ null,
879
+ 2,
880
+ );
881
+ },
882
+ });
883
+
884
+ // ============================================================================
885
+ // Errors
886
+ // ============================================================================
887
+
888
+ export class SwarmError extends Error {
889
+ constructor(
890
+ message: string,
891
+ public readonly operation: string,
892
+ public readonly details?: unknown,
893
+ ) {
894
+ super(message);
895
+ this.name = "SwarmError";
896
+ }
897
+ }
898
+
899
+ export class DecompositionError extends SwarmError {
900
+ constructor(
901
+ message: string,
902
+ public readonly zodError?: z.ZodError,
903
+ ) {
904
+ super(message, "decompose", zodError?.issues);
905
+ }
906
+ }
907
+
908
+ export const decomposeTools = {
909
+ swarm_decompose,
910
+ swarm_validate_decomposition,
911
+ swarm_delegate_planning,
912
+ };