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
package/src/swarm.ts
CHANGED
|
@@ -1,3888 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Swarm Module - High-level swarm coordination
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* This module re-exports from focused submodules for backward compatibility.
|
|
5
|
+
* For new code, prefer importing from specific modules:
|
|
6
|
+
* - swarm-strategies.ts - Strategy selection
|
|
7
|
+
* - swarm-decompose.ts - Task decomposition
|
|
8
|
+
* - swarm-prompts.ts - Prompt templates
|
|
9
|
+
* - swarm-orchestrate.ts - Status and completion
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
* - Task decomposition into bead trees with file assignments
|
|
10
|
-
* - Swarm status tracking via beads + Agent Mail
|
|
11
|
-
* - Progress reporting and completion handling
|
|
12
|
-
* - Prompt templates for decomposition, subtasks, and evaluation
|
|
11
|
+
* @module swarm
|
|
13
12
|
*/
|
|
14
|
-
import { tool } from "@opencode-ai/plugin";
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
import {
|
|
17
|
-
BeadTreeSchema,
|
|
18
|
-
SwarmStatusSchema,
|
|
19
|
-
AgentProgressSchema,
|
|
20
|
-
EvaluationSchema,
|
|
21
|
-
BeadSchema,
|
|
22
|
-
type SwarmStatus,
|
|
23
|
-
type AgentProgress,
|
|
24
|
-
type Evaluation,
|
|
25
|
-
type SpawnedAgent,
|
|
26
|
-
type Bead,
|
|
27
|
-
} from "./schemas";
|
|
28
|
-
import {
|
|
29
|
-
sendSwarmMessage,
|
|
30
|
-
getSwarmInbox,
|
|
31
|
-
readSwarmMessage,
|
|
32
|
-
releaseSwarmFiles,
|
|
33
|
-
} from "./streams/swarm-mail";
|
|
34
|
-
import {
|
|
35
|
-
OutcomeSignalsSchema,
|
|
36
|
-
DecompositionStrategySchema,
|
|
37
|
-
scoreImplicitFeedback,
|
|
38
|
-
outcomeToFeedback,
|
|
39
|
-
ErrorAccumulator,
|
|
40
|
-
ErrorEntrySchema,
|
|
41
|
-
formatMemoryStoreOnSuccess,
|
|
42
|
-
formatMemoryStoreOn3Strike,
|
|
43
|
-
formatMemoryQueryForDecomposition,
|
|
44
|
-
formatMemoryValidationHint,
|
|
45
|
-
type OutcomeSignals,
|
|
46
|
-
type ScoredOutcome,
|
|
47
|
-
type FeedbackEvent,
|
|
48
|
-
type ErrorEntry,
|
|
49
|
-
type ErrorType,
|
|
50
|
-
type DecompositionStrategy as LearningDecompositionStrategy,
|
|
51
|
-
DEFAULT_LEARNING_CONFIG,
|
|
52
|
-
} from "./learning";
|
|
53
|
-
import {
|
|
54
|
-
isToolAvailable,
|
|
55
|
-
warnMissingTool,
|
|
56
|
-
checkAllTools,
|
|
57
|
-
formatToolAvailability,
|
|
58
|
-
type ToolName,
|
|
59
|
-
} from "./tool-availability";
|
|
60
|
-
import {
|
|
61
|
-
getSkillsContextForSwarm,
|
|
62
|
-
findRelevantSkills,
|
|
63
|
-
listSkills,
|
|
64
|
-
} from "./skills";
|
|
65
13
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
14
|
+
// Re-export everything for backward compatibility
|
|
15
|
+
export * from "./swarm-strategies";
|
|
16
|
+
export * from "./swarm-decompose";
|
|
17
|
+
export * from "./swarm-prompts";
|
|
18
|
+
export * from "./swarm-orchestrate";
|
|
69
19
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"must",
|
|
76
|
-
"required",
|
|
77
|
-
"ensure",
|
|
78
|
-
"use",
|
|
79
|
-
"prefer",
|
|
80
|
-
];
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Marker words that indicate negative directives
|
|
84
|
-
*/
|
|
85
|
-
const NEGATIVE_MARKERS = [
|
|
86
|
-
"never",
|
|
87
|
-
"dont",
|
|
88
|
-
"don't",
|
|
89
|
-
"avoid",
|
|
90
|
-
"forbid",
|
|
91
|
-
"no ",
|
|
92
|
-
"not ",
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* A detected conflict between subtask instructions
|
|
97
|
-
*/
|
|
98
|
-
export interface InstructionConflict {
|
|
99
|
-
subtask_a: number;
|
|
100
|
-
subtask_b: number;
|
|
101
|
-
directive_a: string;
|
|
102
|
-
directive_b: string;
|
|
103
|
-
conflict_type: "positive_negative" | "contradictory";
|
|
104
|
-
description: string;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Extract directives from text based on marker words
|
|
109
|
-
*/
|
|
110
|
-
function extractDirectives(text: string): {
|
|
111
|
-
positive: string[];
|
|
112
|
-
negative: string[];
|
|
113
|
-
} {
|
|
114
|
-
const sentences = text.split(/[.!?\n]+/).map((s) => s.trim().toLowerCase());
|
|
115
|
-
const positive: string[] = [];
|
|
116
|
-
const negative: string[] = [];
|
|
117
|
-
|
|
118
|
-
for (const sentence of sentences) {
|
|
119
|
-
if (!sentence) continue;
|
|
120
|
-
|
|
121
|
-
const hasPositive = POSITIVE_MARKERS.some((m) => sentence.includes(m));
|
|
122
|
-
const hasNegative = NEGATIVE_MARKERS.some((m) => sentence.includes(m));
|
|
123
|
-
|
|
124
|
-
if (hasPositive && !hasNegative) {
|
|
125
|
-
positive.push(sentence);
|
|
126
|
-
} else if (hasNegative) {
|
|
127
|
-
negative.push(sentence);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return { positive, negative };
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Check if two directives conflict
|
|
136
|
-
*
|
|
137
|
-
* Simple heuristic: look for common subjects with opposite polarity
|
|
138
|
-
*/
|
|
139
|
-
function directivesConflict(positive: string, negative: string): boolean {
|
|
140
|
-
// Extract key nouns/concepts (simple word overlap check)
|
|
141
|
-
const positiveWords = new Set(
|
|
142
|
-
positive.split(/\s+/).filter((w) => w.length > 3),
|
|
143
|
-
);
|
|
144
|
-
const negativeWords = negative.split(/\s+/).filter((w) => w.length > 3);
|
|
145
|
-
|
|
146
|
-
// If they share significant words, they might conflict
|
|
147
|
-
const overlap = negativeWords.filter((w) => positiveWords.has(w));
|
|
148
|
-
return overlap.length >= 2;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Detect conflicts between subtask instructions
|
|
153
|
-
*
|
|
154
|
-
* Looks for cases where one subtask says "always use X" and another says "avoid X".
|
|
155
|
-
*
|
|
156
|
-
* @param subtasks - Array of subtask descriptions
|
|
157
|
-
* @returns Array of detected conflicts
|
|
158
|
-
*
|
|
159
|
-
* @see https://github.com/Dicklesworthstone/cass_memory_system/blob/main/src/curate.ts#L36-L89
|
|
160
|
-
*/
|
|
161
|
-
export function detectInstructionConflicts(
|
|
162
|
-
subtasks: Array<{ title: string; description?: string }>,
|
|
163
|
-
): InstructionConflict[] {
|
|
164
|
-
const conflicts: InstructionConflict[] = [];
|
|
165
|
-
|
|
166
|
-
// Extract directives from each subtask
|
|
167
|
-
const subtaskDirectives = subtasks.map((s, i) => ({
|
|
168
|
-
index: i,
|
|
169
|
-
title: s.title,
|
|
170
|
-
...extractDirectives(`${s.title} ${s.description || ""}`),
|
|
171
|
-
}));
|
|
172
|
-
|
|
173
|
-
// Compare each pair of subtasks
|
|
174
|
-
for (let i = 0; i < subtaskDirectives.length; i++) {
|
|
175
|
-
for (let j = i + 1; j < subtaskDirectives.length; j++) {
|
|
176
|
-
const a = subtaskDirectives[i];
|
|
177
|
-
const b = subtaskDirectives[j];
|
|
178
|
-
|
|
179
|
-
// Check if A's positive conflicts with B's negative
|
|
180
|
-
for (const posA of a.positive) {
|
|
181
|
-
for (const negB of b.negative) {
|
|
182
|
-
if (directivesConflict(posA, negB)) {
|
|
183
|
-
conflicts.push({
|
|
184
|
-
subtask_a: i,
|
|
185
|
-
subtask_b: j,
|
|
186
|
-
directive_a: posA,
|
|
187
|
-
directive_b: negB,
|
|
188
|
-
conflict_type: "positive_negative",
|
|
189
|
-
description: `Subtask ${i} says "${posA}" but subtask ${j} says "${negB}"`,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Check if B's positive conflicts with A's negative
|
|
196
|
-
for (const posB of b.positive) {
|
|
197
|
-
for (const negA of a.negative) {
|
|
198
|
-
if (directivesConflict(posB, negA)) {
|
|
199
|
-
conflicts.push({
|
|
200
|
-
subtask_a: j,
|
|
201
|
-
subtask_b: i,
|
|
202
|
-
directive_a: posB,
|
|
203
|
-
directive_b: negA,
|
|
204
|
-
conflict_type: "positive_negative",
|
|
205
|
-
description: `Subtask ${j} says "${posB}" but subtask ${i} says "${negA}"`,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return conflicts;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ============================================================================
|
|
217
|
-
// Strategy Definitions
|
|
218
|
-
// ============================================================================
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Decomposition strategy types
|
|
222
|
-
*/
|
|
223
|
-
export type DecompositionStrategy =
|
|
224
|
-
| "file-based"
|
|
225
|
-
| "feature-based"
|
|
226
|
-
| "risk-based"
|
|
227
|
-
| "research-based"
|
|
228
|
-
| "auto";
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Strategy definition with keywords, guidelines, and anti-patterns
|
|
232
|
-
*/
|
|
233
|
-
export interface StrategyDefinition {
|
|
234
|
-
name: DecompositionStrategy;
|
|
235
|
-
description: string;
|
|
236
|
-
keywords: string[];
|
|
237
|
-
guidelines: string[];
|
|
238
|
-
antiPatterns: string[];
|
|
239
|
-
examples: string[];
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Strategy definitions for task decomposition
|
|
244
|
-
*/
|
|
245
|
-
export const STRATEGIES: Record<
|
|
246
|
-
Exclude<DecompositionStrategy, "auto">,
|
|
247
|
-
StrategyDefinition
|
|
248
|
-
> = {
|
|
249
|
-
"file-based": {
|
|
250
|
-
name: "file-based",
|
|
251
|
-
description:
|
|
252
|
-
"Group by file type or directory. Best for refactoring, migrations, and pattern changes across codebase.",
|
|
253
|
-
keywords: [
|
|
254
|
-
"refactor",
|
|
255
|
-
"migrate",
|
|
256
|
-
"update all",
|
|
257
|
-
"rename",
|
|
258
|
-
"replace",
|
|
259
|
-
"convert",
|
|
260
|
-
"upgrade",
|
|
261
|
-
"deprecate",
|
|
262
|
-
"remove",
|
|
263
|
-
"cleanup",
|
|
264
|
-
"lint",
|
|
265
|
-
"format",
|
|
266
|
-
],
|
|
267
|
-
guidelines: [
|
|
268
|
-
"Group files by directory or type (e.g., all components, all tests)",
|
|
269
|
-
"Minimize cross-directory dependencies within a subtask",
|
|
270
|
-
"Handle shared types/utilities first if they change",
|
|
271
|
-
"Each subtask should be a complete transformation of its file set",
|
|
272
|
-
"Consider import/export relationships when grouping",
|
|
273
|
-
],
|
|
274
|
-
antiPatterns: [
|
|
275
|
-
"Don't split tightly coupled files across subtasks",
|
|
276
|
-
"Don't group files that have no relationship",
|
|
277
|
-
"Don't forget to update imports when moving/renaming",
|
|
278
|
-
],
|
|
279
|
-
examples: [
|
|
280
|
-
"Migrate all components to new API → split by component directory",
|
|
281
|
-
"Rename userId to accountId → split by module (types first, then consumers)",
|
|
282
|
-
"Update all tests to use new matcher → split by test directory",
|
|
283
|
-
],
|
|
284
|
-
},
|
|
285
|
-
"feature-based": {
|
|
286
|
-
name: "feature-based",
|
|
287
|
-
description:
|
|
288
|
-
"Vertical slices with UI + API + data. Best for new features and adding functionality.",
|
|
289
|
-
keywords: [
|
|
290
|
-
"add",
|
|
291
|
-
"implement",
|
|
292
|
-
"build",
|
|
293
|
-
"create",
|
|
294
|
-
"feature",
|
|
295
|
-
"new",
|
|
296
|
-
"integrate",
|
|
297
|
-
"connect",
|
|
298
|
-
"enable",
|
|
299
|
-
"support",
|
|
300
|
-
],
|
|
301
|
-
guidelines: [
|
|
302
|
-
"Each subtask is a complete vertical slice (UI + logic + data)",
|
|
303
|
-
"Start with data layer/types, then logic, then UI",
|
|
304
|
-
"Keep related components together (form + validation + submission)",
|
|
305
|
-
"Separate concerns that can be developed independently",
|
|
306
|
-
"Consider user-facing features as natural boundaries",
|
|
307
|
-
],
|
|
308
|
-
antiPatterns: [
|
|
309
|
-
"Don't split a single feature across multiple subtasks",
|
|
310
|
-
"Don't create subtasks that can't be tested independently",
|
|
311
|
-
"Don't forget integration points between features",
|
|
312
|
-
],
|
|
313
|
-
examples: [
|
|
314
|
-
"Add user auth → [OAuth setup, Session management, Protected routes]",
|
|
315
|
-
"Build dashboard → [Data fetching, Chart components, Layout/navigation]",
|
|
316
|
-
"Add search → [Search API, Search UI, Results display]",
|
|
317
|
-
],
|
|
318
|
-
},
|
|
319
|
-
"risk-based": {
|
|
320
|
-
name: "risk-based",
|
|
321
|
-
description:
|
|
322
|
-
"Isolate high-risk changes, add tests first. Best for bug fixes, security issues, and critical changes.",
|
|
323
|
-
keywords: [
|
|
324
|
-
"fix",
|
|
325
|
-
"bug",
|
|
326
|
-
"security",
|
|
327
|
-
"vulnerability",
|
|
328
|
-
"critical",
|
|
329
|
-
"urgent",
|
|
330
|
-
"hotfix",
|
|
331
|
-
"patch",
|
|
332
|
-
"audit",
|
|
333
|
-
"review",
|
|
334
|
-
],
|
|
335
|
-
guidelines: [
|
|
336
|
-
"Write tests FIRST to capture expected behavior",
|
|
337
|
-
"Isolate the risky change to minimize blast radius",
|
|
338
|
-
"Add monitoring/logging around the change",
|
|
339
|
-
"Create rollback plan as part of the task",
|
|
340
|
-
"Audit similar code for the same issue",
|
|
341
|
-
],
|
|
342
|
-
antiPatterns: [
|
|
343
|
-
"Don't make multiple risky changes in one subtask",
|
|
344
|
-
"Don't skip tests for 'simple' fixes",
|
|
345
|
-
"Don't forget to check for similar issues elsewhere",
|
|
346
|
-
],
|
|
347
|
-
examples: [
|
|
348
|
-
"Fix auth bypass → [Add regression test, Fix vulnerability, Audit similar endpoints]",
|
|
349
|
-
"Fix race condition → [Add test reproducing issue, Implement fix, Add concurrency tests]",
|
|
350
|
-
"Security audit → [Scan for vulnerabilities, Fix critical issues, Document remaining risks]",
|
|
351
|
-
],
|
|
352
|
-
},
|
|
353
|
-
"research-based": {
|
|
354
|
-
name: "research-based",
|
|
355
|
-
description:
|
|
356
|
-
"Parallel search across multiple sources, then synthesize. Best for investigation, learning, and discovery tasks.",
|
|
357
|
-
keywords: [
|
|
358
|
-
"research",
|
|
359
|
-
"investigate",
|
|
360
|
-
"explore",
|
|
361
|
-
"find out",
|
|
362
|
-
"discover",
|
|
363
|
-
"understand",
|
|
364
|
-
"learn about",
|
|
365
|
-
"analyze",
|
|
366
|
-
"what is",
|
|
367
|
-
"what are",
|
|
368
|
-
"how does",
|
|
369
|
-
"how do",
|
|
370
|
-
"why does",
|
|
371
|
-
"why do",
|
|
372
|
-
"compare",
|
|
373
|
-
"evaluate",
|
|
374
|
-
"study",
|
|
375
|
-
"look up",
|
|
376
|
-
"look into",
|
|
377
|
-
"search for",
|
|
378
|
-
"dig into",
|
|
379
|
-
"figure out",
|
|
380
|
-
"debug options",
|
|
381
|
-
"debug levers",
|
|
382
|
-
"configuration options",
|
|
383
|
-
"environment variables",
|
|
384
|
-
"available options",
|
|
385
|
-
"documentation",
|
|
386
|
-
],
|
|
387
|
-
guidelines: [
|
|
388
|
-
"Split by information source (PDFs, repos, history, web)",
|
|
389
|
-
"Each agent searches with different query angles",
|
|
390
|
-
"Include a synthesis subtask that depends on all search subtasks",
|
|
391
|
-
"Use pdf-brain for documentation/books if available",
|
|
392
|
-
"Use repo-crawl for GitHub repos if URL provided",
|
|
393
|
-
"Use cass for past agent session history",
|
|
394
|
-
"Assign NO files to research subtasks (read-only)",
|
|
395
|
-
],
|
|
396
|
-
antiPatterns: [
|
|
397
|
-
"Don't have one agent search everything sequentially",
|
|
398
|
-
"Don't skip synthesis - raw search results need consolidation",
|
|
399
|
-
"Don't forget to check tool availability before assigning sources",
|
|
400
|
-
],
|
|
401
|
-
examples: [
|
|
402
|
-
"Research auth patterns → [Search PDFs, Search repos, Search history, Synthesize]",
|
|
403
|
-
"Investigate error → [Search cass for similar errors, Search repo for error handling, Synthesize]",
|
|
404
|
-
"Learn about library → [Search docs, Search examples, Search issues, Synthesize findings]",
|
|
405
|
-
],
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Analyze task description and select best decomposition strategy
|
|
411
|
-
*
|
|
412
|
-
* @param task - Task description
|
|
413
|
-
* @returns Selected strategy with reasoning
|
|
414
|
-
*/
|
|
415
|
-
export function selectStrategy(task: string): {
|
|
416
|
-
strategy: Exclude<DecompositionStrategy, "auto">;
|
|
417
|
-
confidence: number;
|
|
418
|
-
reasoning: string;
|
|
419
|
-
alternatives: Array<{
|
|
420
|
-
strategy: Exclude<DecompositionStrategy, "auto">;
|
|
421
|
-
score: number;
|
|
422
|
-
}>;
|
|
423
|
-
} {
|
|
424
|
-
const taskLower = task.toLowerCase();
|
|
425
|
-
|
|
426
|
-
// Score each strategy based on keyword matches
|
|
427
|
-
const scores: Record<Exclude<DecompositionStrategy, "auto">, number> = {
|
|
428
|
-
"file-based": 0,
|
|
429
|
-
"feature-based": 0,
|
|
430
|
-
"risk-based": 0,
|
|
431
|
-
"research-based": 0,
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
for (const [strategyName, definition] of Object.entries(STRATEGIES)) {
|
|
435
|
-
const name = strategyName as Exclude<DecompositionStrategy, "auto">;
|
|
436
|
-
for (const keyword of definition.keywords) {
|
|
437
|
-
// Use word boundary matching to avoid "debug" matching "bug"
|
|
438
|
-
// For multi-word keywords, just check includes (they're specific enough)
|
|
439
|
-
if (keyword.includes(" ")) {
|
|
440
|
-
if (taskLower.includes(keyword)) {
|
|
441
|
-
scores[name] += 1;
|
|
442
|
-
}
|
|
443
|
-
} else {
|
|
444
|
-
// Single word: use word boundary regex
|
|
445
|
-
const regex = new RegExp(`\\b${keyword}\\b`, "i");
|
|
446
|
-
if (regex.test(taskLower)) {
|
|
447
|
-
scores[name] += 1;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Find the winner
|
|
454
|
-
const entries = Object.entries(scores) as Array<
|
|
455
|
-
[Exclude<DecompositionStrategy, "auto">, number]
|
|
456
|
-
>;
|
|
457
|
-
entries.sort((a, b) => b[1] - a[1]);
|
|
458
|
-
|
|
459
|
-
const [winner, winnerScore] = entries[0];
|
|
460
|
-
const [runnerUp, runnerUpScore] = entries[1] || [null, 0];
|
|
461
|
-
|
|
462
|
-
// Calculate confidence based on margin
|
|
463
|
-
const totalScore = entries.reduce((sum, [, score]) => sum + score, 0);
|
|
464
|
-
const confidence =
|
|
465
|
-
totalScore > 0
|
|
466
|
-
? Math.min(0.95, 0.5 + (winnerScore - runnerUpScore) / totalScore)
|
|
467
|
-
: 0.5; // Default to 50% if no keywords matched
|
|
468
|
-
|
|
469
|
-
// Build reasoning
|
|
470
|
-
let reasoning: string;
|
|
471
|
-
if (winnerScore === 0) {
|
|
472
|
-
reasoning = `No strong keyword signals. Defaulting to feature-based as it's most versatile.`;
|
|
473
|
-
} else {
|
|
474
|
-
const matchedKeywords = STRATEGIES[winner].keywords.filter((k) =>
|
|
475
|
-
taskLower.includes(k),
|
|
476
|
-
);
|
|
477
|
-
reasoning = `Matched keywords: ${matchedKeywords.join(", ")}. ${STRATEGIES[winner].description}`;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// If no keywords matched, default to feature-based
|
|
481
|
-
const finalStrategy = winnerScore === 0 ? "feature-based" : winner;
|
|
482
|
-
|
|
483
|
-
return {
|
|
484
|
-
strategy: finalStrategy,
|
|
485
|
-
confidence,
|
|
486
|
-
reasoning,
|
|
487
|
-
alternatives: entries
|
|
488
|
-
.filter(([s]) => s !== finalStrategy)
|
|
489
|
-
.map(([strategy, score]) => ({ strategy, score })),
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Format strategy-specific guidelines for the decomposition prompt
|
|
495
|
-
*/
|
|
496
|
-
export function formatStrategyGuidelines(
|
|
497
|
-
strategy: Exclude<DecompositionStrategy, "auto">,
|
|
498
|
-
): string {
|
|
499
|
-
const def = STRATEGIES[strategy];
|
|
500
|
-
|
|
501
|
-
const guidelines = def.guidelines.map((g) => `- ${g}`).join("\n");
|
|
502
|
-
const antiPatterns = def.antiPatterns.map((a) => `- ${a}`).join("\n");
|
|
503
|
-
const examples = def.examples.map((e) => `- ${e}`).join("\n");
|
|
504
|
-
|
|
505
|
-
return `## Strategy: ${strategy}
|
|
506
|
-
|
|
507
|
-
${def.description}
|
|
508
|
-
|
|
509
|
-
### Guidelines
|
|
510
|
-
${guidelines}
|
|
511
|
-
|
|
512
|
-
### Anti-Patterns (Avoid These)
|
|
513
|
-
${antiPatterns}
|
|
514
|
-
|
|
515
|
-
### Examples
|
|
516
|
-
${examples}`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// ============================================================================
|
|
520
|
-
// Prompt Templates
|
|
521
|
-
// ============================================================================
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Prompt for decomposing a task into parallelizable subtasks.
|
|
525
|
-
*
|
|
526
|
-
* Used by swarm:decompose to instruct the agent on how to break down work.
|
|
527
|
-
* The agent responds with a BeadTree that gets validated.
|
|
528
|
-
*/
|
|
529
|
-
export const DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
|
|
530
|
-
|
|
531
|
-
## Task
|
|
532
|
-
{task}
|
|
533
|
-
|
|
534
|
-
{context_section}
|
|
535
|
-
|
|
536
|
-
## MANDATORY: Beads Issue Tracking
|
|
537
|
-
|
|
538
|
-
**Every subtask MUST become a bead.** This is non-negotiable.
|
|
539
|
-
|
|
540
|
-
After decomposition, the coordinator will:
|
|
541
|
-
1. Create an epic bead for the overall task
|
|
542
|
-
2. Create child beads for each subtask
|
|
543
|
-
3. Track progress through bead status updates
|
|
544
|
-
4. Close beads with summaries when complete
|
|
545
|
-
|
|
546
|
-
Agents MUST update their bead status as they work. No silent progress.
|
|
547
|
-
|
|
548
|
-
## Requirements
|
|
549
|
-
|
|
550
|
-
1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
|
|
551
|
-
2. **Assign files** - each subtask must specify which files it will modify
|
|
552
|
-
3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
|
|
553
|
-
4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
|
|
554
|
-
5. **Estimate complexity** - 1 (trivial) to 5 (complex)
|
|
555
|
-
6. **Plan aggressively** - break down more than you think necessary, smaller is better
|
|
556
|
-
|
|
557
|
-
## Response Format
|
|
558
|
-
|
|
559
|
-
Respond with a JSON object matching this schema:
|
|
560
|
-
|
|
561
|
-
\`\`\`typescript
|
|
562
|
-
{
|
|
563
|
-
epic: {
|
|
564
|
-
title: string, // Epic title for the beads tracker
|
|
565
|
-
description?: string // Brief description of the overall goal
|
|
566
|
-
},
|
|
567
|
-
subtasks: [
|
|
568
|
-
{
|
|
569
|
-
title: string, // What this subtask accomplishes
|
|
570
|
-
description?: string, // Detailed instructions for the agent
|
|
571
|
-
files: string[], // Files this subtask will modify (globs allowed)
|
|
572
|
-
dependencies: number[], // Indices of subtasks this depends on (0-indexed)
|
|
573
|
-
estimated_complexity: 1-5 // Effort estimate
|
|
574
|
-
},
|
|
575
|
-
// ... more subtasks
|
|
576
|
-
]
|
|
577
|
-
}
|
|
578
|
-
\`\`\`
|
|
579
|
-
|
|
580
|
-
## Guidelines
|
|
581
|
-
|
|
582
|
-
- **Plan aggressively** - when in doubt, split further. 3 small tasks > 1 medium task
|
|
583
|
-
- **Prefer smaller, focused subtasks** over large complex ones
|
|
584
|
-
- **Include test files** in the same subtask as the code they test
|
|
585
|
-
- **Consider shared types** - if multiple files share types, handle that first
|
|
586
|
-
- **Think about imports** - changes to exported APIs affect downstream files
|
|
587
|
-
- **Explicit > implicit** - spell out what each subtask should do, don't assume
|
|
588
|
-
|
|
589
|
-
## File Assignment Examples
|
|
590
|
-
|
|
591
|
-
- Schema change: \`["src/schemas/user.ts", "src/schemas/index.ts"]\`
|
|
592
|
-
- Component + test: \`["src/components/Button.tsx", "src/components/Button.test.tsx"]\`
|
|
593
|
-
- API route: \`["src/app/api/users/route.ts"]\`
|
|
594
|
-
|
|
595
|
-
Now decompose the task:`;
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Prompt template for spawned subtask agents.
|
|
599
|
-
*
|
|
600
|
-
* Each agent receives this prompt with their specific subtask details filled in.
|
|
601
|
-
* The prompt establishes context, constraints, and expectations.
|
|
602
|
-
*/
|
|
603
|
-
export const SUBTASK_PROMPT = `You are a swarm agent working on a subtask of a larger epic.
|
|
604
|
-
|
|
605
|
-
## Your Identity
|
|
606
|
-
- **Agent Name**: {agent_name}
|
|
607
|
-
- **Bead ID**: {bead_id}
|
|
608
|
-
- **Epic ID**: {epic_id}
|
|
609
|
-
|
|
610
|
-
## Your Subtask
|
|
611
|
-
**Title**: {subtask_title}
|
|
612
|
-
|
|
613
|
-
{subtask_description}
|
|
614
|
-
|
|
615
|
-
## File Scope
|
|
616
|
-
You have exclusive reservations for these files:
|
|
617
|
-
{file_list}
|
|
618
|
-
|
|
619
|
-
**CRITICAL**: Only modify files in your reservation. If you need to modify other files,
|
|
620
|
-
send a message to the coordinator requesting the change.
|
|
621
|
-
|
|
622
|
-
## Shared Context
|
|
623
|
-
{shared_context}
|
|
624
|
-
|
|
625
|
-
## MANDATORY: Beads Tracking
|
|
626
|
-
|
|
627
|
-
You MUST keep your bead updated as you work:
|
|
628
|
-
|
|
629
|
-
1. **Your bead is already in_progress** - don't change this unless blocked
|
|
630
|
-
2. **If blocked**: \`bd update {bead_id} --status blocked\` and message coordinator
|
|
631
|
-
3. **When done**: Use \`swarm_complete\` - it closes your bead automatically
|
|
632
|
-
4. **Discovered issues**: Create new beads with \`bd create "issue" -t bug\`
|
|
633
|
-
|
|
634
|
-
**Never work silently.** Your bead status is how the swarm tracks progress.
|
|
635
|
-
|
|
636
|
-
## MANDATORY: Swarm Mail Communication
|
|
637
|
-
|
|
638
|
-
You MUST communicate with other agents:
|
|
639
|
-
|
|
640
|
-
1. **Report progress** every significant milestone (not just at the end)
|
|
641
|
-
2. **Ask questions** if requirements are unclear - don't guess
|
|
642
|
-
3. **Announce blockers** immediately - don't spin trying to fix alone
|
|
643
|
-
4. **Coordinate on shared concerns** - if you see something affecting other agents, say so
|
|
644
|
-
|
|
645
|
-
Use Swarm Mail for all communication:
|
|
646
|
-
\`\`\`
|
|
647
|
-
swarmmail_send(
|
|
648
|
-
to: ["coordinator" or specific agent],
|
|
649
|
-
subject: "Brief subject",
|
|
650
|
-
body: "Message content",
|
|
651
|
-
thread_id: "{epic_id}"
|
|
652
|
-
)
|
|
653
|
-
\`\`\`
|
|
654
|
-
|
|
655
|
-
## Coordination Protocol
|
|
656
|
-
|
|
657
|
-
1. **Start**: Your bead is already marked in_progress
|
|
658
|
-
2. **Progress**: Use swarm_progress to report status updates
|
|
659
|
-
3. **Blocked**: Report immediately via Swarm Mail - don't spin
|
|
660
|
-
4. **Complete**: Use swarm_complete when done - it handles:
|
|
661
|
-
- Closing your bead with a summary
|
|
662
|
-
- Releasing file reservations
|
|
663
|
-
- Notifying the coordinator
|
|
664
|
-
|
|
665
|
-
## Self-Evaluation
|
|
666
|
-
|
|
667
|
-
Before calling swarm_complete, evaluate your work:
|
|
668
|
-
- Type safety: Does it compile without errors?
|
|
669
|
-
- No obvious bugs: Did you handle edge cases?
|
|
670
|
-
- Follows patterns: Does it match existing code style?
|
|
671
|
-
- Readable: Would another developer understand it?
|
|
672
|
-
|
|
673
|
-
If evaluation fails, fix the issues before completing.
|
|
674
|
-
|
|
675
|
-
## Planning Your Work
|
|
676
|
-
|
|
677
|
-
Before writing code:
|
|
678
|
-
1. **Read the files** you're assigned to understand current state
|
|
679
|
-
2. **Plan your approach** - what changes, in what order?
|
|
680
|
-
3. **Identify risks** - what could go wrong? What dependencies?
|
|
681
|
-
4. **Communicate your plan** via Swarm Mail if non-trivial
|
|
682
|
-
|
|
683
|
-
Begin work on your subtask now.`;
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Streamlined subtask prompt (V2) - uses Swarm Mail and beads
|
|
687
|
-
*
|
|
688
|
-
* This is a cleaner version of SUBTASK_PROMPT that's easier to parse.
|
|
689
|
-
* Agents MUST use Swarm Mail for communication and beads for tracking.
|
|
690
|
-
*
|
|
691
|
-
* Supports {error_context} placeholder for retry prompts.
|
|
692
|
-
*/
|
|
693
|
-
export const SUBTASK_PROMPT_V2 = `You are a swarm agent working on: **{subtask_title}**
|
|
694
|
-
|
|
695
|
-
## [IDENTITY]
|
|
696
|
-
Agent: (assigned at spawn)
|
|
697
|
-
Bead: {bead_id}
|
|
698
|
-
Epic: {epic_id}
|
|
699
|
-
|
|
700
|
-
## [TASK]
|
|
701
|
-
{subtask_description}
|
|
702
|
-
|
|
703
|
-
## [FILES]
|
|
704
|
-
Reserved (exclusive):
|
|
705
|
-
{file_list}
|
|
706
|
-
|
|
707
|
-
Only modify these files. Need others? Message the coordinator.
|
|
708
|
-
|
|
709
|
-
## [CONTEXT]
|
|
710
|
-
{shared_context}
|
|
711
|
-
|
|
712
|
-
{compressed_context}
|
|
713
|
-
|
|
714
|
-
{error_context}
|
|
715
|
-
|
|
716
|
-
## [MANDATORY: SWARM MAIL]
|
|
717
|
-
|
|
718
|
-
**YOU MUST USE SWARM MAIL FOR ALL COORDINATION.** This is non-negotiable.
|
|
719
|
-
|
|
720
|
-
### Initialize FIRST (before any work)
|
|
721
|
-
\`\`\`
|
|
722
|
-
swarmmail_init(project_path="$PWD", task_description="{subtask_title}")
|
|
723
|
-
\`\`\`
|
|
724
|
-
|
|
725
|
-
### Reserve Files (if not already reserved by coordinator)
|
|
726
|
-
\`\`\`
|
|
727
|
-
swarmmail_reserve(paths=[...files...], reason="{bead_id}: {subtask_title}")
|
|
728
|
-
\`\`\`
|
|
729
|
-
|
|
730
|
-
### Check Inbox Regularly
|
|
731
|
-
\`\`\`
|
|
732
|
-
swarmmail_inbox() # Check for coordinator messages
|
|
733
|
-
swarmmail_read_message(message_id=N) # Read specific message
|
|
734
|
-
\`\`\`
|
|
735
|
-
|
|
736
|
-
### Report Progress (REQUIRED - don't work silently)
|
|
737
|
-
\`\`\`
|
|
738
|
-
swarmmail_send(
|
|
739
|
-
to=["coordinator"],
|
|
740
|
-
subject="Progress: {bead_id}",
|
|
741
|
-
body="<what you did, blockers, questions>",
|
|
742
|
-
thread_id="{epic_id}"
|
|
743
|
-
)
|
|
744
|
-
\`\`\`
|
|
745
|
-
|
|
746
|
-
### When Blocked
|
|
747
|
-
\`\`\`
|
|
748
|
-
swarmmail_send(
|
|
749
|
-
to=["coordinator"],
|
|
750
|
-
subject="BLOCKED: {bead_id}",
|
|
751
|
-
body="<blocker description, what you need>",
|
|
752
|
-
importance="high",
|
|
753
|
-
thread_id="{epic_id}"
|
|
754
|
-
)
|
|
755
|
-
beads_update(id="{bead_id}", status="blocked")
|
|
756
|
-
\`\`\`
|
|
757
|
-
|
|
758
|
-
### Release Files When Done
|
|
759
|
-
\`\`\`
|
|
760
|
-
swarmmail_release() # Or let swarm_complete handle it
|
|
761
|
-
\`\`\`
|
|
762
|
-
|
|
763
|
-
## [OTHER TOOLS]
|
|
764
|
-
### Beads
|
|
765
|
-
- beads_update(id, status) - Mark blocked if stuck
|
|
766
|
-
- beads_create(title, type) - Log new bugs found
|
|
767
|
-
|
|
768
|
-
### Skills (if available)
|
|
769
|
-
- skills_list() - Discover available skills
|
|
770
|
-
- skills_use(name) - Activate skill for specialized guidance
|
|
771
|
-
|
|
772
|
-
### Completion (REQUIRED)
|
|
773
|
-
- swarm_complete(project_key, agent_name, bead_id, summary, files_touched)
|
|
774
|
-
|
|
775
|
-
## [LEARNING]
|
|
776
|
-
As you work, note reusable patterns, best practices, or domain insights:
|
|
777
|
-
- If you discover something that would help future agents, consider creating a skill
|
|
778
|
-
- Use skills_create to codify patterns for the project
|
|
779
|
-
- Good skills have clear "when to use" descriptions with actionable instructions
|
|
780
|
-
- Skills make swarms smarter over time
|
|
781
|
-
|
|
782
|
-
## [WORKFLOW]
|
|
783
|
-
1. **swarmmail_init** - Initialize session FIRST
|
|
784
|
-
2. Read assigned files
|
|
785
|
-
3. Implement changes
|
|
786
|
-
4. **swarmmail_send** - Report progress to coordinator
|
|
787
|
-
5. Verify (typecheck)
|
|
788
|
-
6. **swarm_complete** - Mark done, release reservations
|
|
789
|
-
|
|
790
|
-
**CRITICAL: Never work silently. Send progress updates via swarmmail_send every significant milestone.**
|
|
791
|
-
|
|
792
|
-
Begin now.`;
|
|
793
|
-
|
|
794
|
-
/**
|
|
795
|
-
* Format the V2 subtask prompt for a specific agent
|
|
796
|
-
*/
|
|
797
|
-
export function formatSubtaskPromptV2(params: {
|
|
798
|
-
bead_id: string;
|
|
799
|
-
epic_id: string;
|
|
800
|
-
subtask_title: string;
|
|
801
|
-
subtask_description: string;
|
|
802
|
-
files: string[];
|
|
803
|
-
shared_context?: string;
|
|
804
|
-
compressed_context?: string;
|
|
805
|
-
error_context?: string;
|
|
806
|
-
}): string {
|
|
807
|
-
const fileList =
|
|
808
|
-
params.files.length > 0
|
|
809
|
-
? params.files.map((f) => `- \`${f}\``).join("\n")
|
|
810
|
-
: "(no specific files - use judgment)";
|
|
811
|
-
|
|
812
|
-
const compressedSection = params.compressed_context
|
|
813
|
-
? params.compressed_context
|
|
814
|
-
: "";
|
|
815
|
-
|
|
816
|
-
const errorSection = params.error_context ? params.error_context : "";
|
|
817
|
-
|
|
818
|
-
return SUBTASK_PROMPT_V2.replace(/{bead_id}/g, params.bead_id)
|
|
819
|
-
.replace(/{epic_id}/g, params.epic_id)
|
|
820
|
-
.replace("{subtask_title}", params.subtask_title)
|
|
821
|
-
.replace(
|
|
822
|
-
"{subtask_description}",
|
|
823
|
-
params.subtask_description || "(see title)",
|
|
824
|
-
)
|
|
825
|
-
.replace("{file_list}", fileList)
|
|
826
|
-
.replace("{shared_context}", params.shared_context || "(none)")
|
|
827
|
-
.replace("{compressed_context}", compressedSection)
|
|
828
|
-
.replace("{error_context}", errorSection);
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* Prompt for self-evaluation before completing a subtask.
|
|
833
|
-
*
|
|
834
|
-
* Agents use this to assess their work quality before marking complete.
|
|
835
|
-
*/
|
|
836
|
-
export const EVALUATION_PROMPT = `Evaluate the work completed for this subtask.
|
|
837
|
-
|
|
838
|
-
## Subtask
|
|
839
|
-
**Bead ID**: {bead_id}
|
|
840
|
-
**Title**: {subtask_title}
|
|
841
|
-
|
|
842
|
-
## Files Modified
|
|
843
|
-
{files_touched}
|
|
844
|
-
|
|
845
|
-
## Evaluation Criteria
|
|
846
|
-
|
|
847
|
-
For each criterion, assess passed/failed and provide brief feedback:
|
|
848
|
-
|
|
849
|
-
1. **type_safe**: Code compiles without TypeScript errors
|
|
850
|
-
2. **no_bugs**: No obvious bugs, edge cases handled
|
|
851
|
-
3. **patterns**: Follows existing codebase patterns and conventions
|
|
852
|
-
4. **readable**: Code is clear and maintainable
|
|
853
|
-
|
|
854
|
-
## Response Format
|
|
855
|
-
|
|
856
|
-
\`\`\`json
|
|
857
|
-
{
|
|
858
|
-
"passed": boolean, // Overall pass/fail
|
|
859
|
-
"criteria": {
|
|
860
|
-
"type_safe": { "passed": boolean, "feedback": string },
|
|
861
|
-
"no_bugs": { "passed": boolean, "feedback": string },
|
|
862
|
-
"patterns": { "passed": boolean, "feedback": string },
|
|
863
|
-
"readable": { "passed": boolean, "feedback": string }
|
|
864
|
-
},
|
|
865
|
-
"overall_feedback": string,
|
|
866
|
-
"retry_suggestion": string | null // If failed, what to fix
|
|
867
|
-
}
|
|
868
|
-
\`\`\`
|
|
869
|
-
|
|
870
|
-
If any criterion fails, the overall evaluation fails and retry_suggestion
|
|
871
|
-
should describe what needs to be fixed.`;
|
|
872
|
-
|
|
873
|
-
// ============================================================================
|
|
874
|
-
// Errors
|
|
875
|
-
// ============================================================================
|
|
876
|
-
|
|
877
|
-
export class SwarmError extends Error {
|
|
878
|
-
constructor(
|
|
879
|
-
message: string,
|
|
880
|
-
public readonly operation: string,
|
|
881
|
-
public readonly details?: unknown,
|
|
882
|
-
) {
|
|
883
|
-
super(message);
|
|
884
|
-
this.name = "SwarmError";
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
export class DecompositionError extends SwarmError {
|
|
889
|
-
constructor(
|
|
890
|
-
message: string,
|
|
891
|
-
public readonly zodError?: z.ZodError,
|
|
892
|
-
) {
|
|
893
|
-
super(message, "decompose", zodError?.issues);
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// ============================================================================
|
|
898
|
-
// Helper Functions
|
|
899
|
-
// ============================================================================
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* Format the decomposition prompt with actual values
|
|
903
|
-
*/
|
|
904
|
-
function formatDecompositionPrompt(
|
|
905
|
-
task: string,
|
|
906
|
-
maxSubtasks: number,
|
|
907
|
-
context?: string,
|
|
908
|
-
): string {
|
|
909
|
-
const contextSection = context
|
|
910
|
-
? `## Additional Context\n${context}`
|
|
911
|
-
: "## Additional Context\n(none provided)";
|
|
912
|
-
|
|
913
|
-
return DECOMPOSITION_PROMPT.replace("{task}", task)
|
|
914
|
-
.replace("{max_subtasks}", maxSubtasks.toString())
|
|
915
|
-
.replace("{context_section}", contextSection);
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
/**
|
|
919
|
-
* Format the subtask prompt for a specific agent
|
|
920
|
-
*/
|
|
921
|
-
export function formatSubtaskPrompt(params: {
|
|
922
|
-
agent_name: string;
|
|
923
|
-
bead_id: string;
|
|
924
|
-
epic_id: string;
|
|
925
|
-
subtask_title: string;
|
|
926
|
-
subtask_description: string;
|
|
927
|
-
files: string[];
|
|
928
|
-
shared_context?: string;
|
|
929
|
-
}): string {
|
|
930
|
-
const fileList = params.files.map((f) => `- \`${f}\``).join("\n");
|
|
931
|
-
|
|
932
|
-
return SUBTASK_PROMPT.replace("{agent_name}", params.agent_name)
|
|
933
|
-
.replace("{bead_id}", params.bead_id)
|
|
934
|
-
.replace(/{epic_id}/g, params.epic_id)
|
|
935
|
-
.replace("{subtask_title}", params.subtask_title)
|
|
936
|
-
.replace("{subtask_description}", params.subtask_description || "(none)")
|
|
937
|
-
.replace("{file_list}", fileList || "(no files assigned)")
|
|
938
|
-
.replace("{shared_context}", params.shared_context || "(none)");
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
/**
|
|
942
|
-
* Format the evaluation prompt
|
|
943
|
-
*/
|
|
944
|
-
export function formatEvaluationPrompt(params: {
|
|
945
|
-
bead_id: string;
|
|
946
|
-
subtask_title: string;
|
|
947
|
-
files_touched: string[];
|
|
948
|
-
}): string {
|
|
949
|
-
const filesList = params.files_touched.map((f) => `- \`${f}\``).join("\n");
|
|
950
|
-
|
|
951
|
-
return EVALUATION_PROMPT.replace("{bead_id}", params.bead_id)
|
|
952
|
-
.replace("{subtask_title}", params.subtask_title)
|
|
953
|
-
.replace("{files_touched}", filesList || "(no files recorded)");
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Query beads for subtasks of an epic
|
|
958
|
-
*/
|
|
959
|
-
async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
|
|
960
|
-
// Check if beads is available
|
|
961
|
-
const beadsAvailable = await isToolAvailable("beads");
|
|
962
|
-
if (!beadsAvailable) {
|
|
963
|
-
warnMissingTool("beads");
|
|
964
|
-
return []; // Return empty - swarm can still function without status tracking
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
const result = await Bun.$`bd list --parent ${epicId} --json`
|
|
968
|
-
.quiet()
|
|
969
|
-
.nothrow();
|
|
970
|
-
|
|
971
|
-
if (result.exitCode !== 0) {
|
|
972
|
-
// Don't throw - just return empty and log error prominently
|
|
973
|
-
console.error(
|
|
974
|
-
`[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
|
|
975
|
-
result.stderr.toString(),
|
|
976
|
-
);
|
|
977
|
-
return [];
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
try {
|
|
981
|
-
const parsed = JSON.parse(result.stdout.toString());
|
|
982
|
-
return z.array(BeadSchema).parse(parsed);
|
|
983
|
-
} catch (error) {
|
|
984
|
-
if (error instanceof z.ZodError) {
|
|
985
|
-
console.error(
|
|
986
|
-
`[swarm] ERROR: Invalid bead data for epic ${epicId}:`,
|
|
987
|
-
error.message,
|
|
988
|
-
);
|
|
989
|
-
return [];
|
|
990
|
-
}
|
|
991
|
-
console.error(
|
|
992
|
-
`[swarm] ERROR: Failed to parse beads for epic ${epicId}:`,
|
|
993
|
-
error,
|
|
994
|
-
);
|
|
995
|
-
throw error;
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Query Agent Mail for swarm thread messages
|
|
1001
|
-
*/
|
|
1002
|
-
async function querySwarmMessages(
|
|
1003
|
-
projectKey: string,
|
|
1004
|
-
threadId: string,
|
|
1005
|
-
): Promise<number> {
|
|
1006
|
-
// Check if agent-mail is available
|
|
1007
|
-
const agentMailAvailable = await isToolAvailable("agent-mail");
|
|
1008
|
-
if (!agentMailAvailable) {
|
|
1009
|
-
// Don't warn here - it's checked elsewhere
|
|
1010
|
-
return 0;
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
try {
|
|
1014
|
-
// Use embedded swarm-mail inbox to count messages in thread
|
|
1015
|
-
const inbox = await getSwarmInbox({
|
|
1016
|
-
projectPath: projectKey,
|
|
1017
|
-
agentName: "coordinator", // Dummy agent name for thread query
|
|
1018
|
-
limit: 5,
|
|
1019
|
-
includeBodies: false,
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
// Count messages that match the thread ID
|
|
1023
|
-
const threadMessages = inbox.messages.filter(
|
|
1024
|
-
(m) => m.thread_id === threadId,
|
|
1025
|
-
);
|
|
1026
|
-
return threadMessages.length;
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
// Thread might not exist yet, or query failed
|
|
1029
|
-
console.warn(
|
|
1030
|
-
`[swarm] Failed to query swarm messages for thread ${threadId}:`,
|
|
1031
|
-
error,
|
|
1032
|
-
);
|
|
1033
|
-
return 0;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Format a progress message for Agent Mail
|
|
1039
|
-
*/
|
|
1040
|
-
function formatProgressMessage(progress: AgentProgress): string {
|
|
1041
|
-
const lines = [
|
|
1042
|
-
`**Status**: ${progress.status}`,
|
|
1043
|
-
progress.progress_percent !== undefined
|
|
1044
|
-
? `**Progress**: ${progress.progress_percent}%`
|
|
1045
|
-
: null,
|
|
1046
|
-
progress.message ? `**Message**: ${progress.message}` : null,
|
|
1047
|
-
progress.files_touched && progress.files_touched.length > 0
|
|
1048
|
-
? `**Files touched**:\n${progress.files_touched.map((f) => `- \`${f}\``).join("\n")}`
|
|
1049
|
-
: null,
|
|
1050
|
-
progress.blockers && progress.blockers.length > 0
|
|
1051
|
-
? `**Blockers**:\n${progress.blockers.map((b) => `- ${b}`).join("\n")}`
|
|
1052
|
-
: null,
|
|
1053
|
-
];
|
|
1054
|
-
|
|
1055
|
-
return lines.filter(Boolean).join("\n\n");
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// ============================================================================
|
|
1059
|
-
// CASS History Integration
|
|
1060
|
-
// ============================================================================
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* CASS search result from similar past tasks
|
|
1064
|
-
*/
|
|
1065
|
-
interface CassSearchResult {
|
|
1066
|
-
query: string;
|
|
1067
|
-
results: Array<{
|
|
1068
|
-
source_path: string;
|
|
1069
|
-
line: number;
|
|
1070
|
-
agent: string;
|
|
1071
|
-
preview: string;
|
|
1072
|
-
score: number;
|
|
1073
|
-
}>;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
/**
|
|
1077
|
-
* CASS query result with status
|
|
1078
|
-
*/
|
|
1079
|
-
type CassQueryResult =
|
|
1080
|
-
| { status: "unavailable" }
|
|
1081
|
-
| { status: "failed"; error?: string }
|
|
1082
|
-
| { status: "empty"; query: string }
|
|
1083
|
-
| { status: "success"; data: CassSearchResult };
|
|
1084
|
-
|
|
1085
|
-
/**
|
|
1086
|
-
* Query CASS for similar past tasks
|
|
1087
|
-
*
|
|
1088
|
-
* @param task - Task description to search for
|
|
1089
|
-
* @param limit - Maximum results to return
|
|
1090
|
-
* @returns Structured result with status indicator
|
|
1091
|
-
*/
|
|
1092
|
-
async function queryCassHistory(
|
|
1093
|
-
task: string,
|
|
1094
|
-
limit: number = 3,
|
|
1095
|
-
): Promise<CassQueryResult> {
|
|
1096
|
-
// Check if CASS is available first
|
|
1097
|
-
const cassAvailable = await isToolAvailable("cass");
|
|
1098
|
-
if (!cassAvailable) {
|
|
1099
|
-
warnMissingTool("cass");
|
|
1100
|
-
return { status: "unavailable" };
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
try {
|
|
1104
|
-
const result = await Bun.$`cass search ${task} --limit ${limit} --json`
|
|
1105
|
-
.quiet()
|
|
1106
|
-
.nothrow();
|
|
1107
|
-
|
|
1108
|
-
if (result.exitCode !== 0) {
|
|
1109
|
-
const error = result.stderr.toString();
|
|
1110
|
-
console.warn(
|
|
1111
|
-
`[swarm] CASS search failed (exit ${result.exitCode}):`,
|
|
1112
|
-
error,
|
|
1113
|
-
);
|
|
1114
|
-
return { status: "failed", error };
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const output = result.stdout.toString();
|
|
1118
|
-
if (!output.trim()) {
|
|
1119
|
-
return { status: "empty", query: task };
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
try {
|
|
1123
|
-
const parsed = JSON.parse(output);
|
|
1124
|
-
const searchResult: CassSearchResult = {
|
|
1125
|
-
query: task,
|
|
1126
|
-
results: Array.isArray(parsed) ? parsed : parsed.results || [],
|
|
1127
|
-
};
|
|
1128
|
-
|
|
1129
|
-
if (searchResult.results.length === 0) {
|
|
1130
|
-
return { status: "empty", query: task };
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return { status: "success", data: searchResult };
|
|
1134
|
-
} catch (error) {
|
|
1135
|
-
console.warn(`[swarm] Failed to parse CASS output:`, error);
|
|
1136
|
-
return { status: "failed", error: String(error) };
|
|
1137
|
-
}
|
|
1138
|
-
} catch (error) {
|
|
1139
|
-
console.error(`[swarm] CASS query error:`, error);
|
|
1140
|
-
return { status: "failed", error: String(error) };
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
/**
|
|
1145
|
-
* Format CASS history for inclusion in decomposition prompt
|
|
1146
|
-
*/
|
|
1147
|
-
function formatCassHistoryForPrompt(history: CassSearchResult): string {
|
|
1148
|
-
if (history.results.length === 0) {
|
|
1149
|
-
return "";
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
const lines = [
|
|
1153
|
-
"## Similar Past Tasks",
|
|
1154
|
-
"",
|
|
1155
|
-
"These similar tasks were found in agent history:",
|
|
1156
|
-
"",
|
|
1157
|
-
...history.results.slice(0, 3).map((r, i) => {
|
|
1158
|
-
const preview = r.preview.slice(0, 200).replace(/\n/g, " ");
|
|
1159
|
-
return `${i + 1}. [${r.agent}] ${preview}...`;
|
|
1160
|
-
}),
|
|
1161
|
-
"",
|
|
1162
|
-
"Consider patterns that worked in these past tasks.",
|
|
1163
|
-
"",
|
|
1164
|
-
];
|
|
1165
|
-
|
|
1166
|
-
return lines.join("\n");
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// ============================================================================
|
|
1170
|
-
// Tool Definitions
|
|
1171
|
-
// ============================================================================
|
|
1172
|
-
|
|
1173
|
-
/**
|
|
1174
|
-
* Select the best decomposition strategy for a task
|
|
1175
|
-
*
|
|
1176
|
-
* Analyzes task description and recommends a strategy with reasoning.
|
|
1177
|
-
* Use this before swarm_plan_prompt to understand the recommended approach.
|
|
1178
|
-
*/
|
|
1179
|
-
export const swarm_select_strategy = tool({
|
|
1180
|
-
description:
|
|
1181
|
-
"Analyze task and recommend decomposition strategy (file-based, feature-based, or risk-based)",
|
|
1182
|
-
args: {
|
|
1183
|
-
task: tool.schema.string().min(1).describe("Task description to analyze"),
|
|
1184
|
-
codebase_context: tool.schema
|
|
1185
|
-
.string()
|
|
1186
|
-
.optional()
|
|
1187
|
-
.describe("Optional codebase context (file structure, tech stack, etc.)"),
|
|
1188
|
-
},
|
|
1189
|
-
async execute(args) {
|
|
1190
|
-
const result = selectStrategy(args.task);
|
|
1191
|
-
|
|
1192
|
-
// Enhance reasoning with codebase context if provided
|
|
1193
|
-
let enhancedReasoning = result.reasoning;
|
|
1194
|
-
if (args.codebase_context) {
|
|
1195
|
-
enhancedReasoning += `\n\nCodebase context considered: ${args.codebase_context.slice(0, 200)}...`;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
return JSON.stringify(
|
|
1199
|
-
{
|
|
1200
|
-
strategy: result.strategy,
|
|
1201
|
-
confidence: Math.round(result.confidence * 100) / 100,
|
|
1202
|
-
reasoning: enhancedReasoning,
|
|
1203
|
-
description: STRATEGIES[result.strategy].description,
|
|
1204
|
-
guidelines: STRATEGIES[result.strategy].guidelines,
|
|
1205
|
-
anti_patterns: STRATEGIES[result.strategy].antiPatterns,
|
|
1206
|
-
alternatives: result.alternatives.map((alt) => ({
|
|
1207
|
-
strategy: alt.strategy,
|
|
1208
|
-
description: STRATEGIES[alt.strategy].description,
|
|
1209
|
-
score: alt.score,
|
|
1210
|
-
})),
|
|
1211
|
-
},
|
|
1212
|
-
null,
|
|
1213
|
-
2,
|
|
1214
|
-
);
|
|
1215
|
-
},
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
/**
|
|
1219
|
-
* Strategy-specific decomposition prompt template
|
|
1220
|
-
*/
|
|
1221
|
-
const STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
|
|
1222
|
-
|
|
1223
|
-
## Task
|
|
1224
|
-
{task}
|
|
1225
|
-
|
|
1226
|
-
{strategy_guidelines}
|
|
1227
|
-
|
|
1228
|
-
{context_section}
|
|
1229
|
-
|
|
1230
|
-
{cass_history}
|
|
1231
|
-
|
|
1232
|
-
{skills_context}
|
|
1233
|
-
|
|
1234
|
-
## MANDATORY: Beads Issue Tracking
|
|
1235
|
-
|
|
1236
|
-
**Every subtask MUST become a bead.** This is non-negotiable.
|
|
1237
|
-
|
|
1238
|
-
After decomposition, the coordinator will:
|
|
1239
|
-
1. Create an epic bead for the overall task
|
|
1240
|
-
2. Create child beads for each subtask
|
|
1241
|
-
3. Track progress through bead status updates
|
|
1242
|
-
4. Close beads with summaries when complete
|
|
1243
|
-
|
|
1244
|
-
Agents MUST update their bead status as they work. No silent progress.
|
|
1245
|
-
|
|
1246
|
-
## Requirements
|
|
1247
|
-
|
|
1248
|
-
1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
|
|
1249
|
-
2. **Assign files** - each subtask must specify which files it will modify
|
|
1250
|
-
3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
|
|
1251
|
-
4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
|
|
1252
|
-
5. **Estimate complexity** - 1 (trivial) to 5 (complex)
|
|
1253
|
-
6. **Plan aggressively** - break down more than you think necessary, smaller is better
|
|
1254
|
-
|
|
1255
|
-
## Response Format
|
|
1256
|
-
|
|
1257
|
-
Respond with a JSON object matching this schema:
|
|
1258
|
-
|
|
1259
|
-
\`\`\`typescript
|
|
1260
|
-
{
|
|
1261
|
-
epic: {
|
|
1262
|
-
title: string, // Epic title for the beads tracker
|
|
1263
|
-
description?: string // Brief description of the overall goal
|
|
1264
|
-
},
|
|
1265
|
-
subtasks: [
|
|
1266
|
-
{
|
|
1267
|
-
title: string, // What this subtask accomplishes
|
|
1268
|
-
description?: string, // Detailed instructions for the agent
|
|
1269
|
-
files: string[], // Files this subtask will modify (globs allowed)
|
|
1270
|
-
dependencies: number[], // Indices of subtasks this depends on (0-indexed)
|
|
1271
|
-
estimated_complexity: 1-5 // Effort estimate
|
|
1272
|
-
},
|
|
1273
|
-
// ... more subtasks
|
|
1274
|
-
]
|
|
1275
|
-
}
|
|
1276
|
-
\`\`\`
|
|
1277
|
-
|
|
1278
|
-
Now decompose the task:`;
|
|
1279
|
-
|
|
1280
|
-
/**
|
|
1281
|
-
* Generate a strategy-specific planning prompt
|
|
1282
|
-
*
|
|
1283
|
-
* Higher-level than swarm_decompose - includes strategy selection and guidelines.
|
|
1284
|
-
* Use this when you want the full planning experience with strategy-specific advice.
|
|
1285
|
-
*/
|
|
1286
|
-
export const swarm_plan_prompt = tool({
|
|
1287
|
-
description:
|
|
1288
|
-
"Generate strategy-specific decomposition prompt. Auto-selects strategy or uses provided one. Queries CASS for similar tasks.",
|
|
1289
|
-
args: {
|
|
1290
|
-
task: tool.schema.string().min(1).describe("Task description to decompose"),
|
|
1291
|
-
strategy: tool.schema
|
|
1292
|
-
.enum(["file-based", "feature-based", "risk-based", "auto"])
|
|
1293
|
-
.optional()
|
|
1294
|
-
.describe("Decomposition strategy (default: auto-detect)"),
|
|
1295
|
-
max_subtasks: tool.schema
|
|
1296
|
-
.number()
|
|
1297
|
-
.int()
|
|
1298
|
-
.min(2)
|
|
1299
|
-
|
|
1300
|
-
.default(5)
|
|
1301
|
-
.describe("Maximum number of subtasks (default: 5)"),
|
|
1302
|
-
context: tool.schema
|
|
1303
|
-
.string()
|
|
1304
|
-
.optional()
|
|
1305
|
-
.describe("Additional context (codebase info, constraints, etc.)"),
|
|
1306
|
-
query_cass: tool.schema
|
|
1307
|
-
.boolean()
|
|
1308
|
-
.optional()
|
|
1309
|
-
.describe("Query CASS for similar past tasks (default: true)"),
|
|
1310
|
-
cass_limit: tool.schema
|
|
1311
|
-
.number()
|
|
1312
|
-
.int()
|
|
1313
|
-
.min(1)
|
|
1314
|
-
|
|
1315
|
-
.optional()
|
|
1316
|
-
.describe("Max CASS results to include (default: 3)"),
|
|
1317
|
-
include_skills: tool.schema
|
|
1318
|
-
.boolean()
|
|
1319
|
-
.optional()
|
|
1320
|
-
.describe("Include available skills in context (default: true)"),
|
|
1321
|
-
},
|
|
1322
|
-
async execute(args) {
|
|
1323
|
-
// Select strategy
|
|
1324
|
-
let selectedStrategy: Exclude<DecompositionStrategy, "auto">;
|
|
1325
|
-
let strategyReasoning: string;
|
|
1326
|
-
|
|
1327
|
-
if (args.strategy && args.strategy !== "auto") {
|
|
1328
|
-
selectedStrategy = args.strategy;
|
|
1329
|
-
strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
|
|
1330
|
-
} else {
|
|
1331
|
-
const selection = selectStrategy(args.task);
|
|
1332
|
-
selectedStrategy = selection.strategy;
|
|
1333
|
-
strategyReasoning = selection.reasoning;
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
// Query CASS for similar past tasks
|
|
1337
|
-
let cassContext = "";
|
|
1338
|
-
let cassResultInfo: {
|
|
1339
|
-
queried: boolean;
|
|
1340
|
-
results_found?: number;
|
|
1341
|
-
included_in_context?: boolean;
|
|
1342
|
-
reason?: string;
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
if (args.query_cass !== false) {
|
|
1346
|
-
const cassResult = await queryCassHistory(
|
|
1347
|
-
args.task,
|
|
1348
|
-
args.cass_limit ?? 3,
|
|
1349
|
-
);
|
|
1350
|
-
if (cassResult.status === "success") {
|
|
1351
|
-
cassContext = formatCassHistoryForPrompt(cassResult.data);
|
|
1352
|
-
cassResultInfo = {
|
|
1353
|
-
queried: true,
|
|
1354
|
-
results_found: cassResult.data.results.length,
|
|
1355
|
-
included_in_context: true,
|
|
1356
|
-
};
|
|
1357
|
-
} else {
|
|
1358
|
-
cassResultInfo = {
|
|
1359
|
-
queried: true,
|
|
1360
|
-
results_found: 0,
|
|
1361
|
-
included_in_context: false,
|
|
1362
|
-
reason: cassResult.status,
|
|
1363
|
-
};
|
|
1364
|
-
}
|
|
1365
|
-
} else {
|
|
1366
|
-
cassResultInfo = { queried: false, reason: "disabled" };
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
// Fetch skills context
|
|
1370
|
-
let skillsContext = "";
|
|
1371
|
-
let skillsInfo: { included: boolean; count?: number; relevant?: string[] } =
|
|
1372
|
-
{
|
|
1373
|
-
included: false,
|
|
1374
|
-
};
|
|
1375
|
-
|
|
1376
|
-
if (args.include_skills !== false) {
|
|
1377
|
-
const allSkills = await listSkills();
|
|
1378
|
-
if (allSkills.length > 0) {
|
|
1379
|
-
skillsContext = await getSkillsContextForSwarm();
|
|
1380
|
-
const relevantSkills = await findRelevantSkills(args.task);
|
|
1381
|
-
skillsInfo = {
|
|
1382
|
-
included: true,
|
|
1383
|
-
count: allSkills.length,
|
|
1384
|
-
relevant: relevantSkills,
|
|
1385
|
-
};
|
|
1386
|
-
|
|
1387
|
-
// Add suggestion for relevant skills
|
|
1388
|
-
if (relevantSkills.length > 0) {
|
|
1389
|
-
skillsContext += `\n\n**Suggested skills for this task**: ${relevantSkills.join(", ")}`;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
// Format strategy guidelines
|
|
1395
|
-
const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
|
|
1396
|
-
|
|
1397
|
-
// Combine user context
|
|
1398
|
-
const contextSection = args.context
|
|
1399
|
-
? `## Additional Context\n${args.context}`
|
|
1400
|
-
: "## Additional Context\n(none provided)";
|
|
1401
|
-
|
|
1402
|
-
// Build the prompt
|
|
1403
|
-
const prompt = STRATEGY_DECOMPOSITION_PROMPT.replace("{task}", args.task)
|
|
1404
|
-
.replace("{strategy_guidelines}", strategyGuidelines)
|
|
1405
|
-
.replace("{context_section}", contextSection)
|
|
1406
|
-
.replace("{cass_history}", cassContext || "")
|
|
1407
|
-
.replace("{skills_context}", skillsContext || "")
|
|
1408
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
|
|
1409
|
-
|
|
1410
|
-
return JSON.stringify(
|
|
1411
|
-
{
|
|
1412
|
-
prompt,
|
|
1413
|
-
strategy: {
|
|
1414
|
-
selected: selectedStrategy,
|
|
1415
|
-
reasoning: strategyReasoning,
|
|
1416
|
-
guidelines: STRATEGIES[selectedStrategy].guidelines,
|
|
1417
|
-
anti_patterns: STRATEGIES[selectedStrategy].antiPatterns,
|
|
1418
|
-
},
|
|
1419
|
-
expected_schema: "BeadTree",
|
|
1420
|
-
schema_hint: {
|
|
1421
|
-
epic: { title: "string", description: "string?" },
|
|
1422
|
-
subtasks: [
|
|
1423
|
-
{
|
|
1424
|
-
title: "string",
|
|
1425
|
-
description: "string?",
|
|
1426
|
-
files: "string[]",
|
|
1427
|
-
dependencies: "number[]",
|
|
1428
|
-
estimated_complexity: "1-5",
|
|
1429
|
-
},
|
|
1430
|
-
],
|
|
1431
|
-
},
|
|
1432
|
-
validation_note:
|
|
1433
|
-
"Parse agent response as JSON and validate with swarm_validate_decomposition",
|
|
1434
|
-
cass_history: cassResultInfo,
|
|
1435
|
-
skills: skillsInfo,
|
|
1436
|
-
// Add semantic-memory query instruction
|
|
1437
|
-
memory_query: formatMemoryQueryForDecomposition(args.task, 3),
|
|
1438
|
-
},
|
|
1439
|
-
null,
|
|
1440
|
-
2,
|
|
1441
|
-
);
|
|
1442
|
-
},
|
|
1443
|
-
});
|
|
1444
|
-
|
|
1445
|
-
/**
|
|
1446
|
-
* Delegate task decomposition to a swarm/planner subagent
|
|
1447
|
-
*
|
|
1448
|
-
* Returns a prompt for spawning a planner agent that will handle all decomposition
|
|
1449
|
-
* reasoning. This keeps the coordinator context lean by offloading:
|
|
1450
|
-
* - Strategy selection
|
|
1451
|
-
* - CASS queries
|
|
1452
|
-
* - Skills discovery
|
|
1453
|
-
* - File analysis
|
|
1454
|
-
* - BeadTree generation
|
|
1455
|
-
*
|
|
1456
|
-
* The planner returns ONLY structured BeadTree JSON, which the coordinator
|
|
1457
|
-
* validates and uses to create beads.
|
|
1458
|
-
*
|
|
1459
|
-
* @example
|
|
1460
|
-
* ```typescript
|
|
1461
|
-
* // Coordinator workflow:
|
|
1462
|
-
* const delegateResult = await swarm_delegate_planning({
|
|
1463
|
-
* task: "Add user authentication",
|
|
1464
|
-
* context: "Next.js 14 app",
|
|
1465
|
-
* });
|
|
1466
|
-
*
|
|
1467
|
-
* // Parse the result
|
|
1468
|
-
* const { prompt, subagent_type } = JSON.parse(delegateResult);
|
|
1469
|
-
*
|
|
1470
|
-
* // Spawn subagent using Task tool
|
|
1471
|
-
* const plannerResponse = await Task(prompt, subagent_type);
|
|
1472
|
-
*
|
|
1473
|
-
* // Validate the response
|
|
1474
|
-
* await swarm_validate_decomposition({ response: plannerResponse });
|
|
1475
|
-
* ```
|
|
1476
|
-
*/
|
|
1477
|
-
export const swarm_delegate_planning = tool({
|
|
1478
|
-
description:
|
|
1479
|
-
"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.",
|
|
1480
|
-
args: {
|
|
1481
|
-
task: tool.schema.string().min(1).describe("The task to decompose"),
|
|
1482
|
-
context: tool.schema
|
|
1483
|
-
.string()
|
|
1484
|
-
.optional()
|
|
1485
|
-
.describe("Additional context to include"),
|
|
1486
|
-
max_subtasks: tool.schema
|
|
1487
|
-
.number()
|
|
1488
|
-
.int()
|
|
1489
|
-
.min(2)
|
|
1490
|
-
.max(10)
|
|
1491
|
-
.optional()
|
|
1492
|
-
.default(5)
|
|
1493
|
-
.describe("Maximum number of subtasks (default: 5)"),
|
|
1494
|
-
strategy: tool.schema
|
|
1495
|
-
.enum(["auto", "file-based", "feature-based", "risk-based"])
|
|
1496
|
-
.optional()
|
|
1497
|
-
.default("auto")
|
|
1498
|
-
.describe("Decomposition strategy (default: auto-detect)"),
|
|
1499
|
-
query_cass: tool.schema
|
|
1500
|
-
.boolean()
|
|
1501
|
-
.optional()
|
|
1502
|
-
.default(true)
|
|
1503
|
-
.describe("Query CASS for similar past tasks (default: true)"),
|
|
1504
|
-
},
|
|
1505
|
-
async execute(args) {
|
|
1506
|
-
// Select strategy
|
|
1507
|
-
let selectedStrategy: Exclude<DecompositionStrategy, "auto">;
|
|
1508
|
-
let strategyReasoning: string;
|
|
1509
|
-
|
|
1510
|
-
if (args.strategy && args.strategy !== "auto") {
|
|
1511
|
-
selectedStrategy = args.strategy;
|
|
1512
|
-
strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
|
|
1513
|
-
} else {
|
|
1514
|
-
const selection = selectStrategy(args.task);
|
|
1515
|
-
selectedStrategy = selection.strategy;
|
|
1516
|
-
strategyReasoning = selection.reasoning;
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
// Query CASS for similar past tasks
|
|
1520
|
-
let cassContext = "";
|
|
1521
|
-
let cassResultInfo: {
|
|
1522
|
-
queried: boolean;
|
|
1523
|
-
results_found?: number;
|
|
1524
|
-
included_in_context?: boolean;
|
|
1525
|
-
reason?: string;
|
|
1526
|
-
};
|
|
1527
|
-
|
|
1528
|
-
if (args.query_cass !== false) {
|
|
1529
|
-
const cassResult = await queryCassHistory(args.task, 3);
|
|
1530
|
-
if (cassResult.status === "success") {
|
|
1531
|
-
cassContext = formatCassHistoryForPrompt(cassResult.data);
|
|
1532
|
-
cassResultInfo = {
|
|
1533
|
-
queried: true,
|
|
1534
|
-
results_found: cassResult.data.results.length,
|
|
1535
|
-
included_in_context: true,
|
|
1536
|
-
};
|
|
1537
|
-
} else {
|
|
1538
|
-
cassResultInfo = {
|
|
1539
|
-
queried: true,
|
|
1540
|
-
results_found: 0,
|
|
1541
|
-
included_in_context: false,
|
|
1542
|
-
reason: cassResult.status,
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
} else {
|
|
1546
|
-
cassResultInfo = { queried: false, reason: "disabled" };
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// Fetch skills context
|
|
1550
|
-
let skillsContext = "";
|
|
1551
|
-
let skillsInfo: { included: boolean; count?: number; relevant?: string[] } =
|
|
1552
|
-
{
|
|
1553
|
-
included: false,
|
|
1554
|
-
};
|
|
1555
|
-
|
|
1556
|
-
const allSkills = await listSkills();
|
|
1557
|
-
if (allSkills.length > 0) {
|
|
1558
|
-
skillsContext = await getSkillsContextForSwarm();
|
|
1559
|
-
const relevantSkills = await findRelevantSkills(args.task);
|
|
1560
|
-
skillsInfo = {
|
|
1561
|
-
included: true,
|
|
1562
|
-
count: allSkills.length,
|
|
1563
|
-
relevant: relevantSkills,
|
|
1564
|
-
};
|
|
1565
|
-
|
|
1566
|
-
// Add suggestion for relevant skills
|
|
1567
|
-
if (relevantSkills.length > 0) {
|
|
1568
|
-
skillsContext += `\n\n**Suggested skills for this task**: ${relevantSkills.join(", ")}`;
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
// Format strategy guidelines
|
|
1573
|
-
const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
|
|
1574
|
-
|
|
1575
|
-
// Combine user context
|
|
1576
|
-
const contextSection = args.context
|
|
1577
|
-
? `## Additional Context\n${args.context}`
|
|
1578
|
-
: "## Additional Context\n(none provided)";
|
|
1579
|
-
|
|
1580
|
-
// Build the planning prompt with clear instructions for JSON-only output
|
|
1581
|
-
const planningPrompt = STRATEGY_DECOMPOSITION_PROMPT.replace(
|
|
1582
|
-
"{task}",
|
|
1583
|
-
args.task,
|
|
1584
|
-
)
|
|
1585
|
-
.replace("{strategy_guidelines}", strategyGuidelines)
|
|
1586
|
-
.replace("{context_section}", contextSection)
|
|
1587
|
-
.replace("{cass_history}", cassContext || "")
|
|
1588
|
-
.replace("{skills_context}", skillsContext || "")
|
|
1589
|
-
.replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
|
|
1590
|
-
|
|
1591
|
-
// Add strict JSON-only instructions for the subagent
|
|
1592
|
-
const subagentInstructions = `
|
|
1593
|
-
## CRITICAL: Output Format
|
|
1594
|
-
|
|
1595
|
-
You are a planner subagent. Your ONLY output must be valid JSON matching the BeadTree schema.
|
|
1596
|
-
|
|
1597
|
-
DO NOT include:
|
|
1598
|
-
- Explanatory text before or after the JSON
|
|
1599
|
-
- Markdown code fences (\`\`\`json)
|
|
1600
|
-
- Commentary or reasoning
|
|
1601
|
-
|
|
1602
|
-
OUTPUT ONLY the raw JSON object.
|
|
1603
|
-
|
|
1604
|
-
## Example Output
|
|
1605
|
-
|
|
1606
|
-
{
|
|
1607
|
-
"epic": {
|
|
1608
|
-
"title": "Add user authentication",
|
|
1609
|
-
"description": "Implement OAuth-based authentication system"
|
|
1610
|
-
},
|
|
1611
|
-
"subtasks": [
|
|
1612
|
-
{
|
|
1613
|
-
"title": "Set up OAuth provider",
|
|
1614
|
-
"description": "Configure OAuth client credentials and redirect URLs",
|
|
1615
|
-
"files": ["src/auth/oauth.ts", "src/config/auth.ts"],
|
|
1616
|
-
"dependencies": [],
|
|
1617
|
-
"estimated_complexity": 2
|
|
1618
|
-
},
|
|
1619
|
-
{
|
|
1620
|
-
"title": "Create auth routes",
|
|
1621
|
-
"description": "Implement login, logout, and callback routes",
|
|
1622
|
-
"files": ["src/app/api/auth/[...nextauth]/route.ts"],
|
|
1623
|
-
"dependencies": [0],
|
|
1624
|
-
"estimated_complexity": 3
|
|
1625
|
-
}
|
|
1626
|
-
]
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
Now generate the BeadTree for the given task.`;
|
|
1630
|
-
|
|
1631
|
-
const fullPrompt = `${planningPrompt}\n\n${subagentInstructions}`;
|
|
1632
|
-
|
|
1633
|
-
// Return structured output for coordinator
|
|
1634
|
-
return JSON.stringify(
|
|
1635
|
-
{
|
|
1636
|
-
prompt: fullPrompt,
|
|
1637
|
-
subagent_type: "swarm/planner",
|
|
1638
|
-
description: "Task decomposition planning",
|
|
1639
|
-
strategy: {
|
|
1640
|
-
selected: selectedStrategy,
|
|
1641
|
-
reasoning: strategyReasoning,
|
|
1642
|
-
},
|
|
1643
|
-
expected_output: "BeadTree JSON (raw JSON, no markdown)",
|
|
1644
|
-
next_steps: [
|
|
1645
|
-
"1. Spawn subagent with Task tool using returned prompt",
|
|
1646
|
-
"2. Parse subagent response as JSON",
|
|
1647
|
-
"3. Validate with swarm_validate_decomposition",
|
|
1648
|
-
"4. Create beads with beads_create_epic",
|
|
1649
|
-
],
|
|
1650
|
-
cass_history: cassResultInfo,
|
|
1651
|
-
skills: skillsInfo,
|
|
1652
|
-
// Add semantic-memory query instruction
|
|
1653
|
-
memory_query: formatMemoryQueryForDecomposition(args.task, 3),
|
|
1654
|
-
},
|
|
1655
|
-
null,
|
|
1656
|
-
2,
|
|
1657
|
-
);
|
|
1658
|
-
},
|
|
1659
|
-
});
|
|
1660
|
-
|
|
1661
|
-
/**
|
|
1662
|
-
* Decompose a task into a bead tree
|
|
1663
|
-
*
|
|
1664
|
-
* This is a PROMPT tool - it returns a prompt for the agent to respond to.
|
|
1665
|
-
* The agent's response (JSON) should be validated with BeadTreeSchema.
|
|
1666
|
-
*
|
|
1667
|
-
* Optionally queries CASS for similar past tasks to inform decomposition.
|
|
1668
|
-
*/
|
|
1669
|
-
export const swarm_decompose = tool({
|
|
1670
|
-
description:
|
|
1671
|
-
"Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
|
|
1672
|
-
args: {
|
|
1673
|
-
task: tool.schema.string().min(1).describe("Task description to decompose"),
|
|
1674
|
-
max_subtasks: tool.schema
|
|
1675
|
-
.number()
|
|
1676
|
-
.int()
|
|
1677
|
-
.min(2)
|
|
1678
|
-
|
|
1679
|
-
.default(5)
|
|
1680
|
-
.describe("Maximum number of subtasks (default: 5)"),
|
|
1681
|
-
context: tool.schema
|
|
1682
|
-
.string()
|
|
1683
|
-
.optional()
|
|
1684
|
-
.describe("Additional context (codebase info, constraints, etc.)"),
|
|
1685
|
-
query_cass: tool.schema
|
|
1686
|
-
.boolean()
|
|
1687
|
-
.optional()
|
|
1688
|
-
.describe("Query CASS for similar past tasks (default: true)"),
|
|
1689
|
-
cass_limit: tool.schema
|
|
1690
|
-
.number()
|
|
1691
|
-
.int()
|
|
1692
|
-
.min(1)
|
|
1693
|
-
|
|
1694
|
-
.optional()
|
|
1695
|
-
.describe("Max CASS results to include (default: 3)"),
|
|
1696
|
-
},
|
|
1697
|
-
async execute(args) {
|
|
1698
|
-
// Query CASS for similar past tasks
|
|
1699
|
-
let cassContext = "";
|
|
1700
|
-
let cassResultInfo: {
|
|
1701
|
-
queried: boolean;
|
|
1702
|
-
results_found?: number;
|
|
1703
|
-
included_in_context?: boolean;
|
|
1704
|
-
reason?: string;
|
|
1705
|
-
};
|
|
1706
|
-
|
|
1707
|
-
if (args.query_cass !== false) {
|
|
1708
|
-
const cassResult = await queryCassHistory(
|
|
1709
|
-
args.task,
|
|
1710
|
-
args.cass_limit ?? 3,
|
|
1711
|
-
);
|
|
1712
|
-
if (cassResult.status === "success") {
|
|
1713
|
-
cassContext = formatCassHistoryForPrompt(cassResult.data);
|
|
1714
|
-
cassResultInfo = {
|
|
1715
|
-
queried: true,
|
|
1716
|
-
results_found: cassResult.data.results.length,
|
|
1717
|
-
included_in_context: true,
|
|
1718
|
-
};
|
|
1719
|
-
} else {
|
|
1720
|
-
cassResultInfo = {
|
|
1721
|
-
queried: true,
|
|
1722
|
-
results_found: 0,
|
|
1723
|
-
included_in_context: false,
|
|
1724
|
-
reason: cassResult.status,
|
|
1725
|
-
};
|
|
1726
|
-
}
|
|
1727
|
-
} else {
|
|
1728
|
-
cassResultInfo = { queried: false, reason: "disabled" };
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// Combine user context with CASS history
|
|
1732
|
-
const fullContext = [args.context, cassContext]
|
|
1733
|
-
.filter(Boolean)
|
|
1734
|
-
.join("\n\n");
|
|
1735
|
-
|
|
1736
|
-
const prompt = formatDecompositionPrompt(
|
|
1737
|
-
args.task,
|
|
1738
|
-
args.max_subtasks ?? 5,
|
|
1739
|
-
fullContext || undefined,
|
|
1740
|
-
);
|
|
1741
|
-
|
|
1742
|
-
// Return the prompt and schema info for the caller
|
|
1743
|
-
return JSON.stringify(
|
|
1744
|
-
{
|
|
1745
|
-
prompt,
|
|
1746
|
-
expected_schema: "BeadTree",
|
|
1747
|
-
schema_hint: {
|
|
1748
|
-
epic: { title: "string", description: "string?" },
|
|
1749
|
-
subtasks: [
|
|
1750
|
-
{
|
|
1751
|
-
title: "string",
|
|
1752
|
-
description: "string?",
|
|
1753
|
-
files: "string[]",
|
|
1754
|
-
dependencies: "number[]",
|
|
1755
|
-
estimated_complexity: "1-5",
|
|
1756
|
-
},
|
|
1757
|
-
],
|
|
1758
|
-
},
|
|
1759
|
-
validation_note:
|
|
1760
|
-
"Parse agent response as JSON and validate with BeadTreeSchema from schemas/bead.ts",
|
|
1761
|
-
cass_history: cassResultInfo,
|
|
1762
|
-
// Add semantic-memory query instruction
|
|
1763
|
-
memory_query: formatMemoryQueryForDecomposition(args.task, 3),
|
|
1764
|
-
},
|
|
1765
|
-
null,
|
|
1766
|
-
2,
|
|
1767
|
-
);
|
|
1768
|
-
},
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
/**
|
|
1772
|
-
* Validate a decomposition response from an agent
|
|
1773
|
-
*
|
|
1774
|
-
* Use this after the agent responds to swarm:decompose to validate the structure.
|
|
1775
|
-
*/
|
|
1776
|
-
export const swarm_validate_decomposition = tool({
|
|
1777
|
-
description: "Validate a decomposition response against BeadTreeSchema",
|
|
1778
|
-
args: {
|
|
1779
|
-
response: tool.schema
|
|
1780
|
-
.string()
|
|
1781
|
-
.describe("JSON response from agent (BeadTree format)"),
|
|
1782
|
-
},
|
|
1783
|
-
async execute(args) {
|
|
1784
|
-
try {
|
|
1785
|
-
const parsed = JSON.parse(args.response);
|
|
1786
|
-
const validated = BeadTreeSchema.parse(parsed);
|
|
1787
|
-
|
|
1788
|
-
// Additional validation: check for file conflicts
|
|
1789
|
-
const allFiles = new Set<string>();
|
|
1790
|
-
const conflicts: string[] = [];
|
|
1791
|
-
|
|
1792
|
-
for (const subtask of validated.subtasks) {
|
|
1793
|
-
for (const file of subtask.files) {
|
|
1794
|
-
if (allFiles.has(file)) {
|
|
1795
|
-
conflicts.push(file);
|
|
1796
|
-
}
|
|
1797
|
-
allFiles.add(file);
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
if (conflicts.length > 0) {
|
|
1802
|
-
return JSON.stringify(
|
|
1803
|
-
{
|
|
1804
|
-
valid: false,
|
|
1805
|
-
error: `File conflicts detected: ${conflicts.join(", ")}`,
|
|
1806
|
-
hint: "Each file can only be assigned to one subtask",
|
|
1807
|
-
},
|
|
1808
|
-
null,
|
|
1809
|
-
2,
|
|
1810
|
-
);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
// Check dependency indices are valid
|
|
1814
|
-
for (let i = 0; i < validated.subtasks.length; i++) {
|
|
1815
|
-
const deps = validated.subtasks[i].dependencies;
|
|
1816
|
-
for (const dep of deps) {
|
|
1817
|
-
// Check bounds first
|
|
1818
|
-
if (dep < 0 || dep >= validated.subtasks.length) {
|
|
1819
|
-
return JSON.stringify(
|
|
1820
|
-
{
|
|
1821
|
-
valid: false,
|
|
1822
|
-
error: `Invalid dependency: subtask ${i} depends on ${dep}, but only ${validated.subtasks.length} subtasks exist (indices 0-${validated.subtasks.length - 1})`,
|
|
1823
|
-
hint: "Dependency index is out of bounds",
|
|
1824
|
-
},
|
|
1825
|
-
null,
|
|
1826
|
-
2,
|
|
1827
|
-
);
|
|
1828
|
-
}
|
|
1829
|
-
// Check forward references
|
|
1830
|
-
if (dep >= i) {
|
|
1831
|
-
return JSON.stringify(
|
|
1832
|
-
{
|
|
1833
|
-
valid: false,
|
|
1834
|
-
error: `Invalid dependency: subtask ${i} depends on ${dep}, but dependencies must be earlier in the array`,
|
|
1835
|
-
hint: "Reorder subtasks so dependencies come before dependents",
|
|
1836
|
-
},
|
|
1837
|
-
null,
|
|
1838
|
-
2,
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
// Check for instruction conflicts between subtasks
|
|
1845
|
-
const instructionConflicts = detectInstructionConflicts(
|
|
1846
|
-
validated.subtasks,
|
|
1847
|
-
);
|
|
1848
|
-
|
|
1849
|
-
return JSON.stringify(
|
|
1850
|
-
{
|
|
1851
|
-
valid: true,
|
|
1852
|
-
bead_tree: validated,
|
|
1853
|
-
stats: {
|
|
1854
|
-
subtask_count: validated.subtasks.length,
|
|
1855
|
-
total_files: allFiles.size,
|
|
1856
|
-
total_complexity: validated.subtasks.reduce(
|
|
1857
|
-
(sum, s) => sum + s.estimated_complexity,
|
|
1858
|
-
0,
|
|
1859
|
-
),
|
|
1860
|
-
},
|
|
1861
|
-
// Include conflicts as warnings (not blocking)
|
|
1862
|
-
warnings:
|
|
1863
|
-
instructionConflicts.length > 0
|
|
1864
|
-
? {
|
|
1865
|
-
instruction_conflicts: instructionConflicts,
|
|
1866
|
-
hint: "Review these potential conflicts between subtask instructions",
|
|
1867
|
-
}
|
|
1868
|
-
: undefined,
|
|
1869
|
-
},
|
|
1870
|
-
null,
|
|
1871
|
-
2,
|
|
1872
|
-
);
|
|
1873
|
-
} catch (error) {
|
|
1874
|
-
if (error instanceof z.ZodError) {
|
|
1875
|
-
return JSON.stringify(
|
|
1876
|
-
{
|
|
1877
|
-
valid: false,
|
|
1878
|
-
error: "Schema validation failed",
|
|
1879
|
-
details: error.issues,
|
|
1880
|
-
},
|
|
1881
|
-
null,
|
|
1882
|
-
2,
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
if (error instanceof SyntaxError) {
|
|
1886
|
-
return JSON.stringify(
|
|
1887
|
-
{
|
|
1888
|
-
valid: false,
|
|
1889
|
-
error: "Invalid JSON",
|
|
1890
|
-
details: error.message,
|
|
1891
|
-
},
|
|
1892
|
-
null,
|
|
1893
|
-
2,
|
|
1894
|
-
);
|
|
1895
|
-
}
|
|
1896
|
-
throw error;
|
|
1897
|
-
}
|
|
1898
|
-
},
|
|
1899
|
-
});
|
|
1900
|
-
|
|
1901
|
-
/**
|
|
1902
|
-
* Get status of a swarm by epic ID
|
|
1903
|
-
*
|
|
1904
|
-
* Requires project_key to query Agent Mail for message counts.
|
|
1905
|
-
*/
|
|
1906
|
-
export const swarm_status = tool({
|
|
1907
|
-
description: "Get status of a swarm by epic ID",
|
|
1908
|
-
args: {
|
|
1909
|
-
epic_id: tool.schema.string().describe("Epic bead ID (e.g., bd-abc123)"),
|
|
1910
|
-
project_key: tool.schema
|
|
1911
|
-
.string()
|
|
1912
|
-
.describe("Project path (for Agent Mail queries)"),
|
|
1913
|
-
},
|
|
1914
|
-
async execute(args) {
|
|
1915
|
-
// Query subtasks from beads
|
|
1916
|
-
const subtasks = await queryEpicSubtasks(args.epic_id);
|
|
1917
|
-
|
|
1918
|
-
// Count statuses
|
|
1919
|
-
const statusCounts = {
|
|
1920
|
-
running: 0,
|
|
1921
|
-
completed: 0,
|
|
1922
|
-
failed: 0,
|
|
1923
|
-
blocked: 0,
|
|
1924
|
-
};
|
|
1925
|
-
|
|
1926
|
-
const agents: SpawnedAgent[] = [];
|
|
1927
|
-
|
|
1928
|
-
for (const bead of subtasks) {
|
|
1929
|
-
// Map bead status to agent status
|
|
1930
|
-
let agentStatus: SpawnedAgent["status"] = "pending";
|
|
1931
|
-
switch (bead.status) {
|
|
1932
|
-
case "in_progress":
|
|
1933
|
-
agentStatus = "running";
|
|
1934
|
-
statusCounts.running++;
|
|
1935
|
-
break;
|
|
1936
|
-
case "closed":
|
|
1937
|
-
agentStatus = "completed";
|
|
1938
|
-
statusCounts.completed++;
|
|
1939
|
-
break;
|
|
1940
|
-
case "blocked":
|
|
1941
|
-
agentStatus = "pending"; // Blocked treated as pending for swarm
|
|
1942
|
-
statusCounts.blocked++;
|
|
1943
|
-
break;
|
|
1944
|
-
default:
|
|
1945
|
-
// open = pending
|
|
1946
|
-
break;
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
agents.push({
|
|
1950
|
-
bead_id: bead.id,
|
|
1951
|
-
agent_name: "", // We don't track this in beads
|
|
1952
|
-
status: agentStatus,
|
|
1953
|
-
files: [], // Would need to parse from description
|
|
1954
|
-
});
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
// Query Agent Mail for message activity
|
|
1958
|
-
const messageCount = await querySwarmMessages(
|
|
1959
|
-
args.project_key,
|
|
1960
|
-
args.epic_id,
|
|
1961
|
-
);
|
|
1962
|
-
|
|
1963
|
-
const status: SwarmStatus = {
|
|
1964
|
-
epic_id: args.epic_id,
|
|
1965
|
-
total_agents: subtasks.length,
|
|
1966
|
-
running: statusCounts.running,
|
|
1967
|
-
completed: statusCounts.completed,
|
|
1968
|
-
failed: statusCounts.failed,
|
|
1969
|
-
blocked: statusCounts.blocked,
|
|
1970
|
-
agents,
|
|
1971
|
-
last_update: new Date().toISOString(),
|
|
1972
|
-
};
|
|
1973
|
-
|
|
1974
|
-
// Validate and return
|
|
1975
|
-
const validated = SwarmStatusSchema.parse(status);
|
|
1976
|
-
|
|
1977
|
-
return JSON.stringify(
|
|
1978
|
-
{
|
|
1979
|
-
...validated,
|
|
1980
|
-
message_count: messageCount,
|
|
1981
|
-
progress_percent:
|
|
1982
|
-
subtasks.length > 0
|
|
1983
|
-
? Math.round((statusCounts.completed / subtasks.length) * 100)
|
|
1984
|
-
: 0,
|
|
1985
|
-
},
|
|
1986
|
-
null,
|
|
1987
|
-
2,
|
|
1988
|
-
);
|
|
1989
|
-
},
|
|
1990
|
-
});
|
|
1991
|
-
|
|
1992
|
-
/**
|
|
1993
|
-
* Report progress on a subtask
|
|
1994
|
-
*
|
|
1995
|
-
* Takes explicit agent identity since tools don't have persistent state.
|
|
1996
|
-
*/
|
|
1997
|
-
export const swarm_progress = tool({
|
|
1998
|
-
description: "Report progress on a subtask to coordinator",
|
|
1999
|
-
args: {
|
|
2000
|
-
project_key: tool.schema.string().describe("Project path"),
|
|
2001
|
-
agent_name: tool.schema.string().describe("Your Agent Mail name"),
|
|
2002
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
2003
|
-
status: tool.schema
|
|
2004
|
-
.enum(["in_progress", "blocked", "completed", "failed"])
|
|
2005
|
-
.describe("Current status"),
|
|
2006
|
-
message: tool.schema
|
|
2007
|
-
.string()
|
|
2008
|
-
.optional()
|
|
2009
|
-
.describe("Progress message or blockers"),
|
|
2010
|
-
progress_percent: tool.schema
|
|
2011
|
-
.number()
|
|
2012
|
-
.min(0)
|
|
2013
|
-
.max(100)
|
|
2014
|
-
.optional()
|
|
2015
|
-
.describe("Completion percentage"),
|
|
2016
|
-
files_touched: tool.schema
|
|
2017
|
-
.array(tool.schema.string())
|
|
2018
|
-
.optional()
|
|
2019
|
-
.describe("Files modified so far"),
|
|
2020
|
-
},
|
|
2021
|
-
async execute(args) {
|
|
2022
|
-
// Build progress report
|
|
2023
|
-
const progress: AgentProgress = {
|
|
2024
|
-
bead_id: args.bead_id,
|
|
2025
|
-
agent_name: args.agent_name,
|
|
2026
|
-
status: args.status,
|
|
2027
|
-
progress_percent: args.progress_percent,
|
|
2028
|
-
message: args.message,
|
|
2029
|
-
files_touched: args.files_touched,
|
|
2030
|
-
timestamp: new Date().toISOString(),
|
|
2031
|
-
};
|
|
2032
|
-
|
|
2033
|
-
// Validate
|
|
2034
|
-
const validated = AgentProgressSchema.parse(progress);
|
|
2035
|
-
|
|
2036
|
-
// Update bead status if needed
|
|
2037
|
-
if (args.status === "blocked" || args.status === "in_progress") {
|
|
2038
|
-
const beadStatus = args.status === "blocked" ? "blocked" : "in_progress";
|
|
2039
|
-
await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`
|
|
2040
|
-
.quiet()
|
|
2041
|
-
.nothrow();
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
// Extract epic ID from bead ID (e.g., bd-abc123.1 -> bd-abc123)
|
|
2045
|
-
const epicId = args.bead_id.includes(".")
|
|
2046
|
-
? args.bead_id.split(".")[0]
|
|
2047
|
-
: args.bead_id;
|
|
2048
|
-
|
|
2049
|
-
// Send progress message to thread using embedded swarm-mail
|
|
2050
|
-
await sendSwarmMessage({
|
|
2051
|
-
projectPath: args.project_key,
|
|
2052
|
-
fromAgent: args.agent_name,
|
|
2053
|
-
toAgents: [], // Coordinator will pick it up from thread
|
|
2054
|
-
subject: `Progress: ${args.bead_id} - ${args.status}`,
|
|
2055
|
-
body: formatProgressMessage(validated),
|
|
2056
|
-
threadId: epicId,
|
|
2057
|
-
importance: args.status === "blocked" ? "high" : "normal",
|
|
2058
|
-
});
|
|
2059
|
-
|
|
2060
|
-
return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}`;
|
|
2061
|
-
},
|
|
2062
|
-
});
|
|
2063
|
-
|
|
2064
|
-
/**
|
|
2065
|
-
* UBS scan result schema
|
|
2066
|
-
*/
|
|
2067
|
-
interface UbsScanResult {
|
|
2068
|
-
exitCode: number;
|
|
2069
|
-
bugs: Array<{
|
|
2070
|
-
file: string;
|
|
2071
|
-
line: number;
|
|
2072
|
-
severity: string;
|
|
2073
|
-
message: string;
|
|
2074
|
-
category: string;
|
|
2075
|
-
}>;
|
|
2076
|
-
summary: {
|
|
2077
|
-
total: number;
|
|
2078
|
-
critical: number;
|
|
2079
|
-
high: number;
|
|
2080
|
-
medium: number;
|
|
2081
|
-
low: number;
|
|
2082
|
-
};
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
// ============================================================================
|
|
2086
|
-
// Verification Gate
|
|
2087
|
-
// ============================================================================
|
|
2088
|
-
|
|
2089
|
-
/**
|
|
2090
|
-
* Verification Gate result - tracks each verification step
|
|
2091
|
-
*
|
|
2092
|
-
* Based on the Gate Function from superpowers:
|
|
2093
|
-
* 1. IDENTIFY: What command proves this claim?
|
|
2094
|
-
* 2. RUN: Execute the FULL command (fresh, complete)
|
|
2095
|
-
* 3. READ: Full output, check exit code, count failures
|
|
2096
|
-
* 4. VERIFY: Does output confirm the claim?
|
|
2097
|
-
* 5. ONLY THEN: Make the claim
|
|
2098
|
-
*/
|
|
2099
|
-
interface VerificationStep {
|
|
2100
|
-
name: string;
|
|
2101
|
-
command: string;
|
|
2102
|
-
passed: boolean;
|
|
2103
|
-
exitCode: number;
|
|
2104
|
-
output?: string;
|
|
2105
|
-
error?: string;
|
|
2106
|
-
skipped?: boolean;
|
|
2107
|
-
skipReason?: string;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
interface VerificationGateResult {
|
|
2111
|
-
passed: boolean;
|
|
2112
|
-
steps: VerificationStep[];
|
|
2113
|
-
summary: string;
|
|
2114
|
-
blockers: string[];
|
|
2115
|
-
}
|
|
2116
|
-
|
|
2117
|
-
/**
|
|
2118
|
-
* Run typecheck verification
|
|
2119
|
-
*
|
|
2120
|
-
* Attempts to run TypeScript type checking on the project.
|
|
2121
|
-
* Falls back gracefully if tsc is not available.
|
|
2122
|
-
*/
|
|
2123
|
-
async function runTypecheckVerification(): Promise<VerificationStep> {
|
|
2124
|
-
const step: VerificationStep = {
|
|
2125
|
-
name: "typecheck",
|
|
2126
|
-
command: "tsc --noEmit",
|
|
2127
|
-
passed: false,
|
|
2128
|
-
exitCode: -1,
|
|
2129
|
-
};
|
|
2130
|
-
|
|
2131
|
-
try {
|
|
2132
|
-
// Check if tsconfig.json exists in current directory
|
|
2133
|
-
const tsconfigExists = await Bun.file("tsconfig.json").exists();
|
|
2134
|
-
if (!tsconfigExists) {
|
|
2135
|
-
step.skipped = true;
|
|
2136
|
-
step.skipReason = "No tsconfig.json found";
|
|
2137
|
-
step.passed = true; // Don't block if no TypeScript
|
|
2138
|
-
return step;
|
|
2139
|
-
}
|
|
2140
|
-
|
|
2141
|
-
const result = await Bun.$`tsc --noEmit`.quiet().nothrow();
|
|
2142
|
-
step.exitCode = result.exitCode;
|
|
2143
|
-
step.passed = result.exitCode === 0;
|
|
2144
|
-
|
|
2145
|
-
if (!step.passed) {
|
|
2146
|
-
step.error = result.stderr.toString().slice(0, 1000); // Truncate for context
|
|
2147
|
-
step.output = result.stdout.toString().slice(0, 1000);
|
|
2148
|
-
}
|
|
2149
|
-
} catch (error) {
|
|
2150
|
-
step.skipped = true;
|
|
2151
|
-
step.skipReason = `tsc not available: ${error instanceof Error ? error.message : String(error)}`;
|
|
2152
|
-
step.passed = true; // Don't block if tsc unavailable
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
return step;
|
|
2156
|
-
}
|
|
2157
|
-
|
|
2158
|
-
/**
|
|
2159
|
-
* Run test verification for specific files
|
|
2160
|
-
*
|
|
2161
|
-
* Attempts to find and run tests related to the touched files.
|
|
2162
|
-
* Uses common test patterns (*.test.ts, *.spec.ts, __tests__/).
|
|
2163
|
-
*/
|
|
2164
|
-
async function runTestVerification(
|
|
2165
|
-
filesTouched: string[],
|
|
2166
|
-
): Promise<VerificationStep> {
|
|
2167
|
-
const step: VerificationStep = {
|
|
2168
|
-
name: "tests",
|
|
2169
|
-
command: "bun test <related-files>",
|
|
2170
|
-
passed: false,
|
|
2171
|
-
exitCode: -1,
|
|
2172
|
-
};
|
|
2173
|
-
|
|
2174
|
-
if (filesTouched.length === 0) {
|
|
2175
|
-
step.skipped = true;
|
|
2176
|
-
step.skipReason = "No files touched";
|
|
2177
|
-
step.passed = true;
|
|
2178
|
-
return step;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// Find test files related to touched files
|
|
2182
|
-
const testPatterns: string[] = [];
|
|
2183
|
-
for (const file of filesTouched) {
|
|
2184
|
-
// Skip if already a test file
|
|
2185
|
-
if (file.includes(".test.") || file.includes(".spec.")) {
|
|
2186
|
-
testPatterns.push(file);
|
|
2187
|
-
continue;
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
// Look for corresponding test file
|
|
2191
|
-
const baseName = file.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
2192
|
-
testPatterns.push(`${baseName}.test.ts`);
|
|
2193
|
-
testPatterns.push(`${baseName}.test.tsx`);
|
|
2194
|
-
testPatterns.push(`${baseName}.spec.ts`);
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// Check if any test files exist
|
|
2198
|
-
const existingTests: string[] = [];
|
|
2199
|
-
for (const pattern of testPatterns) {
|
|
2200
|
-
try {
|
|
2201
|
-
const exists = await Bun.file(pattern).exists();
|
|
2202
|
-
if (exists) {
|
|
2203
|
-
existingTests.push(pattern);
|
|
2204
|
-
}
|
|
2205
|
-
} catch {
|
|
2206
|
-
// File doesn't exist, skip
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
|
|
2210
|
-
if (existingTests.length === 0) {
|
|
2211
|
-
step.skipped = true;
|
|
2212
|
-
step.skipReason = "No related test files found";
|
|
2213
|
-
step.passed = true;
|
|
2214
|
-
return step;
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
try {
|
|
2218
|
-
step.command = `bun test ${existingTests.join(" ")}`;
|
|
2219
|
-
const result = await Bun.$`bun test ${existingTests}`.quiet().nothrow();
|
|
2220
|
-
step.exitCode = result.exitCode;
|
|
2221
|
-
step.passed = result.exitCode === 0;
|
|
2222
|
-
|
|
2223
|
-
if (!step.passed) {
|
|
2224
|
-
step.error = result.stderr.toString().slice(0, 1000);
|
|
2225
|
-
step.output = result.stdout.toString().slice(0, 1000);
|
|
2226
|
-
}
|
|
2227
|
-
} catch (error) {
|
|
2228
|
-
step.skipped = true;
|
|
2229
|
-
step.skipReason = `Test runner failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2230
|
-
step.passed = true; // Don't block if test runner unavailable
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
return step;
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
/**
|
|
2237
|
-
* Run the full Verification Gate
|
|
2238
|
-
*
|
|
2239
|
-
* Implements the Gate Function (IDENTIFY → RUN → READ → VERIFY → CLAIM):
|
|
2240
|
-
* 1. UBS scan (already exists)
|
|
2241
|
-
* 2. Typecheck
|
|
2242
|
-
* 3. Tests for touched files
|
|
2243
|
-
*
|
|
2244
|
-
* All steps must pass (or be skipped with valid reason) to proceed.
|
|
2245
|
-
*/
|
|
2246
|
-
async function runVerificationGate(
|
|
2247
|
-
filesTouched: string[],
|
|
2248
|
-
skipUbs: boolean = false,
|
|
2249
|
-
): Promise<VerificationGateResult> {
|
|
2250
|
-
const steps: VerificationStep[] = [];
|
|
2251
|
-
const blockers: string[] = [];
|
|
2252
|
-
|
|
2253
|
-
// Step 1: UBS scan
|
|
2254
|
-
if (!skipUbs && filesTouched.length > 0) {
|
|
2255
|
-
const ubsResult = await runUbsScan(filesTouched);
|
|
2256
|
-
if (ubsResult) {
|
|
2257
|
-
const ubsStep: VerificationStep = {
|
|
2258
|
-
name: "ubs_scan",
|
|
2259
|
-
command: `ubs scan ${filesTouched.join(" ")}`,
|
|
2260
|
-
passed: ubsResult.summary.critical === 0,
|
|
2261
|
-
exitCode: ubsResult.exitCode,
|
|
2262
|
-
};
|
|
2263
|
-
|
|
2264
|
-
if (!ubsStep.passed) {
|
|
2265
|
-
ubsStep.error = `Found ${ubsResult.summary.critical} critical bugs`;
|
|
2266
|
-
blockers.push(
|
|
2267
|
-
`UBS found ${ubsResult.summary.critical} critical bug(s). Try: Run 'ubs scan ${filesTouched.join(" ")}' to see details, fix critical bugs in reported files, or use skip_ubs_scan=true to bypass (not recommended).`,
|
|
2268
|
-
);
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
steps.push(ubsStep);
|
|
2272
|
-
} else {
|
|
2273
|
-
steps.push({
|
|
2274
|
-
name: "ubs_scan",
|
|
2275
|
-
command: "ubs scan",
|
|
2276
|
-
passed: true,
|
|
2277
|
-
exitCode: 0,
|
|
2278
|
-
skipped: true,
|
|
2279
|
-
skipReason: "UBS not available",
|
|
2280
|
-
});
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
// Step 2: Typecheck
|
|
2285
|
-
const typecheckStep = await runTypecheckVerification();
|
|
2286
|
-
steps.push(typecheckStep);
|
|
2287
|
-
if (!typecheckStep.passed && !typecheckStep.skipped) {
|
|
2288
|
-
blockers.push(
|
|
2289
|
-
`Typecheck failed: ${typecheckStep.error?.slice(0, 100) || "type errors found"}. Try: Run 'tsc --noEmit' to see full errors, check tsconfig.json configuration, or fix reported type errors in modified files.`,
|
|
2290
|
-
);
|
|
2291
|
-
}
|
|
2292
|
-
|
|
2293
|
-
// Step 3: Tests
|
|
2294
|
-
const testStep = await runTestVerification(filesTouched);
|
|
2295
|
-
steps.push(testStep);
|
|
2296
|
-
if (!testStep.passed && !testStep.skipped) {
|
|
2297
|
-
blockers.push(
|
|
2298
|
-
`Tests failed: ${testStep.error?.slice(0, 100) || "test failures"}. Try: Run 'bun test ${testStep.command.split(" ").slice(2).join(" ")}' to see full output, check test assertions, or fix failing tests in modified files.`,
|
|
2299
|
-
);
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
// Build summary
|
|
2303
|
-
const passedCount = steps.filter((s) => s.passed).length;
|
|
2304
|
-
const skippedCount = steps.filter((s) => s.skipped).length;
|
|
2305
|
-
const failedCount = steps.filter((s) => !s.passed && !s.skipped).length;
|
|
2306
|
-
|
|
2307
|
-
const summary =
|
|
2308
|
-
failedCount === 0
|
|
2309
|
-
? `Verification passed: ${passedCount} checks passed, ${skippedCount} skipped`
|
|
2310
|
-
: `Verification FAILED: ${failedCount} checks failed, ${passedCount} passed, ${skippedCount} skipped`;
|
|
2311
|
-
|
|
2312
|
-
return {
|
|
2313
|
-
passed: failedCount === 0,
|
|
2314
|
-
steps,
|
|
2315
|
-
summary,
|
|
2316
|
-
blockers,
|
|
2317
|
-
};
|
|
2318
|
-
}
|
|
2319
|
-
|
|
2320
|
-
/**
|
|
2321
|
-
* Run UBS scan on files before completion
|
|
2322
|
-
*
|
|
2323
|
-
* @param files - Files to scan
|
|
2324
|
-
* @returns Scan result or null if UBS not available
|
|
2325
|
-
*/
|
|
2326
|
-
async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
|
|
2327
|
-
if (files.length === 0) {
|
|
2328
|
-
return null;
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// Check if UBS is available first
|
|
2332
|
-
const ubsAvailable = await isToolAvailable("ubs");
|
|
2333
|
-
if (!ubsAvailable) {
|
|
2334
|
-
warnMissingTool("ubs");
|
|
2335
|
-
return null;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
try {
|
|
2339
|
-
// Run UBS scan with JSON output
|
|
2340
|
-
const result = await Bun.$`ubs scan ${files.join(" ")} --json`
|
|
2341
|
-
.quiet()
|
|
2342
|
-
.nothrow();
|
|
2343
|
-
|
|
2344
|
-
const output = result.stdout.toString();
|
|
2345
|
-
if (!output.trim()) {
|
|
2346
|
-
return {
|
|
2347
|
-
exitCode: result.exitCode,
|
|
2348
|
-
bugs: [],
|
|
2349
|
-
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
|
|
2353
|
-
try {
|
|
2354
|
-
const parsed = JSON.parse(output);
|
|
2355
|
-
|
|
2356
|
-
// Basic validation of structure
|
|
2357
|
-
if (typeof parsed !== "object" || parsed === null) {
|
|
2358
|
-
throw new Error("UBS output is not an object");
|
|
2359
|
-
}
|
|
2360
|
-
if (!Array.isArray(parsed.bugs)) {
|
|
2361
|
-
console.warn("[swarm] UBS output missing bugs array, using empty");
|
|
2362
|
-
}
|
|
2363
|
-
if (typeof parsed.summary !== "object" || parsed.summary === null) {
|
|
2364
|
-
console.warn("[swarm] UBS output missing summary object, using empty");
|
|
2365
|
-
}
|
|
2366
|
-
|
|
2367
|
-
return {
|
|
2368
|
-
exitCode: result.exitCode,
|
|
2369
|
-
bugs: Array.isArray(parsed.bugs) ? parsed.bugs : [],
|
|
2370
|
-
summary: parsed.summary || {
|
|
2371
|
-
total: 0,
|
|
2372
|
-
critical: 0,
|
|
2373
|
-
high: 0,
|
|
2374
|
-
medium: 0,
|
|
2375
|
-
low: 0,
|
|
2376
|
-
},
|
|
2377
|
-
};
|
|
2378
|
-
} catch (error) {
|
|
2379
|
-
// UBS output wasn't JSON - this is an error condition
|
|
2380
|
-
console.error(
|
|
2381
|
-
`[swarm] CRITICAL: UBS scan failed to parse JSON output because output is malformed:`,
|
|
2382
|
-
error,
|
|
2383
|
-
);
|
|
2384
|
-
console.error(
|
|
2385
|
-
`[swarm] Raw output: ${output}. Try: Run 'ubs doctor' to check installation, verify UBS version with 'ubs --version' (need v1.0.0+), or check if UBS supports --json flag.`,
|
|
2386
|
-
);
|
|
2387
|
-
return {
|
|
2388
|
-
exitCode: result.exitCode,
|
|
2389
|
-
bugs: [],
|
|
2390
|
-
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
|
|
2391
|
-
};
|
|
2392
|
-
}
|
|
2393
|
-
} catch {
|
|
2394
|
-
return null;
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
/**
|
|
2399
|
-
* Broadcast context updates to all agents in the epic
|
|
2400
|
-
*
|
|
2401
|
-
* Enables mid-task coordination by sharing discoveries, warnings, or blockers
|
|
2402
|
-
* with all agents working on the same epic. Agents can broadcast without
|
|
2403
|
-
* waiting for task completion.
|
|
2404
|
-
*
|
|
2405
|
-
* Based on "Patterns for Building AI Agents" p.31: "Ensure subagents can share context along the way"
|
|
2406
|
-
*/
|
|
2407
|
-
export const swarm_broadcast = tool({
|
|
2408
|
-
description:
|
|
2409
|
-
"Broadcast context update to all agents working on the same epic",
|
|
2410
|
-
args: {
|
|
2411
|
-
project_path: tool.schema
|
|
2412
|
-
.string()
|
|
2413
|
-
.describe("Absolute path to project root"),
|
|
2414
|
-
agent_name: tool.schema
|
|
2415
|
-
.string()
|
|
2416
|
-
.describe("Name of the agent broadcasting the message"),
|
|
2417
|
-
epic_id: tool.schema.string().describe("Epic ID (e.g., bd-abc123)"),
|
|
2418
|
-
message: tool.schema
|
|
2419
|
-
.string()
|
|
2420
|
-
.describe("Context update to share (what changed, what was learned)"),
|
|
2421
|
-
importance: tool.schema
|
|
2422
|
-
.enum(["info", "warning", "blocker"])
|
|
2423
|
-
.default("info")
|
|
2424
|
-
.describe("Priority level (default: info)"),
|
|
2425
|
-
files_affected: tool.schema
|
|
2426
|
-
.array(tool.schema.string())
|
|
2427
|
-
.optional()
|
|
2428
|
-
.describe("Files this context relates to"),
|
|
2429
|
-
},
|
|
2430
|
-
async execute(args, ctx) {
|
|
2431
|
-
// Extract bead_id from context if available (for traceability)
|
|
2432
|
-
const beadId = (ctx as { beadId?: string }).beadId || "unknown";
|
|
2433
|
-
|
|
2434
|
-
// Format the broadcast message
|
|
2435
|
-
const body = [
|
|
2436
|
-
`## Context Update`,
|
|
2437
|
-
"",
|
|
2438
|
-
`**From**: ${args.agent_name} (${beadId})`,
|
|
2439
|
-
`**Priority**: ${args.importance.toUpperCase()}`,
|
|
2440
|
-
"",
|
|
2441
|
-
args.message,
|
|
2442
|
-
"",
|
|
2443
|
-
args.files_affected && args.files_affected.length > 0
|
|
2444
|
-
? `**Files affected**:\n${args.files_affected.map((f) => `- \`${f}\``).join("\n")}`
|
|
2445
|
-
: "",
|
|
2446
|
-
]
|
|
2447
|
-
.filter(Boolean)
|
|
2448
|
-
.join("\n");
|
|
2449
|
-
|
|
2450
|
-
// Map importance to Agent Mail importance
|
|
2451
|
-
const mailImportance =
|
|
2452
|
-
args.importance === "blocker"
|
|
2453
|
-
? "urgent"
|
|
2454
|
-
: args.importance === "warning"
|
|
2455
|
-
? "high"
|
|
2456
|
-
: "normal";
|
|
2457
|
-
|
|
2458
|
-
// Send as broadcast to thread using embedded swarm-mail
|
|
2459
|
-
await sendSwarmMessage({
|
|
2460
|
-
projectPath: args.project_path,
|
|
2461
|
-
fromAgent: args.agent_name,
|
|
2462
|
-
toAgents: [], // Broadcast to thread
|
|
2463
|
-
subject: `[${args.importance.toUpperCase()}] Context update from ${args.agent_name}`,
|
|
2464
|
-
body,
|
|
2465
|
-
threadId: args.epic_id,
|
|
2466
|
-
importance: mailImportance,
|
|
2467
|
-
ackRequired: args.importance === "blocker",
|
|
2468
|
-
});
|
|
2469
|
-
|
|
2470
|
-
return JSON.stringify(
|
|
2471
|
-
{
|
|
2472
|
-
broadcast: true,
|
|
2473
|
-
epic_id: args.epic_id,
|
|
2474
|
-
from: args.agent_name,
|
|
2475
|
-
bead_id: beadId,
|
|
2476
|
-
importance: args.importance,
|
|
2477
|
-
recipients: "all agents in epic",
|
|
2478
|
-
ack_required: args.importance === "blocker",
|
|
2479
|
-
},
|
|
2480
|
-
null,
|
|
2481
|
-
2,
|
|
2482
|
-
);
|
|
2483
|
-
},
|
|
2484
|
-
});
|
|
2485
|
-
|
|
2486
|
-
/**
|
|
2487
|
-
* Mark a subtask as complete
|
|
2488
|
-
*
|
|
2489
|
-
* Implements the Verification Gate (from superpowers):
|
|
2490
|
-
* 1. IDENTIFY: What commands prove this claim?
|
|
2491
|
-
* 2. RUN: Execute verification (UBS, typecheck, tests)
|
|
2492
|
-
* 3. READ: Check exit codes and output
|
|
2493
|
-
* 4. VERIFY: All checks must pass
|
|
2494
|
-
* 5. ONLY THEN: Close the bead
|
|
2495
|
-
*
|
|
2496
|
-
* Closes bead, releases reservations, notifies coordinator.
|
|
2497
|
-
*/
|
|
2498
|
-
export const swarm_complete = tool({
|
|
2499
|
-
description:
|
|
2500
|
-
"Mark subtask complete with Verification Gate. Runs UBS scan, typecheck, and tests before allowing completion.",
|
|
2501
|
-
args: {
|
|
2502
|
-
project_key: tool.schema.string().describe("Project path"),
|
|
2503
|
-
agent_name: tool.schema.string().describe("Your Agent Mail name"),
|
|
2504
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
2505
|
-
summary: tool.schema.string().describe("Brief summary of work done"),
|
|
2506
|
-
evaluation: tool.schema
|
|
2507
|
-
.string()
|
|
2508
|
-
.optional()
|
|
2509
|
-
.describe("Self-evaluation JSON (Evaluation schema)"),
|
|
2510
|
-
files_touched: tool.schema
|
|
2511
|
-
.array(tool.schema.string())
|
|
2512
|
-
.optional()
|
|
2513
|
-
.describe("Files modified - will be verified (UBS, typecheck, tests)"),
|
|
2514
|
-
skip_ubs_scan: tool.schema
|
|
2515
|
-
.boolean()
|
|
2516
|
-
.optional()
|
|
2517
|
-
.describe("Skip UBS bug scan (default: false)"),
|
|
2518
|
-
skip_verification: tool.schema
|
|
2519
|
-
.boolean()
|
|
2520
|
-
.optional()
|
|
2521
|
-
.describe(
|
|
2522
|
-
"Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)",
|
|
2523
|
-
),
|
|
2524
|
-
},
|
|
2525
|
-
async execute(args) {
|
|
2526
|
-
// Run Verification Gate unless explicitly skipped
|
|
2527
|
-
let verificationResult: VerificationGateResult | null = null;
|
|
2528
|
-
|
|
2529
|
-
if (!args.skip_verification && args.files_touched?.length) {
|
|
2530
|
-
verificationResult = await runVerificationGate(
|
|
2531
|
-
args.files_touched,
|
|
2532
|
-
args.skip_ubs_scan ?? false,
|
|
2533
|
-
);
|
|
2534
|
-
|
|
2535
|
-
// Block completion if verification failed
|
|
2536
|
-
if (!verificationResult.passed) {
|
|
2537
|
-
return JSON.stringify(
|
|
2538
|
-
{
|
|
2539
|
-
success: false,
|
|
2540
|
-
error: "Verification Gate FAILED - fix issues before completing",
|
|
2541
|
-
verification: {
|
|
2542
|
-
passed: false,
|
|
2543
|
-
summary: verificationResult.summary,
|
|
2544
|
-
blockers: verificationResult.blockers,
|
|
2545
|
-
steps: verificationResult.steps.map((s) => ({
|
|
2546
|
-
name: s.name,
|
|
2547
|
-
passed: s.passed,
|
|
2548
|
-
skipped: s.skipped,
|
|
2549
|
-
skipReason: s.skipReason,
|
|
2550
|
-
error: s.error?.slice(0, 200),
|
|
2551
|
-
})),
|
|
2552
|
-
},
|
|
2553
|
-
hint:
|
|
2554
|
-
verificationResult.blockers.length > 0
|
|
2555
|
-
? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.`
|
|
2556
|
-
: "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
|
|
2557
|
-
gate_function:
|
|
2558
|
-
"IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)",
|
|
2559
|
-
},
|
|
2560
|
-
null,
|
|
2561
|
-
2,
|
|
2562
|
-
);
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
|
|
2566
|
-
// Legacy UBS-only path for backward compatibility (when no files_touched)
|
|
2567
|
-
let ubsResult: UbsScanResult | null = null;
|
|
2568
|
-
if (
|
|
2569
|
-
!args.skip_verification &&
|
|
2570
|
-
!verificationResult &&
|
|
2571
|
-
args.files_touched?.length &&
|
|
2572
|
-
!args.skip_ubs_scan
|
|
2573
|
-
) {
|
|
2574
|
-
ubsResult = await runUbsScan(args.files_touched);
|
|
2575
|
-
|
|
2576
|
-
// Block completion if critical bugs found
|
|
2577
|
-
if (ubsResult && ubsResult.summary.critical > 0) {
|
|
2578
|
-
return JSON.stringify(
|
|
2579
|
-
{
|
|
2580
|
-
success: false,
|
|
2581
|
-
error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
|
|
2582
|
-
ubs_scan: {
|
|
2583
|
-
critical_count: ubsResult.summary.critical,
|
|
2584
|
-
bugs: ubsResult.bugs.filter((b) => b.severity === "critical"),
|
|
2585
|
-
},
|
|
2586
|
-
hint: `Fix these critical bugs: ${ubsResult.bugs
|
|
2587
|
-
.filter((b) => b.severity === "critical")
|
|
2588
|
-
.map((b) => `${b.file}:${b.line} - ${b.message}`)
|
|
2589
|
-
.slice(0, 3)
|
|
2590
|
-
.join(
|
|
2591
|
-
"; ",
|
|
2592
|
-
)}. Try: Run 'ubs scan ${args.files_touched?.join(" ") || "."} --json' for full report, fix reported issues, or use skip_ubs_scan=true to bypass (not recommended).`,
|
|
2593
|
-
},
|
|
2594
|
-
null,
|
|
2595
|
-
2,
|
|
2596
|
-
);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
// Parse and validate evaluation if provided
|
|
2601
|
-
let parsedEvaluation: Evaluation | undefined;
|
|
2602
|
-
if (args.evaluation) {
|
|
2603
|
-
try {
|
|
2604
|
-
parsedEvaluation = EvaluationSchema.parse(JSON.parse(args.evaluation));
|
|
2605
|
-
} catch (error) {
|
|
2606
|
-
return JSON.stringify(
|
|
2607
|
-
{
|
|
2608
|
-
success: false,
|
|
2609
|
-
error: "Invalid evaluation format",
|
|
2610
|
-
details: error instanceof z.ZodError ? error.issues : String(error),
|
|
2611
|
-
},
|
|
2612
|
-
null,
|
|
2613
|
-
2,
|
|
2614
|
-
);
|
|
2615
|
-
}
|
|
2616
|
-
|
|
2617
|
-
// If evaluation failed, don't complete
|
|
2618
|
-
if (!parsedEvaluation.passed) {
|
|
2619
|
-
return JSON.stringify(
|
|
2620
|
-
{
|
|
2621
|
-
success: false,
|
|
2622
|
-
error: "Self-evaluation failed",
|
|
2623
|
-
retry_suggestion: parsedEvaluation.retry_suggestion,
|
|
2624
|
-
feedback: parsedEvaluation.overall_feedback,
|
|
2625
|
-
},
|
|
2626
|
-
null,
|
|
2627
|
-
2,
|
|
2628
|
-
);
|
|
2629
|
-
}
|
|
2630
|
-
}
|
|
2631
|
-
|
|
2632
|
-
// Close the bead
|
|
2633
|
-
const closeResult =
|
|
2634
|
-
await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
|
|
2635
|
-
.quiet()
|
|
2636
|
-
.nothrow();
|
|
2637
|
-
|
|
2638
|
-
if (closeResult.exitCode !== 0) {
|
|
2639
|
-
throw new SwarmError(
|
|
2640
|
-
`Failed to close bead because bd close command failed: ${closeResult.stderr.toString()}. Try: Verify bead exists and is not already closed with 'bd show ${args.bead_id}', check if bead ID is correct with 'beads_query()', or use beads_close tool directly.`,
|
|
2641
|
-
"complete",
|
|
2642
|
-
);
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
// Release file reservations for this agent using embedded swarm-mail
|
|
2646
|
-
try {
|
|
2647
|
-
await releaseSwarmFiles({
|
|
2648
|
-
projectPath: args.project_key,
|
|
2649
|
-
agentName: args.agent_name,
|
|
2650
|
-
// Release all reservations for this agent
|
|
2651
|
-
});
|
|
2652
|
-
} catch (error) {
|
|
2653
|
-
// Release might fail (e.g., no reservations existed)
|
|
2654
|
-
// This is non-fatal - log and continue
|
|
2655
|
-
console.warn(
|
|
2656
|
-
`[swarm] Failed to release file reservations for ${args.agent_name}:`,
|
|
2657
|
-
error,
|
|
2658
|
-
);
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
// Extract epic ID
|
|
2662
|
-
const epicId = args.bead_id.includes(".")
|
|
2663
|
-
? args.bead_id.split(".")[0]
|
|
2664
|
-
: args.bead_id;
|
|
2665
|
-
|
|
2666
|
-
// Send completion message using embedded swarm-mail
|
|
2667
|
-
const completionBody = [
|
|
2668
|
-
`## Subtask Complete: ${args.bead_id}`,
|
|
2669
|
-
"",
|
|
2670
|
-
`**Summary**: ${args.summary}`,
|
|
2671
|
-
"",
|
|
2672
|
-
parsedEvaluation
|
|
2673
|
-
? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}`
|
|
2674
|
-
: "",
|
|
2675
|
-
parsedEvaluation?.overall_feedback
|
|
2676
|
-
? `**Feedback**: ${parsedEvaluation.overall_feedback}`
|
|
2677
|
-
: "",
|
|
2678
|
-
]
|
|
2679
|
-
.filter(Boolean)
|
|
2680
|
-
.join("\n");
|
|
2681
|
-
|
|
2682
|
-
await sendSwarmMessage({
|
|
2683
|
-
projectPath: args.project_key,
|
|
2684
|
-
fromAgent: args.agent_name,
|
|
2685
|
-
toAgents: [], // Thread broadcast
|
|
2686
|
-
subject: `Complete: ${args.bead_id}`,
|
|
2687
|
-
body: completionBody,
|
|
2688
|
-
threadId: epicId,
|
|
2689
|
-
importance: "normal",
|
|
2690
|
-
});
|
|
2691
|
-
|
|
2692
|
-
// Build success response with semantic-memory integration
|
|
2693
|
-
const response = {
|
|
2694
|
-
success: true,
|
|
2695
|
-
bead_id: args.bead_id,
|
|
2696
|
-
closed: true,
|
|
2697
|
-
reservations_released: true,
|
|
2698
|
-
message_sent: true,
|
|
2699
|
-
verification_gate: verificationResult
|
|
2700
|
-
? {
|
|
2701
|
-
passed: true,
|
|
2702
|
-
summary: verificationResult.summary,
|
|
2703
|
-
steps: verificationResult.steps.map((s) => ({
|
|
2704
|
-
name: s.name,
|
|
2705
|
-
passed: s.passed,
|
|
2706
|
-
skipped: s.skipped,
|
|
2707
|
-
skipReason: s.skipReason,
|
|
2708
|
-
})),
|
|
2709
|
-
}
|
|
2710
|
-
: args.skip_verification
|
|
2711
|
-
? { skipped: true, reason: "skip_verification=true" }
|
|
2712
|
-
: { skipped: true, reason: "no files_touched provided" },
|
|
2713
|
-
ubs_scan: ubsResult
|
|
2714
|
-
? {
|
|
2715
|
-
ran: true,
|
|
2716
|
-
bugs_found: ubsResult.summary.total,
|
|
2717
|
-
summary: ubsResult.summary,
|
|
2718
|
-
warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
|
|
2719
|
-
}
|
|
2720
|
-
: verificationResult
|
|
2721
|
-
? { ran: true, included_in_verification_gate: true }
|
|
2722
|
-
: {
|
|
2723
|
-
ran: false,
|
|
2724
|
-
reason: args.skip_ubs_scan
|
|
2725
|
-
? "skipped"
|
|
2726
|
-
: "no files or ubs unavailable",
|
|
2727
|
-
},
|
|
2728
|
-
learning_prompt: `## Reflection
|
|
2729
|
-
|
|
2730
|
-
Did you learn anything reusable during this subtask? Consider:
|
|
2731
|
-
|
|
2732
|
-
1. **Patterns**: Any code patterns or approaches that worked well?
|
|
2733
|
-
2. **Gotchas**: Edge cases or pitfalls to warn future agents about?
|
|
2734
|
-
3. **Best Practices**: Domain-specific guidelines worth documenting?
|
|
2735
|
-
4. **Tool Usage**: Effective ways to use tools for this type of task?
|
|
2736
|
-
|
|
2737
|
-
If you discovered something valuable, use \`swarm_learn\` or \`skills_create\` to preserve it as a skill for future swarms.
|
|
2738
|
-
|
|
2739
|
-
Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
|
|
2740
|
-
// Add semantic-memory integration on success
|
|
2741
|
-
memory_store: formatMemoryStoreOnSuccess(
|
|
2742
|
-
args.bead_id,
|
|
2743
|
-
args.summary,
|
|
2744
|
-
args.files_touched || [],
|
|
2745
|
-
),
|
|
2746
|
-
};
|
|
2747
|
-
|
|
2748
|
-
return JSON.stringify(response, null, 2);
|
|
2749
|
-
},
|
|
2750
|
-
});
|
|
2751
|
-
|
|
2752
|
-
/**
|
|
2753
|
-
* Classify failure based on error message heuristics
|
|
2754
|
-
*
|
|
2755
|
-
* Simple pattern matching to categorize why a task failed.
|
|
2756
|
-
* Used when failure_mode is not explicitly provided.
|
|
2757
|
-
*
|
|
2758
|
-
* @param error - Error object or message
|
|
2759
|
-
* @returns FailureMode classification
|
|
2760
|
-
*/
|
|
2761
|
-
function classifyFailure(error: Error | string): string {
|
|
2762
|
-
const msg = (typeof error === "string" ? error : error.message).toLowerCase();
|
|
2763
|
-
|
|
2764
|
-
if (msg.includes("timeout")) return "timeout";
|
|
2765
|
-
if (msg.includes("conflict") || msg.includes("reservation"))
|
|
2766
|
-
return "conflict";
|
|
2767
|
-
if (msg.includes("validation") || msg.includes("schema")) return "validation";
|
|
2768
|
-
if (msg.includes("context") || msg.includes("token"))
|
|
2769
|
-
return "context_overflow";
|
|
2770
|
-
if (msg.includes("blocked") || msg.includes("dependency"))
|
|
2771
|
-
return "dependency_blocked";
|
|
2772
|
-
if (msg.includes("cancel")) return "user_cancelled";
|
|
2773
|
-
|
|
2774
|
-
// Check for tool failure patterns
|
|
2775
|
-
if (
|
|
2776
|
-
msg.includes("tool") ||
|
|
2777
|
-
msg.includes("command") ||
|
|
2778
|
-
msg.includes("failed to execute")
|
|
2779
|
-
) {
|
|
2780
|
-
return "tool_failure";
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
return "unknown";
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
/**
|
|
2787
|
-
* Record outcome signals from a completed subtask
|
|
2788
|
-
*
|
|
2789
|
-
* Tracks implicit feedback (duration, errors, retries) to score
|
|
2790
|
-
* decomposition quality over time. This data feeds into criterion
|
|
2791
|
-
* weight calculations.
|
|
2792
|
-
*
|
|
2793
|
-
* Strategy tracking enables learning about which decomposition strategies
|
|
2794
|
-
* work best for different task types.
|
|
2795
|
-
*
|
|
2796
|
-
* @see src/learning.ts for scoring logic
|
|
2797
|
-
*/
|
|
2798
|
-
export const swarm_record_outcome = tool({
|
|
2799
|
-
description:
|
|
2800
|
-
"Record subtask outcome for implicit feedback scoring. Tracks duration, errors, retries to learn decomposition quality.",
|
|
2801
|
-
args: {
|
|
2802
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
2803
|
-
duration_ms: tool.schema
|
|
2804
|
-
.number()
|
|
2805
|
-
.int()
|
|
2806
|
-
.min(0)
|
|
2807
|
-
.describe("Duration in milliseconds"),
|
|
2808
|
-
error_count: tool.schema
|
|
2809
|
-
.number()
|
|
2810
|
-
.int()
|
|
2811
|
-
.min(0)
|
|
2812
|
-
.default(0)
|
|
2813
|
-
.describe("Number of errors encountered"),
|
|
2814
|
-
retry_count: tool.schema
|
|
2815
|
-
.number()
|
|
2816
|
-
.int()
|
|
2817
|
-
.min(0)
|
|
2818
|
-
.default(0)
|
|
2819
|
-
.describe("Number of retry attempts"),
|
|
2820
|
-
success: tool.schema.boolean().describe("Whether the subtask succeeded"),
|
|
2821
|
-
files_touched: tool.schema
|
|
2822
|
-
.array(tool.schema.string())
|
|
2823
|
-
.optional()
|
|
2824
|
-
.describe("Files that were modified"),
|
|
2825
|
-
criteria: tool.schema
|
|
2826
|
-
.array(tool.schema.string())
|
|
2827
|
-
.optional()
|
|
2828
|
-
.describe(
|
|
2829
|
-
"Criteria to generate feedback for (default: all default criteria)",
|
|
2830
|
-
),
|
|
2831
|
-
strategy: tool.schema
|
|
2832
|
-
.enum(["file-based", "feature-based", "risk-based", "research-based"])
|
|
2833
|
-
.optional()
|
|
2834
|
-
.describe("Decomposition strategy used for this task"),
|
|
2835
|
-
failure_mode: tool.schema
|
|
2836
|
-
.enum([
|
|
2837
|
-
"timeout",
|
|
2838
|
-
"conflict",
|
|
2839
|
-
"validation",
|
|
2840
|
-
"tool_failure",
|
|
2841
|
-
"context_overflow",
|
|
2842
|
-
"dependency_blocked",
|
|
2843
|
-
"user_cancelled",
|
|
2844
|
-
"unknown",
|
|
2845
|
-
])
|
|
2846
|
-
.optional()
|
|
2847
|
-
.describe(
|
|
2848
|
-
"Failure classification (only when success=false). Auto-classified if not provided.",
|
|
2849
|
-
),
|
|
2850
|
-
failure_details: tool.schema
|
|
2851
|
-
.string()
|
|
2852
|
-
.optional()
|
|
2853
|
-
.describe("Detailed failure context (error message, stack trace, etc.)"),
|
|
2854
|
-
},
|
|
2855
|
-
async execute(args) {
|
|
2856
|
-
// Build outcome signals
|
|
2857
|
-
const signals: OutcomeSignals = {
|
|
2858
|
-
bead_id: args.bead_id,
|
|
2859
|
-
duration_ms: args.duration_ms,
|
|
2860
|
-
error_count: args.error_count ?? 0,
|
|
2861
|
-
retry_count: args.retry_count ?? 0,
|
|
2862
|
-
success: args.success,
|
|
2863
|
-
files_touched: args.files_touched ?? [],
|
|
2864
|
-
timestamp: new Date().toISOString(),
|
|
2865
|
-
strategy: args.strategy as LearningDecompositionStrategy | undefined,
|
|
2866
|
-
failure_mode: args.failure_mode,
|
|
2867
|
-
failure_details: args.failure_details,
|
|
2868
|
-
};
|
|
2869
|
-
|
|
2870
|
-
// If task failed but no failure_mode provided, try to classify from failure_details
|
|
2871
|
-
if (!args.success && !args.failure_mode && args.failure_details) {
|
|
2872
|
-
signals.failure_mode = classifyFailure(args.failure_details) as any;
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
// Validate signals
|
|
2876
|
-
const validated = OutcomeSignalsSchema.parse(signals);
|
|
2877
|
-
|
|
2878
|
-
// Score the outcome
|
|
2879
|
-
const scored: ScoredOutcome = scoreImplicitFeedback(
|
|
2880
|
-
validated,
|
|
2881
|
-
DEFAULT_LEARNING_CONFIG,
|
|
2882
|
-
);
|
|
2883
|
-
|
|
2884
|
-
// Get error patterns from accumulator
|
|
2885
|
-
const errorStats = await globalErrorAccumulator.getErrorStats(args.bead_id);
|
|
2886
|
-
|
|
2887
|
-
// Generate feedback events for each criterion
|
|
2888
|
-
const criteriaToScore = args.criteria ?? [
|
|
2889
|
-
"type_safe",
|
|
2890
|
-
"no_bugs",
|
|
2891
|
-
"patterns",
|
|
2892
|
-
"readable",
|
|
2893
|
-
];
|
|
2894
|
-
const feedbackEvents: FeedbackEvent[] = criteriaToScore.map((criterion) => {
|
|
2895
|
-
const event = outcomeToFeedback(scored, criterion);
|
|
2896
|
-
// Include strategy in feedback context for future analysis
|
|
2897
|
-
if (args.strategy) {
|
|
2898
|
-
event.context =
|
|
2899
|
-
`${event.context || ""} [strategy: ${args.strategy}]`.trim();
|
|
2900
|
-
}
|
|
2901
|
-
// Include error patterns in feedback context
|
|
2902
|
-
if (errorStats.total > 0) {
|
|
2903
|
-
const errorSummary = Object.entries(errorStats.by_type)
|
|
2904
|
-
.map(([type, count]) => `${type}:${count}`)
|
|
2905
|
-
.join(", ");
|
|
2906
|
-
event.context =
|
|
2907
|
-
`${event.context || ""} [errors: ${errorSummary}]`.trim();
|
|
2908
|
-
}
|
|
2909
|
-
return event;
|
|
2910
|
-
});
|
|
2911
|
-
|
|
2912
|
-
return JSON.stringify(
|
|
2913
|
-
{
|
|
2914
|
-
success: true,
|
|
2915
|
-
outcome: {
|
|
2916
|
-
signals: validated,
|
|
2917
|
-
scored: {
|
|
2918
|
-
type: scored.type,
|
|
2919
|
-
decayed_value: scored.decayed_value,
|
|
2920
|
-
reasoning: scored.reasoning,
|
|
2921
|
-
},
|
|
2922
|
-
},
|
|
2923
|
-
feedback_events: feedbackEvents,
|
|
2924
|
-
error_patterns: errorStats,
|
|
2925
|
-
summary: {
|
|
2926
|
-
feedback_type: scored.type,
|
|
2927
|
-
duration_seconds: Math.round(args.duration_ms / 1000),
|
|
2928
|
-
error_count: args.error_count ?? 0,
|
|
2929
|
-
retry_count: args.retry_count ?? 0,
|
|
2930
|
-
success: args.success,
|
|
2931
|
-
strategy: args.strategy,
|
|
2932
|
-
failure_mode: validated.failure_mode,
|
|
2933
|
-
failure_details: validated.failure_details,
|
|
2934
|
-
accumulated_errors: errorStats.total,
|
|
2935
|
-
unresolved_errors: errorStats.unresolved,
|
|
2936
|
-
},
|
|
2937
|
-
note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights.",
|
|
2938
|
-
},
|
|
2939
|
-
null,
|
|
2940
|
-
2,
|
|
2941
|
-
);
|
|
2942
|
-
},
|
|
2943
|
-
});
|
|
2944
|
-
|
|
2945
|
-
/**
|
|
2946
|
-
* Generate subtask prompt for a spawned agent
|
|
2947
|
-
*/
|
|
2948
|
-
export const swarm_subtask_prompt = tool({
|
|
2949
|
-
description: "Generate the prompt for a spawned subtask agent",
|
|
2950
|
-
args: {
|
|
2951
|
-
agent_name: tool.schema.string().describe("Agent Mail name for the agent"),
|
|
2952
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
2953
|
-
epic_id: tool.schema.string().describe("Epic bead ID"),
|
|
2954
|
-
subtask_title: tool.schema.string().describe("Subtask title"),
|
|
2955
|
-
subtask_description: tool.schema
|
|
2956
|
-
.string()
|
|
2957
|
-
.optional()
|
|
2958
|
-
.describe("Detailed subtask instructions"),
|
|
2959
|
-
files: tool.schema
|
|
2960
|
-
.array(tool.schema.string())
|
|
2961
|
-
.describe("Files assigned to this subtask"),
|
|
2962
|
-
shared_context: tool.schema
|
|
2963
|
-
.string()
|
|
2964
|
-
.optional()
|
|
2965
|
-
.describe("Context shared across all agents"),
|
|
2966
|
-
},
|
|
2967
|
-
async execute(args) {
|
|
2968
|
-
const prompt = formatSubtaskPrompt({
|
|
2969
|
-
agent_name: args.agent_name,
|
|
2970
|
-
bead_id: args.bead_id,
|
|
2971
|
-
epic_id: args.epic_id,
|
|
2972
|
-
subtask_title: args.subtask_title,
|
|
2973
|
-
subtask_description: args.subtask_description || "",
|
|
2974
|
-
files: args.files,
|
|
2975
|
-
shared_context: args.shared_context,
|
|
2976
|
-
});
|
|
2977
|
-
|
|
2978
|
-
return prompt;
|
|
2979
|
-
},
|
|
2980
|
-
});
|
|
2981
|
-
|
|
2982
|
-
/**
|
|
2983
|
-
* Prepare a subtask for spawning with Task tool (V2 prompt)
|
|
2984
|
-
*
|
|
2985
|
-
* Generates a streamlined prompt that tells agents to USE Agent Mail and beads.
|
|
2986
|
-
* Returns JSON that can be directly used with Task tool.
|
|
2987
|
-
*/
|
|
2988
|
-
export const swarm_spawn_subtask = tool({
|
|
2989
|
-
description:
|
|
2990
|
-
"Prepare a subtask for spawning. Returns prompt with Agent Mail/beads instructions.",
|
|
2991
|
-
args: {
|
|
2992
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
2993
|
-
epic_id: tool.schema.string().describe("Parent epic bead ID"),
|
|
2994
|
-
subtask_title: tool.schema.string().describe("Subtask title"),
|
|
2995
|
-
subtask_description: tool.schema
|
|
2996
|
-
.string()
|
|
2997
|
-
.optional()
|
|
2998
|
-
.describe("Detailed subtask instructions"),
|
|
2999
|
-
files: tool.schema
|
|
3000
|
-
.array(tool.schema.string())
|
|
3001
|
-
.describe("Files assigned to this subtask"),
|
|
3002
|
-
shared_context: tool.schema
|
|
3003
|
-
.string()
|
|
3004
|
-
.optional()
|
|
3005
|
-
.describe("Context shared across all agents"),
|
|
3006
|
-
},
|
|
3007
|
-
async execute(args) {
|
|
3008
|
-
const prompt = formatSubtaskPromptV2({
|
|
3009
|
-
bead_id: args.bead_id,
|
|
3010
|
-
epic_id: args.epic_id,
|
|
3011
|
-
subtask_title: args.subtask_title,
|
|
3012
|
-
subtask_description: args.subtask_description || "",
|
|
3013
|
-
files: args.files,
|
|
3014
|
-
shared_context: args.shared_context,
|
|
3015
|
-
});
|
|
3016
|
-
|
|
3017
|
-
return JSON.stringify(
|
|
3018
|
-
{
|
|
3019
|
-
prompt,
|
|
3020
|
-
bead_id: args.bead_id,
|
|
3021
|
-
epic_id: args.epic_id,
|
|
3022
|
-
files: args.files,
|
|
3023
|
-
},
|
|
3024
|
-
null,
|
|
3025
|
-
2,
|
|
3026
|
-
);
|
|
3027
|
-
},
|
|
3028
|
-
});
|
|
3029
|
-
|
|
3030
|
-
/**
|
|
3031
|
-
* Schema for task agent result
|
|
3032
|
-
*/
|
|
3033
|
-
const TaskResultSchema = z.object({
|
|
3034
|
-
success: z.boolean(),
|
|
3035
|
-
summary: z.string(),
|
|
3036
|
-
files_modified: z.array(z.string()).optional().default([]),
|
|
3037
|
-
files_created: z.array(z.string()).optional().default([]),
|
|
3038
|
-
issues_found: z.array(z.string()).optional().default([]),
|
|
3039
|
-
tests_passed: z.boolean().optional(),
|
|
3040
|
-
notes: z.string().optional(),
|
|
3041
|
-
blocker: z.string().optional(),
|
|
3042
|
-
suggestions: z.array(z.string()).optional(),
|
|
3043
|
-
});
|
|
3044
|
-
|
|
3045
|
-
type TaskResult = z.infer<typeof TaskResultSchema>;
|
|
3046
|
-
|
|
3047
|
-
/**
|
|
3048
|
-
* Handle subtask completion from a Task agent
|
|
3049
|
-
*
|
|
3050
|
-
* This tool is for coordinators to process the result after a Task subagent
|
|
3051
|
-
* returns. It parses the JSON result, closes the bead on success, and
|
|
3052
|
-
* creates new beads for any issues discovered.
|
|
3053
|
-
*
|
|
3054
|
-
* @example
|
|
3055
|
-
* // Task agent returns JSON:
|
|
3056
|
-
* // { "success": true, "summary": "Added auth", "files_modified": ["src/auth.ts"], "issues_found": ["Missing tests"] }
|
|
3057
|
-
* //
|
|
3058
|
-
* // Coordinator calls:
|
|
3059
|
-
* swarm_complete_subtask(bead_id="bd-123.1", task_result=<agent_response>)
|
|
3060
|
-
*/
|
|
3061
|
-
export const swarm_complete_subtask = tool({
|
|
3062
|
-
description:
|
|
3063
|
-
"Handle subtask completion after Task agent returns. Parses result JSON, closes bead on success, creates new beads for issues found.",
|
|
3064
|
-
args: {
|
|
3065
|
-
bead_id: z.string().describe("Subtask bead ID to close"),
|
|
3066
|
-
task_result: z
|
|
3067
|
-
.string()
|
|
3068
|
-
.describe("JSON result from the Task agent (TaskResult schema)"),
|
|
3069
|
-
files_touched: z
|
|
3070
|
-
.array(z.string())
|
|
3071
|
-
.optional()
|
|
3072
|
-
.describe(
|
|
3073
|
-
"Override files touched (uses task_result.files_modified if not provided)",
|
|
3074
|
-
),
|
|
3075
|
-
},
|
|
3076
|
-
async execute(args) {
|
|
3077
|
-
// Parse the task result JSON
|
|
3078
|
-
let result: TaskResult;
|
|
3079
|
-
try {
|
|
3080
|
-
const parsed = JSON.parse(args.task_result);
|
|
3081
|
-
result = TaskResultSchema.parse(parsed);
|
|
3082
|
-
} catch (error) {
|
|
3083
|
-
// Handle parse errors gracefully
|
|
3084
|
-
const errorMessage =
|
|
3085
|
-
error instanceof SyntaxError
|
|
3086
|
-
? `Invalid JSON: ${error.message}`
|
|
3087
|
-
: error instanceof z.ZodError
|
|
3088
|
-
? `Schema validation failed: ${error.issues.map((i) => i.message).join(", ")}`
|
|
3089
|
-
: String(error);
|
|
3090
|
-
|
|
3091
|
-
return JSON.stringify(
|
|
3092
|
-
{
|
|
3093
|
-
success: false,
|
|
3094
|
-
error: "Failed to parse task result",
|
|
3095
|
-
details: errorMessage,
|
|
3096
|
-
hint: "Task agent should return JSON matching TaskResult schema: { success, summary, files_modified?, issues_found?, ... }",
|
|
3097
|
-
},
|
|
3098
|
-
null,
|
|
3099
|
-
2,
|
|
3100
|
-
);
|
|
3101
|
-
}
|
|
3102
|
-
|
|
3103
|
-
const filesTouched = args.files_touched ?? [
|
|
3104
|
-
...result.files_modified,
|
|
3105
|
-
...result.files_created,
|
|
3106
|
-
];
|
|
3107
|
-
const issuesCreated: Array<{ title: string; id?: string }> = [];
|
|
3108
|
-
|
|
3109
|
-
// If task failed, don't close the bead - return info for coordinator to handle
|
|
3110
|
-
if (!result.success) {
|
|
3111
|
-
return JSON.stringify(
|
|
3112
|
-
{
|
|
3113
|
-
success: false,
|
|
3114
|
-
bead_id: args.bead_id,
|
|
3115
|
-
task_failed: true,
|
|
3116
|
-
summary: result.summary,
|
|
3117
|
-
blocker: result.blocker,
|
|
3118
|
-
suggestions: result.suggestions,
|
|
3119
|
-
files_touched: filesTouched,
|
|
3120
|
-
action_needed:
|
|
3121
|
-
"Task failed - review blocker and decide whether to retry or close as failed",
|
|
3122
|
-
},
|
|
3123
|
-
null,
|
|
3124
|
-
2,
|
|
3125
|
-
);
|
|
3126
|
-
}
|
|
3127
|
-
|
|
3128
|
-
// Task succeeded - close the bead
|
|
3129
|
-
const closeReason = result.summary.slice(0, 200); // Truncate for safety
|
|
3130
|
-
await Bun.$`bd close ${args.bead_id} -r "${closeReason}"`.quiet().nothrow();
|
|
3131
|
-
|
|
3132
|
-
// Create new beads for each issue found
|
|
3133
|
-
if (result.issues_found.length > 0) {
|
|
3134
|
-
for (const issue of result.issues_found) {
|
|
3135
|
-
const issueTitle = issue.slice(0, 100); // Truncate long titles
|
|
3136
|
-
const createResult = await Bun.$`bd create "${issueTitle}" -t bug`
|
|
3137
|
-
.quiet()
|
|
3138
|
-
.nothrow();
|
|
3139
|
-
|
|
3140
|
-
if (createResult.exitCode === 0) {
|
|
3141
|
-
// Try to parse the bead ID from output
|
|
3142
|
-
const output = createResult.stdout.toString();
|
|
3143
|
-
const idMatch = output.match(/bd-[a-z0-9]+/);
|
|
3144
|
-
issuesCreated.push({
|
|
3145
|
-
title: issueTitle,
|
|
3146
|
-
id: idMatch?.[0],
|
|
3147
|
-
});
|
|
3148
|
-
} else {
|
|
3149
|
-
issuesCreated.push({
|
|
3150
|
-
title: issueTitle,
|
|
3151
|
-
id: undefined, // Failed to create
|
|
3152
|
-
});
|
|
3153
|
-
}
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
return JSON.stringify(
|
|
3158
|
-
{
|
|
3159
|
-
success: true,
|
|
3160
|
-
bead_id: args.bead_id,
|
|
3161
|
-
bead_closed: true,
|
|
3162
|
-
summary: result.summary,
|
|
3163
|
-
files_touched: filesTouched,
|
|
3164
|
-
tests_passed: result.tests_passed,
|
|
3165
|
-
notes: result.notes,
|
|
3166
|
-
issues_created: issuesCreated.length > 0 ? issuesCreated : undefined,
|
|
3167
|
-
issues_count: issuesCreated.length,
|
|
3168
|
-
},
|
|
3169
|
-
null,
|
|
3170
|
-
2,
|
|
3171
|
-
);
|
|
3172
|
-
},
|
|
3173
|
-
});
|
|
3174
|
-
|
|
3175
|
-
/**
|
|
3176
|
-
* Generate self-evaluation prompt
|
|
3177
|
-
*/
|
|
3178
|
-
export const swarm_evaluation_prompt = tool({
|
|
3179
|
-
description: "Generate self-evaluation prompt for a completed subtask",
|
|
3180
|
-
args: {
|
|
3181
|
-
bead_id: tool.schema.string().describe("Subtask bead ID"),
|
|
3182
|
-
subtask_title: tool.schema.string().describe("Subtask title"),
|
|
3183
|
-
files_touched: tool.schema
|
|
3184
|
-
.array(tool.schema.string())
|
|
3185
|
-
.describe("Files that were modified"),
|
|
3186
|
-
},
|
|
3187
|
-
async execute(args) {
|
|
3188
|
-
const prompt = formatEvaluationPrompt({
|
|
3189
|
-
bead_id: args.bead_id,
|
|
3190
|
-
subtask_title: args.subtask_title,
|
|
3191
|
-
files_touched: args.files_touched,
|
|
3192
|
-
});
|
|
3193
|
-
|
|
3194
|
-
return JSON.stringify(
|
|
3195
|
-
{
|
|
3196
|
-
prompt,
|
|
3197
|
-
expected_schema: "Evaluation",
|
|
3198
|
-
schema_hint: {
|
|
3199
|
-
passed: "boolean",
|
|
3200
|
-
criteria: {
|
|
3201
|
-
type_safe: { passed: "boolean", feedback: "string" },
|
|
3202
|
-
no_bugs: { passed: "boolean", feedback: "string" },
|
|
3203
|
-
patterns: { passed: "boolean", feedback: "string" },
|
|
3204
|
-
readable: { passed: "boolean", feedback: "string" },
|
|
3205
|
-
},
|
|
3206
|
-
overall_feedback: "string",
|
|
3207
|
-
retry_suggestion: "string | null",
|
|
3208
|
-
},
|
|
3209
|
-
},
|
|
3210
|
-
null,
|
|
3211
|
-
2,
|
|
3212
|
-
);
|
|
3213
|
-
},
|
|
3214
|
-
});
|
|
3215
|
-
|
|
3216
|
-
// ============================================================================
|
|
3217
|
-
// Swarm Learning
|
|
3218
|
-
// ============================================================================
|
|
3219
|
-
|
|
3220
|
-
/**
|
|
3221
|
-
* Learn from completed work and optionally create a skill
|
|
3222
|
-
*
|
|
3223
|
-
* This tool helps agents reflect on patterns, best practices, or domain
|
|
3224
|
-
* knowledge discovered during task execution and codify them into reusable
|
|
3225
|
-
* skills for future swarms.
|
|
3226
|
-
*
|
|
3227
|
-
* Implements the "learning swarm" pattern where swarms get smarter over time.
|
|
3228
|
-
*/
|
|
3229
|
-
export const swarm_learn = tool({
|
|
3230
|
-
description: `Analyze completed work and optionally create a skill from learned patterns.
|
|
3231
|
-
|
|
3232
|
-
Use after completing a subtask when you've discovered:
|
|
3233
|
-
- Reusable code patterns or approaches
|
|
3234
|
-
- Domain-specific best practices
|
|
3235
|
-
- Gotchas or edge cases to warn about
|
|
3236
|
-
- Effective tool usage patterns
|
|
3237
|
-
|
|
3238
|
-
This tool helps you formalize learnings into a skill that future agents can discover and use.`,
|
|
3239
|
-
args: {
|
|
3240
|
-
summary: tool.schema
|
|
3241
|
-
.string()
|
|
3242
|
-
.describe("Brief summary of what was learned (1-2 sentences)"),
|
|
3243
|
-
pattern_type: tool.schema
|
|
3244
|
-
.enum([
|
|
3245
|
-
"code-pattern",
|
|
3246
|
-
"best-practice",
|
|
3247
|
-
"gotcha",
|
|
3248
|
-
"tool-usage",
|
|
3249
|
-
"domain-knowledge",
|
|
3250
|
-
"workflow",
|
|
3251
|
-
])
|
|
3252
|
-
.describe("Category of the learning"),
|
|
3253
|
-
details: tool.schema
|
|
3254
|
-
.string()
|
|
3255
|
-
.describe("Detailed explanation of the pattern or practice"),
|
|
3256
|
-
example: tool.schema
|
|
3257
|
-
.string()
|
|
3258
|
-
.optional()
|
|
3259
|
-
.describe("Code example or concrete illustration"),
|
|
3260
|
-
when_to_use: tool.schema
|
|
3261
|
-
.string()
|
|
3262
|
-
.describe("When should an agent apply this knowledge?"),
|
|
3263
|
-
files_context: tool.schema
|
|
3264
|
-
.array(tool.schema.string())
|
|
3265
|
-
.optional()
|
|
3266
|
-
.describe("Files that exemplify this pattern"),
|
|
3267
|
-
create_skill: tool.schema
|
|
3268
|
-
.boolean()
|
|
3269
|
-
.optional()
|
|
3270
|
-
.describe(
|
|
3271
|
-
"Create a skill from this learning (default: false, just document)",
|
|
3272
|
-
),
|
|
3273
|
-
skill_name: tool.schema
|
|
3274
|
-
.string()
|
|
3275
|
-
.regex(/^[a-z0-9-]+$/)
|
|
3276
|
-
.max(64)
|
|
3277
|
-
.optional()
|
|
3278
|
-
.describe("Skill name if creating (required if create_skill=true)"),
|
|
3279
|
-
skill_tags: tool.schema
|
|
3280
|
-
.array(tool.schema.string())
|
|
3281
|
-
.optional()
|
|
3282
|
-
.describe("Tags for the skill if creating"),
|
|
3283
|
-
},
|
|
3284
|
-
async execute(args) {
|
|
3285
|
-
// Format the learning as structured documentation
|
|
3286
|
-
const learning = {
|
|
3287
|
-
summary: args.summary,
|
|
3288
|
-
type: args.pattern_type,
|
|
3289
|
-
details: args.details,
|
|
3290
|
-
example: args.example,
|
|
3291
|
-
when_to_use: args.when_to_use,
|
|
3292
|
-
files_context: args.files_context,
|
|
3293
|
-
recorded_at: new Date().toISOString(),
|
|
3294
|
-
};
|
|
3295
|
-
|
|
3296
|
-
// If creating a skill, generate and create it
|
|
3297
|
-
if (args.create_skill) {
|
|
3298
|
-
if (!args.skill_name) {
|
|
3299
|
-
return JSON.stringify(
|
|
3300
|
-
{
|
|
3301
|
-
success: false,
|
|
3302
|
-
error: "skill_name is required when create_skill=true",
|
|
3303
|
-
learning: learning,
|
|
3304
|
-
},
|
|
3305
|
-
null,
|
|
3306
|
-
2,
|
|
3307
|
-
);
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
// Build skill body from learning
|
|
3311
|
-
const skillBody = `# ${args.summary}
|
|
3312
|
-
|
|
3313
|
-
## When to Use
|
|
3314
|
-
${args.when_to_use}
|
|
3315
|
-
|
|
3316
|
-
## ${args.pattern_type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
|
|
3317
|
-
|
|
3318
|
-
${args.details}
|
|
3319
|
-
|
|
3320
|
-
${args.example ? `## Example\n\n\`\`\`\n${args.example}\n\`\`\`\n` : ""}
|
|
3321
|
-
${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n${args.files_context.map((f) => `- \`${f}\``).join("\n")}\n` : ""}
|
|
3322
|
-
|
|
3323
|
-
---
|
|
3324
|
-
*Learned from swarm execution on ${new Date().toISOString().split("T")[0]}*`;
|
|
3325
|
-
|
|
3326
|
-
// Import skills_create functionality
|
|
3327
|
-
const { getSkill, invalidateSkillsCache } = await import("./skills");
|
|
3328
|
-
const { mkdir, writeFile } = await import("fs/promises");
|
|
3329
|
-
const { join } = await import("path");
|
|
3330
|
-
|
|
3331
|
-
// Check if skill exists
|
|
3332
|
-
const existing = await getSkill(args.skill_name);
|
|
3333
|
-
if (existing) {
|
|
3334
|
-
return JSON.stringify(
|
|
3335
|
-
{
|
|
3336
|
-
success: false,
|
|
3337
|
-
error: `Skill '${args.skill_name}' already exists`,
|
|
3338
|
-
existing_path: existing.path,
|
|
3339
|
-
learning: learning,
|
|
3340
|
-
suggestion:
|
|
3341
|
-
"Use skills_update to add to existing skill, or choose a different name",
|
|
3342
|
-
},
|
|
3343
|
-
null,
|
|
3344
|
-
2,
|
|
3345
|
-
);
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
// Create skill directory and file
|
|
3349
|
-
const skillDir = join(
|
|
3350
|
-
process.cwd(),
|
|
3351
|
-
".opencode",
|
|
3352
|
-
"skills",
|
|
3353
|
-
args.skill_name,
|
|
3354
|
-
);
|
|
3355
|
-
const skillPath = join(skillDir, "SKILL.md");
|
|
3356
|
-
|
|
3357
|
-
const frontmatter = [
|
|
3358
|
-
"---",
|
|
3359
|
-
`name: ${args.skill_name}`,
|
|
3360
|
-
`description: ${args.when_to_use.slice(0, 200)}${args.when_to_use.length > 200 ? "..." : ""}`,
|
|
3361
|
-
"tags:",
|
|
3362
|
-
` - ${args.pattern_type}`,
|
|
3363
|
-
` - learned`,
|
|
3364
|
-
...(args.skill_tags || []).map((t) => ` - ${t}`),
|
|
3365
|
-
"---",
|
|
3366
|
-
].join("\n");
|
|
3367
|
-
|
|
3368
|
-
try {
|
|
3369
|
-
await mkdir(skillDir, { recursive: true });
|
|
3370
|
-
await writeFile(skillPath, `${frontmatter}\n\n${skillBody}`, "utf-8");
|
|
3371
|
-
invalidateSkillsCache();
|
|
3372
|
-
|
|
3373
|
-
return JSON.stringify(
|
|
3374
|
-
{
|
|
3375
|
-
success: true,
|
|
3376
|
-
skill_created: true,
|
|
3377
|
-
skill: {
|
|
3378
|
-
name: args.skill_name,
|
|
3379
|
-
path: skillPath,
|
|
3380
|
-
type: args.pattern_type,
|
|
3381
|
-
},
|
|
3382
|
-
learning: learning,
|
|
3383
|
-
message: `Created skill '${args.skill_name}' from learned pattern. Future agents can discover it with skills_list.`,
|
|
3384
|
-
},
|
|
3385
|
-
null,
|
|
3386
|
-
2,
|
|
3387
|
-
);
|
|
3388
|
-
} catch (error) {
|
|
3389
|
-
return JSON.stringify(
|
|
3390
|
-
{
|
|
3391
|
-
success: false,
|
|
3392
|
-
error: `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`,
|
|
3393
|
-
learning: learning,
|
|
3394
|
-
},
|
|
3395
|
-
null,
|
|
3396
|
-
2,
|
|
3397
|
-
);
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
|
|
3401
|
-
// Just document the learning without creating a skill
|
|
3402
|
-
return JSON.stringify(
|
|
3403
|
-
{
|
|
3404
|
-
success: true,
|
|
3405
|
-
skill_created: false,
|
|
3406
|
-
learning: learning,
|
|
3407
|
-
message:
|
|
3408
|
-
"Learning documented. Use create_skill=true to persist as a skill for future agents.",
|
|
3409
|
-
suggested_skill_name:
|
|
3410
|
-
args.skill_name ||
|
|
3411
|
-
args.summary
|
|
3412
|
-
.toLowerCase()
|
|
3413
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
3414
|
-
.replace(/\s+/g, "-")
|
|
3415
|
-
.slice(0, 64),
|
|
3416
|
-
},
|
|
3417
|
-
null,
|
|
3418
|
-
2,
|
|
3419
|
-
);
|
|
3420
|
-
},
|
|
3421
|
-
});
|
|
3422
|
-
|
|
3423
|
-
// ============================================================================
|
|
3424
|
-
// Error Accumulator
|
|
3425
|
-
// ============================================================================
|
|
3426
|
-
|
|
3427
|
-
/**
|
|
3428
|
-
* Global error accumulator for tracking errors across subtasks
|
|
3429
|
-
*
|
|
3430
|
-
* This is a session-level singleton that accumulates errors during
|
|
3431
|
-
* swarm execution for feeding into retry prompts.
|
|
3432
|
-
*/
|
|
3433
|
-
const globalErrorAccumulator = new ErrorAccumulator();
|
|
3434
|
-
|
|
3435
|
-
/**
|
|
3436
|
-
* Record an error during subtask execution
|
|
3437
|
-
*
|
|
3438
|
-
* Implements pattern from "Patterns for Building AI Agents" p.40:
|
|
3439
|
-
* "Good agents examine and correct errors when something goes wrong"
|
|
3440
|
-
*
|
|
3441
|
-
* Errors are accumulated and can be fed into retry prompts to help
|
|
3442
|
-
* agents learn from past failures.
|
|
3443
|
-
*/
|
|
3444
|
-
export const swarm_accumulate_error = tool({
|
|
3445
|
-
description:
|
|
3446
|
-
"Record an error during subtask execution. Errors feed into retry prompts.",
|
|
3447
|
-
args: {
|
|
3448
|
-
bead_id: tool.schema.string().describe("Bead ID where error occurred"),
|
|
3449
|
-
error_type: tool.schema
|
|
3450
|
-
.enum(["validation", "timeout", "conflict", "tool_failure", "unknown"])
|
|
3451
|
-
.describe("Category of error"),
|
|
3452
|
-
message: tool.schema.string().describe("Human-readable error message"),
|
|
3453
|
-
stack_trace: tool.schema
|
|
3454
|
-
.string()
|
|
3455
|
-
.optional()
|
|
3456
|
-
.describe("Stack trace for debugging"),
|
|
3457
|
-
tool_name: tool.schema.string().optional().describe("Tool that failed"),
|
|
3458
|
-
context: tool.schema
|
|
3459
|
-
.string()
|
|
3460
|
-
.optional()
|
|
3461
|
-
.describe("What was happening when error occurred"),
|
|
3462
|
-
},
|
|
3463
|
-
async execute(args) {
|
|
3464
|
-
const entry = await globalErrorAccumulator.recordError(
|
|
3465
|
-
args.bead_id,
|
|
3466
|
-
args.error_type as ErrorType,
|
|
3467
|
-
args.message,
|
|
3468
|
-
{
|
|
3469
|
-
stack_trace: args.stack_trace,
|
|
3470
|
-
tool_name: args.tool_name,
|
|
3471
|
-
context: args.context,
|
|
3472
|
-
},
|
|
3473
|
-
);
|
|
3474
|
-
|
|
3475
|
-
return JSON.stringify(
|
|
3476
|
-
{
|
|
3477
|
-
success: true,
|
|
3478
|
-
error_id: entry.id,
|
|
3479
|
-
bead_id: entry.bead_id,
|
|
3480
|
-
error_type: entry.error_type,
|
|
3481
|
-
message: entry.message,
|
|
3482
|
-
timestamp: entry.timestamp,
|
|
3483
|
-
note: "Error recorded for retry context. Use swarm_get_error_context to retrieve accumulated errors.",
|
|
3484
|
-
},
|
|
3485
|
-
null,
|
|
3486
|
-
2,
|
|
3487
|
-
);
|
|
3488
|
-
},
|
|
3489
|
-
});
|
|
3490
|
-
|
|
3491
|
-
/**
|
|
3492
|
-
* Get accumulated errors for a bead to feed into retry prompts
|
|
3493
|
-
*
|
|
3494
|
-
* Returns formatted error context that can be injected into retry prompts
|
|
3495
|
-
* to help agents learn from past failures.
|
|
3496
|
-
*/
|
|
3497
|
-
export const swarm_get_error_context = tool({
|
|
3498
|
-
description:
|
|
3499
|
-
"Get accumulated errors for a bead. Returns formatted context for retry prompts.",
|
|
3500
|
-
args: {
|
|
3501
|
-
bead_id: tool.schema.string().describe("Bead ID to get errors for"),
|
|
3502
|
-
include_resolved: tool.schema
|
|
3503
|
-
.boolean()
|
|
3504
|
-
.optional()
|
|
3505
|
-
.describe("Include resolved errors (default: false)"),
|
|
3506
|
-
},
|
|
3507
|
-
async execute(args) {
|
|
3508
|
-
const errorContext = await globalErrorAccumulator.getErrorContext(
|
|
3509
|
-
args.bead_id,
|
|
3510
|
-
args.include_resolved ?? false,
|
|
3511
|
-
);
|
|
3512
|
-
|
|
3513
|
-
const stats = await globalErrorAccumulator.getErrorStats(args.bead_id);
|
|
3514
|
-
|
|
3515
|
-
return JSON.stringify(
|
|
3516
|
-
{
|
|
3517
|
-
bead_id: args.bead_id,
|
|
3518
|
-
error_context: errorContext,
|
|
3519
|
-
stats: {
|
|
3520
|
-
total_errors: stats.total,
|
|
3521
|
-
unresolved: stats.unresolved,
|
|
3522
|
-
by_type: stats.by_type,
|
|
3523
|
-
},
|
|
3524
|
-
has_errors: errorContext.length > 0,
|
|
3525
|
-
usage:
|
|
3526
|
-
"Inject error_context into retry prompt using {error_context} placeholder",
|
|
3527
|
-
},
|
|
3528
|
-
null,
|
|
3529
|
-
2,
|
|
3530
|
-
);
|
|
3531
|
-
},
|
|
3532
|
-
});
|
|
3533
|
-
|
|
3534
|
-
/**
|
|
3535
|
-
* Mark an error as resolved
|
|
3536
|
-
*
|
|
3537
|
-
* Call this after an agent successfully addresses an error to update
|
|
3538
|
-
* the accumulator state.
|
|
3539
|
-
*/
|
|
3540
|
-
export const swarm_resolve_error = tool({
|
|
3541
|
-
description:
|
|
3542
|
-
"Mark an error as resolved after fixing it. Updates error accumulator state.",
|
|
3543
|
-
args: {
|
|
3544
|
-
error_id: tool.schema.string().describe("Error ID to mark as resolved"),
|
|
3545
|
-
},
|
|
3546
|
-
async execute(args) {
|
|
3547
|
-
await globalErrorAccumulator.resolveError(args.error_id);
|
|
3548
|
-
|
|
3549
|
-
return JSON.stringify(
|
|
3550
|
-
{
|
|
3551
|
-
success: true,
|
|
3552
|
-
error_id: args.error_id,
|
|
3553
|
-
resolved: true,
|
|
3554
|
-
},
|
|
3555
|
-
null,
|
|
3556
|
-
2,
|
|
3557
|
-
);
|
|
3558
|
-
},
|
|
3559
|
-
});
|
|
20
|
+
// Import tools from each module
|
|
21
|
+
import { strategyTools } from "./swarm-strategies";
|
|
22
|
+
import { decomposeTools } from "./swarm-decompose";
|
|
23
|
+
import { promptTools } from "./swarm-prompts";
|
|
24
|
+
import { orchestrateTools } from "./swarm-orchestrate";
|
|
3560
25
|
|
|
3561
26
|
/**
|
|
3562
|
-
*
|
|
3563
|
-
*
|
|
3564
|
-
* Call this at the start of a swarm session to see what tools are available,
|
|
3565
|
-
* what skills exist in the project, and what features will be degraded.
|
|
3566
|
-
*
|
|
3567
|
-
* Skills are automatically discovered from:
|
|
3568
|
-
* - .opencode/skills/
|
|
3569
|
-
* - .claude/skills/
|
|
3570
|
-
* - skills/
|
|
27
|
+
* Combined swarm tools for plugin registration.
|
|
28
|
+
* Includes all tools from strategy, decompose, prompt, and orchestrate modules.
|
|
3571
29
|
*/
|
|
3572
|
-
export const swarm_init = tool({
|
|
3573
|
-
description:
|
|
3574
|
-
"Initialize swarm session: discovers available skills, checks tool availability. ALWAYS call at swarm start.",
|
|
3575
|
-
args: {
|
|
3576
|
-
project_path: tool.schema
|
|
3577
|
-
.string()
|
|
3578
|
-
.optional()
|
|
3579
|
-
.describe("Project path (for Agent Mail init)"),
|
|
3580
|
-
},
|
|
3581
|
-
async execute(args) {
|
|
3582
|
-
// Check all tools
|
|
3583
|
-
const availability = await checkAllTools();
|
|
3584
|
-
|
|
3585
|
-
// Build status report
|
|
3586
|
-
const report = formatToolAvailability(availability);
|
|
3587
|
-
|
|
3588
|
-
// Check critical tools
|
|
3589
|
-
const beadsAvailable = availability.get("beads")?.status.available ?? false;
|
|
3590
|
-
const agentMailAvailable =
|
|
3591
|
-
availability.get("agent-mail")?.status.available ?? false;
|
|
3592
|
-
|
|
3593
|
-
// Build warnings
|
|
3594
|
-
const warnings: string[] = [];
|
|
3595
|
-
const degradedFeatures: string[] = [];
|
|
3596
|
-
|
|
3597
|
-
if (!beadsAvailable) {
|
|
3598
|
-
warnings.push(
|
|
3599
|
-
"⚠️ beads (bd) not available - issue tracking disabled, swarm coordination will be limited",
|
|
3600
|
-
);
|
|
3601
|
-
degradedFeatures.push("issue tracking", "progress persistence");
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
if (!agentMailAvailable) {
|
|
3605
|
-
warnings.push(
|
|
3606
|
-
"⚠️ agent-mail not available - multi-agent communication disabled",
|
|
3607
|
-
);
|
|
3608
|
-
degradedFeatures.push("agent communication", "file reservations");
|
|
3609
|
-
}
|
|
3610
|
-
|
|
3611
|
-
if (!availability.get("cass")?.status.available) {
|
|
3612
|
-
degradedFeatures.push("historical context from past sessions");
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
|
-
if (!availability.get("ubs")?.status.available) {
|
|
3616
|
-
degradedFeatures.push("pre-completion bug scanning");
|
|
3617
|
-
}
|
|
3618
|
-
|
|
3619
|
-
if (!availability.get("semantic-memory")?.status.available) {
|
|
3620
|
-
degradedFeatures.push("persistent learning (using in-memory fallback)");
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
// Discover available skills
|
|
3624
|
-
const availableSkills = await listSkills();
|
|
3625
|
-
const skillsInfo = {
|
|
3626
|
-
count: availableSkills.length,
|
|
3627
|
-
available: availableSkills.length > 0,
|
|
3628
|
-
skills: availableSkills.map((s) => ({
|
|
3629
|
-
name: s.name,
|
|
3630
|
-
description: s.description,
|
|
3631
|
-
hasScripts: s.hasScripts,
|
|
3632
|
-
})),
|
|
3633
|
-
};
|
|
3634
|
-
|
|
3635
|
-
// Add skills guidance if available
|
|
3636
|
-
let skillsGuidance: string | undefined;
|
|
3637
|
-
if (availableSkills.length > 0) {
|
|
3638
|
-
skillsGuidance = `Found ${availableSkills.length} skill(s). Use skills_list to see details, skills_use to activate.`;
|
|
3639
|
-
} else {
|
|
3640
|
-
skillsGuidance =
|
|
3641
|
-
"No skills found. Add skills to .opencode/skills/ or .claude/skills/ for specialized guidance.";
|
|
3642
|
-
}
|
|
3643
|
-
|
|
3644
|
-
return JSON.stringify(
|
|
3645
|
-
{
|
|
3646
|
-
ready: true,
|
|
3647
|
-
tool_availability: Object.fromEntries(
|
|
3648
|
-
Array.from(availability.entries()).map(([k, v]) => [
|
|
3649
|
-
k,
|
|
3650
|
-
{
|
|
3651
|
-
available: v.status.available,
|
|
3652
|
-
fallback: v.status.available ? null : v.fallbackBehavior,
|
|
3653
|
-
},
|
|
3654
|
-
]),
|
|
3655
|
-
),
|
|
3656
|
-
skills: skillsInfo,
|
|
3657
|
-
warnings: warnings.length > 0 ? warnings : undefined,
|
|
3658
|
-
degraded_features:
|
|
3659
|
-
degradedFeatures.length > 0 ? degradedFeatures : undefined,
|
|
3660
|
-
recommendations: {
|
|
3661
|
-
skills: skillsGuidance,
|
|
3662
|
-
beads: beadsAvailable
|
|
3663
|
-
? "✓ Use beads for all task tracking"
|
|
3664
|
-
: "Install beads: npm i -g @joelhooks/beads",
|
|
3665
|
-
agent_mail: agentMailAvailable
|
|
3666
|
-
? "✓ Use Agent Mail for coordination"
|
|
3667
|
-
: "Start Agent Mail: agent-mail serve",
|
|
3668
|
-
},
|
|
3669
|
-
report,
|
|
3670
|
-
},
|
|
3671
|
-
null,
|
|
3672
|
-
2,
|
|
3673
|
-
);
|
|
3674
|
-
},
|
|
3675
|
-
});
|
|
3676
|
-
|
|
3677
|
-
// ============================================================================
|
|
3678
|
-
// Export all tools
|
|
3679
|
-
// ============================================================================
|
|
3680
|
-
|
|
3681
30
|
export const swarmTools = {
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
swarm_decompose: swarm_decompose,
|
|
3687
|
-
swarm_validate_decomposition: swarm_validate_decomposition,
|
|
3688
|
-
swarm_status: swarm_status,
|
|
3689
|
-
swarm_progress: swarm_progress,
|
|
3690
|
-
swarm_broadcast: swarm_broadcast,
|
|
3691
|
-
swarm_complete: swarm_complete,
|
|
3692
|
-
swarm_learn: swarm_learn,
|
|
3693
|
-
swarm_record_outcome: swarm_record_outcome,
|
|
3694
|
-
swarm_subtask_prompt: swarm_subtask_prompt,
|
|
3695
|
-
swarm_spawn_subtask: swarm_spawn_subtask,
|
|
3696
|
-
swarm_complete_subtask: swarm_complete_subtask,
|
|
3697
|
-
swarm_evaluation_prompt: swarm_evaluation_prompt,
|
|
3698
|
-
swarm_accumulate_error: swarm_accumulate_error,
|
|
3699
|
-
swarm_get_error_context: swarm_get_error_context,
|
|
3700
|
-
swarm_resolve_error: swarm_resolve_error,
|
|
31
|
+
...strategyTools,
|
|
32
|
+
...decomposeTools,
|
|
33
|
+
...promptTools,
|
|
34
|
+
...orchestrateTools,
|
|
3701
35
|
};
|
|
3702
|
-
|
|
3703
|
-
// ============================================================================
|
|
3704
|
-
// 3-Strike Detection
|
|
3705
|
-
// ============================================================================
|
|
3706
|
-
|
|
3707
|
-
/**
|
|
3708
|
-
* Global strike storage for tracking consecutive fix failures
|
|
3709
|
-
*/
|
|
3710
|
-
import {
|
|
3711
|
-
InMemoryStrikeStorage,
|
|
3712
|
-
addStrike,
|
|
3713
|
-
getStrikes,
|
|
3714
|
-
isStrikedOut,
|
|
3715
|
-
getArchitecturePrompt,
|
|
3716
|
-
clearStrikes,
|
|
3717
|
-
type StrikeStorage,
|
|
3718
|
-
} from "./learning";
|
|
3719
|
-
|
|
3720
|
-
const globalStrikeStorage: StrikeStorage = new InMemoryStrikeStorage();
|
|
3721
|
-
|
|
3722
|
-
/**
|
|
3723
|
-
* Check if a bead has struck out (3 consecutive failures)
|
|
3724
|
-
*
|
|
3725
|
-
* The 3-Strike Rule:
|
|
3726
|
-
* IF 3+ fixes have failed:
|
|
3727
|
-
* STOP → Question the architecture
|
|
3728
|
-
* DON'T attempt Fix #4
|
|
3729
|
-
* Discuss with human partner
|
|
3730
|
-
*
|
|
3731
|
-
* This is NOT a failed hypothesis.
|
|
3732
|
-
* This is a WRONG ARCHITECTURE.
|
|
3733
|
-
*
|
|
3734
|
-
* Use this tool to:
|
|
3735
|
-
* - Check strike count before attempting a fix
|
|
3736
|
-
* - Get architecture review prompt if struck out
|
|
3737
|
-
* - Record a strike when a fix fails
|
|
3738
|
-
* - Clear strikes when a fix succeeds
|
|
3739
|
-
*/
|
|
3740
|
-
export const swarm_check_strikes = tool({
|
|
3741
|
-
description:
|
|
3742
|
-
"Check 3-strike status for a bead. Records failures, detects architectural problems, generates architecture review prompts.",
|
|
3743
|
-
args: {
|
|
3744
|
-
bead_id: tool.schema.string().describe("Bead ID to check"),
|
|
3745
|
-
action: tool.schema
|
|
3746
|
-
.enum(["check", "add_strike", "clear", "get_prompt"])
|
|
3747
|
-
.describe(
|
|
3748
|
-
"Action: check count, add strike, clear strikes, or get prompt",
|
|
3749
|
-
),
|
|
3750
|
-
attempt: tool.schema
|
|
3751
|
-
.string()
|
|
3752
|
-
.optional()
|
|
3753
|
-
.describe("Description of fix attempt (required for add_strike)"),
|
|
3754
|
-
reason: tool.schema
|
|
3755
|
-
.string()
|
|
3756
|
-
.optional()
|
|
3757
|
-
.describe("Why the fix failed (required for add_strike)"),
|
|
3758
|
-
},
|
|
3759
|
-
async execute(args) {
|
|
3760
|
-
switch (args.action) {
|
|
3761
|
-
case "check": {
|
|
3762
|
-
const count = await getStrikes(args.bead_id, globalStrikeStorage);
|
|
3763
|
-
const strikedOut = await isStrikedOut(
|
|
3764
|
-
args.bead_id,
|
|
3765
|
-
globalStrikeStorage,
|
|
3766
|
-
);
|
|
3767
|
-
|
|
3768
|
-
return JSON.stringify(
|
|
3769
|
-
{
|
|
3770
|
-
bead_id: args.bead_id,
|
|
3771
|
-
strike_count: count,
|
|
3772
|
-
is_striked_out: strikedOut,
|
|
3773
|
-
message: strikedOut
|
|
3774
|
-
? "⚠️ STRUCK OUT: 3 strikes reached. Use get_prompt action for architecture review."
|
|
3775
|
-
: count === 0
|
|
3776
|
-
? "No strikes. Clear to proceed."
|
|
3777
|
-
: `${count} strike${count > 1 ? "s" : ""}. ${3 - count} remaining before architecture review required.`,
|
|
3778
|
-
next_action: strikedOut
|
|
3779
|
-
? "Call with action=get_prompt to get architecture review questions"
|
|
3780
|
-
: "Continue with fix attempt",
|
|
3781
|
-
},
|
|
3782
|
-
null,
|
|
3783
|
-
2,
|
|
3784
|
-
);
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
case "add_strike": {
|
|
3788
|
-
if (!args.attempt || !args.reason) {
|
|
3789
|
-
return JSON.stringify(
|
|
3790
|
-
{
|
|
3791
|
-
error: "add_strike requires 'attempt' and 'reason' parameters",
|
|
3792
|
-
},
|
|
3793
|
-
null,
|
|
3794
|
-
2,
|
|
3795
|
-
);
|
|
3796
|
-
}
|
|
3797
|
-
|
|
3798
|
-
const record = await addStrike(
|
|
3799
|
-
args.bead_id,
|
|
3800
|
-
args.attempt,
|
|
3801
|
-
args.reason,
|
|
3802
|
-
globalStrikeStorage,
|
|
3803
|
-
);
|
|
3804
|
-
|
|
3805
|
-
const strikedOut = record.strike_count >= 3;
|
|
3806
|
-
|
|
3807
|
-
// Build response with memory storage hint on 3-strike
|
|
3808
|
-
const response: Record<string, unknown> = {
|
|
3809
|
-
bead_id: args.bead_id,
|
|
3810
|
-
strike_count: record.strike_count,
|
|
3811
|
-
is_striked_out: strikedOut,
|
|
3812
|
-
failures: record.failures,
|
|
3813
|
-
message: strikedOut
|
|
3814
|
-
? "⚠️ STRUCK OUT: 3 strikes reached. STOP and question the architecture."
|
|
3815
|
-
: `Strike ${record.strike_count} recorded. ${3 - record.strike_count} remaining.`,
|
|
3816
|
-
warning: strikedOut
|
|
3817
|
-
? "DO NOT attempt Fix #4. Call with action=get_prompt for architecture review."
|
|
3818
|
-
: undefined,
|
|
3819
|
-
};
|
|
3820
|
-
|
|
3821
|
-
// Add semantic-memory storage hint on 3-strike
|
|
3822
|
-
if (strikedOut) {
|
|
3823
|
-
response.memory_store = formatMemoryStoreOn3Strike(
|
|
3824
|
-
args.bead_id,
|
|
3825
|
-
record.failures,
|
|
3826
|
-
);
|
|
3827
|
-
}
|
|
3828
|
-
|
|
3829
|
-
return JSON.stringify(response, null, 2);
|
|
3830
|
-
}
|
|
3831
|
-
|
|
3832
|
-
case "clear": {
|
|
3833
|
-
await clearStrikes(args.bead_id, globalStrikeStorage);
|
|
3834
|
-
|
|
3835
|
-
return JSON.stringify(
|
|
3836
|
-
{
|
|
3837
|
-
bead_id: args.bead_id,
|
|
3838
|
-
strike_count: 0,
|
|
3839
|
-
is_striked_out: false,
|
|
3840
|
-
message: "Strikes cleared. Fresh start.",
|
|
3841
|
-
},
|
|
3842
|
-
null,
|
|
3843
|
-
2,
|
|
3844
|
-
);
|
|
3845
|
-
}
|
|
3846
|
-
|
|
3847
|
-
case "get_prompt": {
|
|
3848
|
-
const prompt = await getArchitecturePrompt(
|
|
3849
|
-
args.bead_id,
|
|
3850
|
-
globalStrikeStorage,
|
|
3851
|
-
);
|
|
3852
|
-
|
|
3853
|
-
if (!prompt) {
|
|
3854
|
-
return JSON.stringify(
|
|
3855
|
-
{
|
|
3856
|
-
bead_id: args.bead_id,
|
|
3857
|
-
has_prompt: false,
|
|
3858
|
-
message: "No architecture prompt (not struck out yet)",
|
|
3859
|
-
},
|
|
3860
|
-
null,
|
|
3861
|
-
2,
|
|
3862
|
-
);
|
|
3863
|
-
}
|
|
3864
|
-
|
|
3865
|
-
return JSON.stringify(
|
|
3866
|
-
{
|
|
3867
|
-
bead_id: args.bead_id,
|
|
3868
|
-
has_prompt: true,
|
|
3869
|
-
architecture_review_prompt: prompt,
|
|
3870
|
-
message:
|
|
3871
|
-
"Architecture review required. Present this prompt to the human partner.",
|
|
3872
|
-
},
|
|
3873
|
-
null,
|
|
3874
|
-
2,
|
|
3875
|
-
);
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
default:
|
|
3879
|
-
return JSON.stringify(
|
|
3880
|
-
{
|
|
3881
|
-
error: `Unknown action: ${args.action}`,
|
|
3882
|
-
},
|
|
3883
|
-
null,
|
|
3884
|
-
2,
|
|
3885
|
-
);
|
|
3886
|
-
}
|
|
3887
|
-
},
|
|
3888
|
-
});
|