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.
- package/.beads/issues.jsonl +71 -61
- package/.github/workflows/ci.yml +5 -1
- package/README.md +48 -4
- package/dist/index.js +6643 -6326
- package/dist/plugin.js +2726 -2404
- package/package.json +1 -1
- package/src/agent-mail.ts +13 -0
- package/src/anti-patterns.test.ts +1167 -0
- package/src/anti-patterns.ts +29 -11
- package/src/pattern-maturity.ts +51 -13
- package/src/plugin.ts +15 -3
- package/src/schemas/bead.ts +35 -4
- package/src/schemas/evaluation.ts +18 -6
- package/src/schemas/index.ts +25 -2
- package/src/schemas/task.ts +49 -21
- package/src/streams/debug.ts +101 -3
- package/src/streams/index.ts +58 -1
- package/src/streams/migrations.ts +46 -4
- package/src/streams/store.integration.test.ts +110 -0
- package/src/streams/store.ts +311 -126
- package/src/structured.test.ts +1046 -0
- package/src/structured.ts +74 -27
- package/src/swarm-decompose.ts +912 -0
- package/src/swarm-orchestrate.ts +1869 -0
- package/src/swarm-prompts.ts +756 -0
- package/src/swarm-strategies.ts +407 -0
- package/src/swarm.ts +23 -3876
- package/src/tool-availability.ts +29 -6
- package/test-bug-fixes.ts +86 -0
|
@@ -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
|
+
};
|