opencode-swarm-plugin 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1869 @@
1
+ /**
2
+ * Swarm Orchestrate Module - Status tracking and completion handling
3
+ *
4
+ * Handles swarm execution lifecycle:
5
+ * - Initialization and tool availability
6
+ * - Status tracking and progress reporting
7
+ * - Completion verification and gates
8
+ * - Error accumulation and 3-strike detection
9
+ * - Learning from outcomes
10
+ *
11
+ * Key responsibilities:
12
+ * - swarm_init - Check tools and discover skills
13
+ * - swarm_status - Query epic progress
14
+ * - swarm_progress - Report agent progress
15
+ * - swarm_complete - Verification gate and completion
16
+ * - swarm_record_outcome - Learning signals
17
+ * - swarm_broadcast - Mid-task context sharing
18
+ * - Error accumulation tools
19
+ * - 3-strike detection for architectural problems
20
+ */
21
+
22
+ import { tool } from "@opencode-ai/plugin";
23
+ import { z } from "zod";
24
+ import {
25
+ type AgentProgress,
26
+ AgentProgressSchema,
27
+ type Bead,
28
+ BeadSchema,
29
+ type Evaluation,
30
+ EvaluationSchema,
31
+ type SpawnedAgent,
32
+ type SwarmStatus,
33
+ SwarmStatusSchema,
34
+ } from "./schemas";
35
+ import {
36
+ getSwarmInbox,
37
+ releaseSwarmFiles,
38
+ sendSwarmMessage,
39
+ } from "./streams/swarm-mail";
40
+ import {
41
+ addStrike,
42
+ clearStrikes,
43
+ DEFAULT_LEARNING_CONFIG,
44
+ type DecompositionStrategy as LearningDecompositionStrategy,
45
+ ErrorAccumulator,
46
+ type ErrorType,
47
+ type FeedbackEvent,
48
+ formatMemoryStoreOn3Strike,
49
+ formatMemoryStoreOnSuccess,
50
+ getArchitecturePrompt,
51
+ getStrikes,
52
+ InMemoryStrikeStorage,
53
+ isStrikedOut,
54
+ type OutcomeSignals,
55
+ OutcomeSignalsSchema,
56
+ outcomeToFeedback,
57
+ type ScoredOutcome,
58
+ scoreImplicitFeedback,
59
+ type StrikeStorage,
60
+ } from "./learning";
61
+ import {
62
+ checkAllTools,
63
+ formatToolAvailability,
64
+ isToolAvailable,
65
+ warnMissingTool,
66
+ } from "./tool-availability";
67
+ import { listSkills } from "./skills";
68
+
69
+ // ============================================================================
70
+ // Helper Functions
71
+ // ============================================================================
72
+
73
+ /**
74
+ * Query beads for subtasks of an epic
75
+ */
76
+ async function queryEpicSubtasks(epicId: string): Promise<Bead[]> {
77
+ // Check if beads is available
78
+ const beadsAvailable = await isToolAvailable("beads");
79
+ if (!beadsAvailable) {
80
+ warnMissingTool("beads");
81
+ return []; // Return empty - swarm can still function without status tracking
82
+ }
83
+
84
+ const result = await Bun.$`bd list --parent ${epicId} --json`
85
+ .quiet()
86
+ .nothrow();
87
+
88
+ if (result.exitCode !== 0) {
89
+ // Don't throw - just return empty and log error prominently
90
+ console.error(
91
+ `[swarm] ERROR: Failed to query subtasks for epic ${epicId}:`,
92
+ result.stderr.toString(),
93
+ );
94
+ return [];
95
+ }
96
+
97
+ try {
98
+ const parsed = JSON.parse(result.stdout.toString());
99
+ return z.array(BeadSchema).parse(parsed);
100
+ } catch (error) {
101
+ if (error instanceof z.ZodError) {
102
+ console.error(
103
+ `[swarm] ERROR: Invalid bead data for epic ${epicId}:`,
104
+ error.message,
105
+ );
106
+ return [];
107
+ }
108
+ console.error(
109
+ `[swarm] ERROR: Failed to parse beads for epic ${epicId}:`,
110
+ error,
111
+ );
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Query Agent Mail for swarm thread messages
118
+ */
119
+ async function querySwarmMessages(
120
+ projectKey: string,
121
+ threadId: string,
122
+ ): Promise<number> {
123
+ // Check if agent-mail is available
124
+ const agentMailAvailable = await isToolAvailable("agent-mail");
125
+ if (!agentMailAvailable) {
126
+ // Don't warn here - it's checked elsewhere
127
+ return 0;
128
+ }
129
+
130
+ try {
131
+ // Use embedded swarm-mail inbox to count messages in thread
132
+ const inbox = await getSwarmInbox({
133
+ projectPath: projectKey,
134
+ agentName: "coordinator", // Dummy agent name for thread query
135
+ limit: 5,
136
+ includeBodies: false,
137
+ });
138
+
139
+ // Count messages that match the thread ID
140
+ const threadMessages = inbox.messages.filter(
141
+ (m) => m.thread_id === threadId,
142
+ );
143
+ return threadMessages.length;
144
+ } catch (error) {
145
+ // Thread might not exist yet, or query failed
146
+ console.warn(
147
+ `[swarm] Failed to query swarm messages for thread ${threadId}:`,
148
+ error,
149
+ );
150
+ return 0;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Format a progress message for Agent Mail
156
+ */
157
+ function formatProgressMessage(progress: AgentProgress): string {
158
+ const lines = [
159
+ `**Status**: ${progress.status}`,
160
+ progress.progress_percent !== undefined
161
+ ? `**Progress**: ${progress.progress_percent}%`
162
+ : null,
163
+ progress.message ? `**Message**: ${progress.message}` : null,
164
+ progress.files_touched && progress.files_touched.length > 0
165
+ ? `**Files touched**:\n${progress.files_touched.map((f) => `- \`${f}\``).join("\n")}`
166
+ : null,
167
+ progress.blockers && progress.blockers.length > 0
168
+ ? `**Blockers**:\n${progress.blockers.map((b) => `- ${b}`).join("\n")}`
169
+ : null,
170
+ ];
171
+
172
+ return lines.filter(Boolean).join("\n\n");
173
+ }
174
+
175
+ // ============================================================================
176
+ // Verification Gate
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Verification Gate result - tracks each verification step
181
+ *
182
+ * Based on the Gate Function from superpowers:
183
+ * 1. IDENTIFY: What command proves this claim?
184
+ * 2. RUN: Execute the FULL command (fresh, complete)
185
+ * 3. READ: Full output, check exit code, count failures
186
+ * 4. VERIFY: Does output confirm the claim?
187
+ * 5. ONLY THEN: Make the claim
188
+ */
189
+ interface VerificationStep {
190
+ name: string;
191
+ command: string;
192
+ passed: boolean;
193
+ exitCode: number;
194
+ output?: string;
195
+ error?: string;
196
+ skipped?: boolean;
197
+ skipReason?: string;
198
+ }
199
+
200
+ interface VerificationGateResult {
201
+ passed: boolean;
202
+ steps: VerificationStep[];
203
+ summary: string;
204
+ blockers: string[];
205
+ }
206
+
207
+ /**
208
+ * UBS scan result schema
209
+ */
210
+ interface UbsScanResult {
211
+ exitCode: number;
212
+ bugs: Array<{
213
+ file: string;
214
+ line: number;
215
+ severity: string;
216
+ message: string;
217
+ category: string;
218
+ }>;
219
+ summary: {
220
+ total: number;
221
+ critical: number;
222
+ high: number;
223
+ medium: number;
224
+ low: number;
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Run UBS scan on files before completion
230
+ *
231
+ * @param files - Files to scan
232
+ * @returns Scan result or null if UBS not available
233
+ */
234
+ async function runUbsScan(files: string[]): Promise<UbsScanResult | null> {
235
+ if (files.length === 0) {
236
+ return null;
237
+ }
238
+
239
+ // Check if UBS is available first
240
+ const ubsAvailable = await isToolAvailable("ubs");
241
+ if (!ubsAvailable) {
242
+ warnMissingTool("ubs");
243
+ return null;
244
+ }
245
+
246
+ try {
247
+ // Run UBS scan with JSON output
248
+ const result = await Bun.$`ubs scan ${files.join(" ")} --json`
249
+ .quiet()
250
+ .nothrow();
251
+
252
+ const output = result.stdout.toString();
253
+ if (!output.trim()) {
254
+ return {
255
+ exitCode: result.exitCode,
256
+ bugs: [],
257
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
258
+ };
259
+ }
260
+
261
+ try {
262
+ const parsed = JSON.parse(output);
263
+
264
+ // Basic validation of structure
265
+ if (typeof parsed !== "object" || parsed === null) {
266
+ throw new Error("UBS output is not an object");
267
+ }
268
+ if (!Array.isArray(parsed.bugs)) {
269
+ console.warn("[swarm] UBS output missing bugs array, using empty");
270
+ }
271
+ if (typeof parsed.summary !== "object" || parsed.summary === null) {
272
+ console.warn("[swarm] UBS output missing summary object, using empty");
273
+ }
274
+
275
+ return {
276
+ exitCode: result.exitCode,
277
+ bugs: Array.isArray(parsed.bugs) ? parsed.bugs : [],
278
+ summary: parsed.summary || {
279
+ total: 0,
280
+ critical: 0,
281
+ high: 0,
282
+ medium: 0,
283
+ low: 0,
284
+ },
285
+ };
286
+ } catch (error) {
287
+ // UBS output wasn't JSON - this is an error condition
288
+ console.error(
289
+ `[swarm] CRITICAL: UBS scan failed to parse JSON output because output is malformed:`,
290
+ error,
291
+ );
292
+ console.error(
293
+ `[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.`,
294
+ );
295
+ return {
296
+ exitCode: result.exitCode,
297
+ bugs: [],
298
+ summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 },
299
+ };
300
+ }
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Run typecheck verification
308
+ *
309
+ * Attempts to run TypeScript type checking on the project.
310
+ * Falls back gracefully if tsc is not available.
311
+ */
312
+ async function runTypecheckVerification(): Promise<VerificationStep> {
313
+ const step: VerificationStep = {
314
+ name: "typecheck",
315
+ command: "tsc --noEmit",
316
+ passed: false,
317
+ exitCode: -1,
318
+ };
319
+
320
+ try {
321
+ // Check if tsconfig.json exists in current directory
322
+ const tsconfigExists = await Bun.file("tsconfig.json").exists();
323
+ if (!tsconfigExists) {
324
+ step.skipped = true;
325
+ step.skipReason = "No tsconfig.json found";
326
+ step.passed = true; // Don't block if no TypeScript
327
+ return step;
328
+ }
329
+
330
+ const result = await Bun.$`tsc --noEmit`.quiet().nothrow();
331
+ step.exitCode = result.exitCode;
332
+ step.passed = result.exitCode === 0;
333
+
334
+ if (!step.passed) {
335
+ step.error = result.stderr.toString().slice(0, 1000); // Truncate for context
336
+ step.output = result.stdout.toString().slice(0, 1000);
337
+ }
338
+ } catch (error) {
339
+ step.skipped = true;
340
+ step.skipReason = `tsc not available: ${error instanceof Error ? error.message : String(error)}`;
341
+ step.passed = true; // Don't block if tsc unavailable
342
+ }
343
+
344
+ return step;
345
+ }
346
+
347
+ /**
348
+ * Run test verification for specific files
349
+ *
350
+ * Attempts to find and run tests related to the touched files.
351
+ * Uses common test patterns (*.test.ts, *.spec.ts, __tests__/).
352
+ */
353
+ async function runTestVerification(
354
+ filesTouched: string[],
355
+ ): Promise<VerificationStep> {
356
+ const step: VerificationStep = {
357
+ name: "tests",
358
+ command: "bun test <related-files>",
359
+ passed: false,
360
+ exitCode: -1,
361
+ };
362
+
363
+ if (filesTouched.length === 0) {
364
+ step.skipped = true;
365
+ step.skipReason = "No files touched";
366
+ step.passed = true;
367
+ return step;
368
+ }
369
+
370
+ // Find test files related to touched files
371
+ const testPatterns: string[] = [];
372
+ for (const file of filesTouched) {
373
+ // Skip if already a test file
374
+ if (file.includes(".test.") || file.includes(".spec.")) {
375
+ testPatterns.push(file);
376
+ continue;
377
+ }
378
+
379
+ // Look for corresponding test file
380
+ const baseName = file.replace(/\.(ts|tsx|js|jsx)$/, "");
381
+ testPatterns.push(`${baseName}.test.ts`);
382
+ testPatterns.push(`${baseName}.test.tsx`);
383
+ testPatterns.push(`${baseName}.spec.ts`);
384
+ }
385
+
386
+ // Check if any test files exist
387
+ const existingTests: string[] = [];
388
+ for (const pattern of testPatterns) {
389
+ try {
390
+ const exists = await Bun.file(pattern).exists();
391
+ if (exists) {
392
+ existingTests.push(pattern);
393
+ }
394
+ } catch {
395
+ // File doesn't exist, skip
396
+ }
397
+ }
398
+
399
+ if (existingTests.length === 0) {
400
+ step.skipped = true;
401
+ step.skipReason = "No related test files found";
402
+ step.passed = true;
403
+ return step;
404
+ }
405
+
406
+ try {
407
+ step.command = `bun test ${existingTests.join(" ")}`;
408
+ const result = await Bun.$`bun test ${existingTests}`.quiet().nothrow();
409
+ step.exitCode = result.exitCode;
410
+ step.passed = result.exitCode === 0;
411
+
412
+ if (!step.passed) {
413
+ step.error = result.stderr.toString().slice(0, 1000);
414
+ step.output = result.stdout.toString().slice(0, 1000);
415
+ }
416
+ } catch (error) {
417
+ step.skipped = true;
418
+ step.skipReason = `Test runner failed: ${error instanceof Error ? error.message : String(error)}`;
419
+ step.passed = true; // Don't block if test runner unavailable
420
+ }
421
+
422
+ return step;
423
+ }
424
+
425
+ /**
426
+ * Run the full Verification Gate
427
+ *
428
+ * Implements the Gate Function (IDENTIFY → RUN → READ → VERIFY → CLAIM):
429
+ * 1. UBS scan (already exists)
430
+ * 2. Typecheck
431
+ * 3. Tests for touched files
432
+ *
433
+ * All steps must pass (or be skipped with valid reason) to proceed.
434
+ */
435
+ async function runVerificationGate(
436
+ filesTouched: string[],
437
+ skipUbs: boolean = false,
438
+ ): Promise<VerificationGateResult> {
439
+ const steps: VerificationStep[] = [];
440
+ const blockers: string[] = [];
441
+
442
+ // Step 1: UBS scan
443
+ if (!skipUbs && filesTouched.length > 0) {
444
+ const ubsResult = await runUbsScan(filesTouched);
445
+ if (ubsResult) {
446
+ const ubsStep: VerificationStep = {
447
+ name: "ubs_scan",
448
+ command: `ubs scan ${filesTouched.join(" ")}`,
449
+ passed: ubsResult.summary.critical === 0,
450
+ exitCode: ubsResult.exitCode,
451
+ };
452
+
453
+ if (!ubsStep.passed) {
454
+ ubsStep.error = `Found ${ubsResult.summary.critical} critical bugs`;
455
+ blockers.push(
456
+ `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).`,
457
+ );
458
+ }
459
+
460
+ steps.push(ubsStep);
461
+ } else {
462
+ steps.push({
463
+ name: "ubs_scan",
464
+ command: "ubs scan",
465
+ passed: true,
466
+ exitCode: 0,
467
+ skipped: true,
468
+ skipReason: "UBS not available",
469
+ });
470
+ }
471
+ }
472
+
473
+ // Step 2: Typecheck
474
+ const typecheckStep = await runTypecheckVerification();
475
+ steps.push(typecheckStep);
476
+ if (!typecheckStep.passed && !typecheckStep.skipped) {
477
+ blockers.push(
478
+ `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.`,
479
+ );
480
+ }
481
+
482
+ // Step 3: Tests
483
+ const testStep = await runTestVerification(filesTouched);
484
+ steps.push(testStep);
485
+ if (!testStep.passed && !testStep.skipped) {
486
+ blockers.push(
487
+ `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.`,
488
+ );
489
+ }
490
+
491
+ // Build summary
492
+ const passedCount = steps.filter((s) => s.passed).length;
493
+ const skippedCount = steps.filter((s) => s.skipped).length;
494
+ const failedCount = steps.filter((s) => !s.passed && !s.skipped).length;
495
+
496
+ const summary =
497
+ failedCount === 0
498
+ ? `Verification passed: ${passedCount} checks passed, ${skippedCount} skipped`
499
+ : `Verification FAILED: ${failedCount} checks failed, ${passedCount} passed, ${skippedCount} skipped`;
500
+
501
+ return {
502
+ passed: failedCount === 0,
503
+ steps,
504
+ summary,
505
+ blockers,
506
+ };
507
+ }
508
+
509
+ /**
510
+ * Classify failure based on error message heuristics
511
+ *
512
+ * Simple pattern matching to categorize why a task failed.
513
+ * Used when failure_mode is not explicitly provided.
514
+ *
515
+ * @param error - Error object or message
516
+ * @returns FailureMode classification
517
+ */
518
+ function classifyFailure(error: Error | string): string {
519
+ const msg = (typeof error === "string" ? error : error.message).toLowerCase();
520
+
521
+ if (msg.includes("timeout")) return "timeout";
522
+ if (msg.includes("conflict") || msg.includes("reservation"))
523
+ return "conflict";
524
+ if (msg.includes("validation") || msg.includes("schema")) return "validation";
525
+ if (msg.includes("context") || msg.includes("token"))
526
+ return "context_overflow";
527
+ if (msg.includes("blocked") || msg.includes("dependency"))
528
+ return "dependency_blocked";
529
+ if (msg.includes("cancel")) return "user_cancelled";
530
+
531
+ // Check for tool failure patterns
532
+ if (
533
+ msg.includes("tool") ||
534
+ msg.includes("command") ||
535
+ msg.includes("failed to execute")
536
+ ) {
537
+ return "tool_failure";
538
+ }
539
+
540
+ return "unknown";
541
+ }
542
+
543
+ // ============================================================================
544
+ // Global Storage
545
+ // ============================================================================
546
+
547
+ /**
548
+ * Global error accumulator for tracking errors across subtasks
549
+ *
550
+ * This is a session-level singleton that accumulates errors during
551
+ * swarm execution for feeding into retry prompts.
552
+ */
553
+ const globalErrorAccumulator = new ErrorAccumulator();
554
+
555
+ /**
556
+ * Global strike storage for tracking consecutive fix failures
557
+ */
558
+ const globalStrikeStorage: StrikeStorage = new InMemoryStrikeStorage();
559
+
560
+ // ============================================================================
561
+ // Tool Definitions
562
+ // ============================================================================
563
+
564
+ /**
565
+ * Initialize swarm and check tool availability
566
+ *
567
+ * Call this at the start of a swarm session to see what tools are available,
568
+ * what skills exist in the project, and what features will be degraded.
569
+ *
570
+ * Skills are automatically discovered from:
571
+ * - .opencode/skills/
572
+ * - .claude/skills/
573
+ * - skills/
574
+ */
575
+ export const swarm_init = tool({
576
+ description:
577
+ "Initialize swarm session: discovers available skills, checks tool availability. ALWAYS call at swarm start.",
578
+ args: {
579
+ project_path: tool.schema
580
+ .string()
581
+ .optional()
582
+ .describe("Project path (for Agent Mail init)"),
583
+ },
584
+ async execute(args) {
585
+ // Check all tools
586
+ const availability = await checkAllTools();
587
+
588
+ // Build status report
589
+ const report = formatToolAvailability(availability);
590
+
591
+ // Check critical tools
592
+ const beadsAvailable = availability.get("beads")?.status.available ?? false;
593
+ const agentMailAvailable =
594
+ availability.get("agent-mail")?.status.available ?? false;
595
+
596
+ // Build warnings
597
+ const warnings: string[] = [];
598
+ const degradedFeatures: string[] = [];
599
+
600
+ if (!beadsAvailable) {
601
+ warnings.push(
602
+ "⚠️ beads (bd) not available - issue tracking disabled, swarm coordination will be limited",
603
+ );
604
+ degradedFeatures.push("issue tracking", "progress persistence");
605
+ }
606
+
607
+ if (!agentMailAvailable) {
608
+ warnings.push(
609
+ "⚠️ agent-mail not available - multi-agent communication disabled",
610
+ );
611
+ degradedFeatures.push("agent communication", "file reservations");
612
+ }
613
+
614
+ if (!availability.get("cass")?.status.available) {
615
+ degradedFeatures.push("historical context from past sessions");
616
+ }
617
+
618
+ if (!availability.get("ubs")?.status.available) {
619
+ degradedFeatures.push("pre-completion bug scanning");
620
+ }
621
+
622
+ if (!availability.get("semantic-memory")?.status.available) {
623
+ degradedFeatures.push("persistent learning (using in-memory fallback)");
624
+ }
625
+
626
+ // Discover available skills
627
+ const availableSkills = await listSkills();
628
+ const skillsInfo = {
629
+ count: availableSkills.length,
630
+ available: availableSkills.length > 0,
631
+ skills: availableSkills.map((s) => ({
632
+ name: s.name,
633
+ description: s.description,
634
+ hasScripts: s.hasScripts,
635
+ })),
636
+ };
637
+
638
+ // Add skills guidance if available
639
+ let skillsGuidance: string | undefined;
640
+ if (availableSkills.length > 0) {
641
+ skillsGuidance = `Found ${availableSkills.length} skill(s). Use skills_list to see details, skills_use to activate.`;
642
+ } else {
643
+ skillsGuidance =
644
+ "No skills found. Add skills to .opencode/skills/ or .claude/skills/ for specialized guidance.";
645
+ }
646
+
647
+ return JSON.stringify(
648
+ {
649
+ ready: true,
650
+ tool_availability: Object.fromEntries(
651
+ Array.from(availability.entries()).map(([k, v]) => [
652
+ k,
653
+ {
654
+ available: v.status.available,
655
+ fallback: v.status.available ? null : v.fallbackBehavior,
656
+ },
657
+ ]),
658
+ ),
659
+ skills: skillsInfo,
660
+ warnings: warnings.length > 0 ? warnings : undefined,
661
+ degraded_features:
662
+ degradedFeatures.length > 0 ? degradedFeatures : undefined,
663
+ recommendations: {
664
+ skills: skillsGuidance,
665
+ beads: beadsAvailable
666
+ ? "✓ Use beads for all task tracking"
667
+ : "Install beads: npm i -g @joelhooks/beads",
668
+ agent_mail: agentMailAvailable
669
+ ? "✓ Use Agent Mail for coordination"
670
+ : "Start Agent Mail: agent-mail serve",
671
+ },
672
+ report,
673
+ },
674
+ null,
675
+ 2,
676
+ );
677
+ },
678
+ });
679
+
680
+ /**
681
+ * Get status of a swarm by epic ID
682
+ *
683
+ * Requires project_key to query Agent Mail for message counts.
684
+ */
685
+ export const swarm_status = tool({
686
+ description: "Get status of a swarm by epic ID",
687
+ args: {
688
+ epic_id: tool.schema.string().describe("Epic bead ID (e.g., bd-abc123)"),
689
+ project_key: tool.schema
690
+ .string()
691
+ .describe("Project path (for Agent Mail queries)"),
692
+ },
693
+ async execute(args) {
694
+ // Query subtasks from beads
695
+ const subtasks = await queryEpicSubtasks(args.epic_id);
696
+
697
+ // Count statuses
698
+ const statusCounts = {
699
+ running: 0,
700
+ completed: 0,
701
+ failed: 0,
702
+ blocked: 0,
703
+ };
704
+
705
+ const agents: SpawnedAgent[] = [];
706
+
707
+ for (const bead of subtasks) {
708
+ // Map bead status to agent status
709
+ let agentStatus: SpawnedAgent["status"] = "pending";
710
+ switch (bead.status) {
711
+ case "in_progress":
712
+ agentStatus = "running";
713
+ statusCounts.running++;
714
+ break;
715
+ case "closed":
716
+ agentStatus = "completed";
717
+ statusCounts.completed++;
718
+ break;
719
+ case "blocked":
720
+ agentStatus = "pending"; // Blocked treated as pending for swarm
721
+ statusCounts.blocked++;
722
+ break;
723
+ default:
724
+ // open = pending
725
+ break;
726
+ }
727
+
728
+ agents.push({
729
+ bead_id: bead.id,
730
+ agent_name: "", // We don't track this in beads
731
+ status: agentStatus,
732
+ files: [], // Would need to parse from description
733
+ });
734
+ }
735
+
736
+ // Query Agent Mail for message activity
737
+ const messageCount = await querySwarmMessages(
738
+ args.project_key,
739
+ args.epic_id,
740
+ );
741
+
742
+ const status: SwarmStatus = {
743
+ epic_id: args.epic_id,
744
+ total_agents: subtasks.length,
745
+ running: statusCounts.running,
746
+ completed: statusCounts.completed,
747
+ failed: statusCounts.failed,
748
+ blocked: statusCounts.blocked,
749
+ agents,
750
+ last_update: new Date().toISOString(),
751
+ };
752
+
753
+ // Validate and return
754
+ const validated = SwarmStatusSchema.parse(status);
755
+
756
+ return JSON.stringify(
757
+ {
758
+ ...validated,
759
+ message_count: messageCount,
760
+ progress_percent:
761
+ subtasks.length > 0
762
+ ? Math.round((statusCounts.completed / subtasks.length) * 100)
763
+ : 0,
764
+ },
765
+ null,
766
+ 2,
767
+ );
768
+ },
769
+ });
770
+
771
+ /**
772
+ * Report progress on a subtask
773
+ *
774
+ * Takes explicit agent identity since tools don't have persistent state.
775
+ */
776
+ export const swarm_progress = tool({
777
+ description: "Report progress on a subtask to coordinator",
778
+ args: {
779
+ project_key: tool.schema.string().describe("Project path"),
780
+ agent_name: tool.schema.string().describe("Your Agent Mail name"),
781
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
782
+ status: tool.schema
783
+ .enum(["in_progress", "blocked", "completed", "failed"])
784
+ .describe("Current status"),
785
+ message: tool.schema
786
+ .string()
787
+ .optional()
788
+ .describe("Progress message or blockers"),
789
+ progress_percent: tool.schema
790
+ .number()
791
+ .min(0)
792
+ .max(100)
793
+ .optional()
794
+ .describe("Completion percentage"),
795
+ files_touched: tool.schema
796
+ .array(tool.schema.string())
797
+ .optional()
798
+ .describe("Files modified so far"),
799
+ },
800
+ async execute(args) {
801
+ // Build progress report
802
+ const progress: AgentProgress = {
803
+ bead_id: args.bead_id,
804
+ agent_name: args.agent_name,
805
+ status: args.status,
806
+ progress_percent: args.progress_percent,
807
+ message: args.message,
808
+ files_touched: args.files_touched,
809
+ timestamp: new Date().toISOString(),
810
+ };
811
+
812
+ // Validate
813
+ const validated = AgentProgressSchema.parse(progress);
814
+
815
+ // Update bead status if needed
816
+ if (args.status === "blocked" || args.status === "in_progress") {
817
+ const beadStatus = args.status === "blocked" ? "blocked" : "in_progress";
818
+ await Bun.$`bd update ${args.bead_id} --status ${beadStatus} --json`
819
+ .quiet()
820
+ .nothrow();
821
+ }
822
+
823
+ // Extract epic ID from bead ID (e.g., bd-abc123.1 -> bd-abc123)
824
+ const epicId = args.bead_id.includes(".")
825
+ ? args.bead_id.split(".")[0]
826
+ : args.bead_id;
827
+
828
+ // Send progress message to thread using embedded swarm-mail
829
+ await sendSwarmMessage({
830
+ projectPath: args.project_key,
831
+ fromAgent: args.agent_name,
832
+ toAgents: [], // Coordinator will pick it up from thread
833
+ subject: `Progress: ${args.bead_id} - ${args.status}`,
834
+ body: formatProgressMessage(validated),
835
+ threadId: epicId,
836
+ importance: args.status === "blocked" ? "high" : "normal",
837
+ });
838
+
839
+ return `Progress reported: ${args.status}${args.progress_percent !== undefined ? ` (${args.progress_percent}%)` : ""}`;
840
+ },
841
+ });
842
+
843
+ /**
844
+ * Broadcast context updates to all agents in the epic
845
+ *
846
+ * Enables mid-task coordination by sharing discoveries, warnings, or blockers
847
+ * with all agents working on the same epic. Agents can broadcast without
848
+ * waiting for task completion.
849
+ *
850
+ * Based on "Patterns for Building AI Agents" p.31: "Ensure subagents can share context along the way"
851
+ */
852
+ export const swarm_broadcast = tool({
853
+ description:
854
+ "Broadcast context update to all agents working on the same epic",
855
+ args: {
856
+ project_path: tool.schema
857
+ .string()
858
+ .describe("Absolute path to project root"),
859
+ agent_name: tool.schema
860
+ .string()
861
+ .describe("Name of the agent broadcasting the message"),
862
+ epic_id: tool.schema.string().describe("Epic ID (e.g., bd-abc123)"),
863
+ message: tool.schema
864
+ .string()
865
+ .describe("Context update to share (what changed, what was learned)"),
866
+ importance: tool.schema
867
+ .enum(["info", "warning", "blocker"])
868
+ .default("info")
869
+ .describe("Priority level (default: info)"),
870
+ files_affected: tool.schema
871
+ .array(tool.schema.string())
872
+ .optional()
873
+ .describe("Files this context relates to"),
874
+ },
875
+ async execute(args) {
876
+ // Extract bead_id from context if available (for traceability)
877
+ const beadId = "unknown"; // Context not currently available in tool execution
878
+
879
+ // Format the broadcast message
880
+ const body = [
881
+ `## Context Update`,
882
+ "",
883
+ `**From**: ${args.agent_name} (${beadId})`,
884
+ `**Priority**: ${args.importance.toUpperCase()}`,
885
+ "",
886
+ args.message,
887
+ "",
888
+ args.files_affected && args.files_affected.length > 0
889
+ ? `**Files affected**:\n${args.files_affected.map((f) => `- \`${f}\``).join("\n")}`
890
+ : "",
891
+ ]
892
+ .filter(Boolean)
893
+ .join("\n");
894
+
895
+ // Map importance to Agent Mail importance
896
+ const mailImportance =
897
+ args.importance === "blocker"
898
+ ? "urgent"
899
+ : args.importance === "warning"
900
+ ? "high"
901
+ : "normal";
902
+
903
+ // Send as broadcast to thread using embedded swarm-mail
904
+ await sendSwarmMessage({
905
+ projectPath: args.project_path,
906
+ fromAgent: args.agent_name,
907
+ toAgents: [], // Broadcast to thread
908
+ subject: `[${args.importance.toUpperCase()}] Context update from ${args.agent_name}`,
909
+ body,
910
+ threadId: args.epic_id,
911
+ importance: mailImportance,
912
+ ackRequired: args.importance === "blocker",
913
+ });
914
+
915
+ return JSON.stringify(
916
+ {
917
+ broadcast: true,
918
+ epic_id: args.epic_id,
919
+ from: args.agent_name,
920
+ bead_id: beadId,
921
+ importance: args.importance,
922
+ recipients: "all agents in epic",
923
+ ack_required: args.importance === "blocker",
924
+ },
925
+ null,
926
+ 2,
927
+ );
928
+ },
929
+ });
930
+
931
+ /**
932
+ * Mark a subtask as complete
933
+ *
934
+ * Implements the Verification Gate (from superpowers):
935
+ * 1. IDENTIFY: What commands prove this claim?
936
+ * 2. RUN: Execute verification (UBS, typecheck, tests)
937
+ * 3. READ: Check exit codes and output
938
+ * 4. VERIFY: All checks must pass
939
+ * 5. ONLY THEN: Close the bead
940
+ *
941
+ * Closes bead, releases reservations, notifies coordinator.
942
+ */
943
+ export const swarm_complete = tool({
944
+ description:
945
+ "Mark subtask complete with Verification Gate. Runs UBS scan, typecheck, and tests before allowing completion.",
946
+ args: {
947
+ project_key: tool.schema.string().describe("Project path"),
948
+ agent_name: tool.schema.string().describe("Your Agent Mail name"),
949
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
950
+ summary: tool.schema.string().describe("Brief summary of work done"),
951
+ evaluation: tool.schema
952
+ .string()
953
+ .optional()
954
+ .describe("Self-evaluation JSON (Evaluation schema)"),
955
+ files_touched: tool.schema
956
+ .array(tool.schema.string())
957
+ .optional()
958
+ .describe("Files modified - will be verified (UBS, typecheck, tests)"),
959
+ skip_ubs_scan: tool.schema
960
+ .boolean()
961
+ .optional()
962
+ .describe("Skip UBS bug scan (default: false)"),
963
+ skip_verification: tool.schema
964
+ .boolean()
965
+ .optional()
966
+ .describe(
967
+ "Skip ALL verification (UBS, typecheck, tests). Use sparingly! (default: false)",
968
+ ),
969
+ },
970
+ async execute(args) {
971
+ // Run Verification Gate unless explicitly skipped
972
+ let verificationResult: VerificationGateResult | null = null;
973
+
974
+ if (!args.skip_verification && args.files_touched?.length) {
975
+ verificationResult = await runVerificationGate(
976
+ args.files_touched,
977
+ args.skip_ubs_scan ?? false,
978
+ );
979
+
980
+ // Block completion if verification failed
981
+ if (!verificationResult.passed) {
982
+ return JSON.stringify(
983
+ {
984
+ success: false,
985
+ error: "Verification Gate FAILED - fix issues before completing",
986
+ verification: {
987
+ passed: false,
988
+ summary: verificationResult.summary,
989
+ blockers: verificationResult.blockers,
990
+ steps: verificationResult.steps.map((s) => ({
991
+ name: s.name,
992
+ passed: s.passed,
993
+ skipped: s.skipped,
994
+ skipReason: s.skipReason,
995
+ error: s.error?.slice(0, 200),
996
+ })),
997
+ },
998
+ hint:
999
+ verificationResult.blockers.length > 0
1000
+ ? `Fix these issues: ${verificationResult.blockers.map((b, i) => `${i + 1}. ${b}`).join(", ")}. Use skip_verification=true only as last resort.`
1001
+ : "Fix the failing checks and try again. Use skip_verification=true only as last resort.",
1002
+ gate_function:
1003
+ "IDENTIFY → RUN → READ → VERIFY → CLAIM (you are at VERIFY, claim blocked)",
1004
+ },
1005
+ null,
1006
+ 2,
1007
+ );
1008
+ }
1009
+ }
1010
+
1011
+ // Legacy UBS-only path for backward compatibility (when no files_touched)
1012
+ let ubsResult: UbsScanResult | null = null;
1013
+ if (
1014
+ !args.skip_verification &&
1015
+ !verificationResult &&
1016
+ args.files_touched?.length &&
1017
+ !args.skip_ubs_scan
1018
+ ) {
1019
+ ubsResult = await runUbsScan(args.files_touched);
1020
+
1021
+ // Block completion if critical bugs found
1022
+ if (ubsResult && ubsResult.summary.critical > 0) {
1023
+ return JSON.stringify(
1024
+ {
1025
+ success: false,
1026
+ error: `UBS found ${ubsResult.summary.critical} critical bug(s) that must be fixed before completing`,
1027
+ ubs_scan: {
1028
+ critical_count: ubsResult.summary.critical,
1029
+ bugs: ubsResult.bugs.filter((b) => b.severity === "critical"),
1030
+ },
1031
+ hint: `Fix these critical bugs: ${ubsResult.bugs
1032
+ .filter((b) => b.severity === "critical")
1033
+ .map((b) => `${b.file}:${b.line} - ${b.message}`)
1034
+ .slice(0, 3)
1035
+ .join(
1036
+ "; ",
1037
+ )}. 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).`,
1038
+ },
1039
+ null,
1040
+ 2,
1041
+ );
1042
+ }
1043
+ }
1044
+
1045
+ // Parse and validate evaluation if provided
1046
+ let parsedEvaluation: Evaluation | undefined;
1047
+ if (args.evaluation) {
1048
+ try {
1049
+ parsedEvaluation = EvaluationSchema.parse(JSON.parse(args.evaluation));
1050
+ } catch (error) {
1051
+ return JSON.stringify(
1052
+ {
1053
+ success: false,
1054
+ error: "Invalid evaluation format",
1055
+ details: error instanceof z.ZodError ? error.issues : String(error),
1056
+ },
1057
+ null,
1058
+ 2,
1059
+ );
1060
+ }
1061
+
1062
+ // If evaluation failed, don't complete
1063
+ if (!parsedEvaluation.passed) {
1064
+ return JSON.stringify(
1065
+ {
1066
+ success: false,
1067
+ error: "Self-evaluation failed",
1068
+ retry_suggestion: parsedEvaluation.retry_suggestion,
1069
+ feedback: parsedEvaluation.overall_feedback,
1070
+ },
1071
+ null,
1072
+ 2,
1073
+ );
1074
+ }
1075
+ }
1076
+
1077
+ // Close the bead
1078
+ const closeResult =
1079
+ await Bun.$`bd close ${args.bead_id} --reason ${args.summary} --json`
1080
+ .quiet()
1081
+ .nothrow();
1082
+
1083
+ if (closeResult.exitCode !== 0) {
1084
+ throw new Error(
1085
+ `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.`,
1086
+ );
1087
+ }
1088
+
1089
+ // Release file reservations for this agent using embedded swarm-mail
1090
+ try {
1091
+ await releaseSwarmFiles({
1092
+ projectPath: args.project_key,
1093
+ agentName: args.agent_name,
1094
+ // Release all reservations for this agent
1095
+ });
1096
+ } catch (error) {
1097
+ // Release might fail (e.g., no reservations existed)
1098
+ // This is non-fatal - log and continue
1099
+ console.warn(
1100
+ `[swarm] Failed to release file reservations for ${args.agent_name}:`,
1101
+ error,
1102
+ );
1103
+ }
1104
+
1105
+ // Extract epic ID
1106
+ const epicId = args.bead_id.includes(".")
1107
+ ? args.bead_id.split(".")[0]
1108
+ : args.bead_id;
1109
+
1110
+ // Send completion message using embedded swarm-mail
1111
+ const completionBody = [
1112
+ `## Subtask Complete: ${args.bead_id}`,
1113
+ "",
1114
+ `**Summary**: ${args.summary}`,
1115
+ "",
1116
+ parsedEvaluation
1117
+ ? `**Self-Evaluation**: ${parsedEvaluation.passed ? "PASSED" : "FAILED"}`
1118
+ : "",
1119
+ parsedEvaluation?.overall_feedback
1120
+ ? `**Feedback**: ${parsedEvaluation.overall_feedback}`
1121
+ : "",
1122
+ ]
1123
+ .filter(Boolean)
1124
+ .join("\n");
1125
+
1126
+ await sendSwarmMessage({
1127
+ projectPath: args.project_key,
1128
+ fromAgent: args.agent_name,
1129
+ toAgents: [], // Thread broadcast
1130
+ subject: `Complete: ${args.bead_id}`,
1131
+ body: completionBody,
1132
+ threadId: epicId,
1133
+ importance: "normal",
1134
+ });
1135
+
1136
+ // Build success response with semantic-memory integration
1137
+ const response = {
1138
+ success: true,
1139
+ bead_id: args.bead_id,
1140
+ closed: true,
1141
+ reservations_released: true,
1142
+ message_sent: true,
1143
+ verification_gate: verificationResult
1144
+ ? {
1145
+ passed: true,
1146
+ summary: verificationResult.summary,
1147
+ steps: verificationResult.steps.map((s) => ({
1148
+ name: s.name,
1149
+ passed: s.passed,
1150
+ skipped: s.skipped,
1151
+ skipReason: s.skipReason,
1152
+ })),
1153
+ }
1154
+ : args.skip_verification
1155
+ ? { skipped: true, reason: "skip_verification=true" }
1156
+ : { skipped: true, reason: "no files_touched provided" },
1157
+ ubs_scan: ubsResult
1158
+ ? {
1159
+ ran: true,
1160
+ bugs_found: ubsResult.summary.total,
1161
+ summary: ubsResult.summary,
1162
+ warnings: ubsResult.bugs.filter((b) => b.severity !== "critical"),
1163
+ }
1164
+ : verificationResult
1165
+ ? { ran: true, included_in_verification_gate: true }
1166
+ : {
1167
+ ran: false,
1168
+ reason: args.skip_ubs_scan
1169
+ ? "skipped"
1170
+ : "no files or ubs unavailable",
1171
+ },
1172
+ learning_prompt: `## Reflection
1173
+
1174
+ Did you learn anything reusable during this subtask? Consider:
1175
+
1176
+ 1. **Patterns**: Any code patterns or approaches that worked well?
1177
+ 2. **Gotchas**: Edge cases or pitfalls to warn future agents about?
1178
+ 3. **Best Practices**: Domain-specific guidelines worth documenting?
1179
+ 4. **Tool Usage**: Effective ways to use tools for this type of task?
1180
+
1181
+ If you discovered something valuable, use \`swarm_learn\` or \`skills_create\` to preserve it as a skill for future swarms.
1182
+
1183
+ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
1184
+ // Add semantic-memory integration on success
1185
+ memory_store: formatMemoryStoreOnSuccess(
1186
+ args.bead_id,
1187
+ args.summary,
1188
+ args.files_touched || [],
1189
+ ),
1190
+ };
1191
+
1192
+ return JSON.stringify(response, null, 2);
1193
+ },
1194
+ });
1195
+
1196
+ /**
1197
+ * Record outcome signals from a completed subtask
1198
+ *
1199
+ * Tracks implicit feedback (duration, errors, retries) to score
1200
+ * decomposition quality over time. This data feeds into criterion
1201
+ * weight calculations.
1202
+ *
1203
+ * Strategy tracking enables learning about which decomposition strategies
1204
+ * work best for different task types.
1205
+ *
1206
+ * @see src/learning.ts for scoring logic
1207
+ */
1208
+ export const swarm_record_outcome = tool({
1209
+ description:
1210
+ "Record subtask outcome for implicit feedback scoring. Tracks duration, errors, retries to learn decomposition quality.",
1211
+ args: {
1212
+ bead_id: tool.schema.string().describe("Subtask bead ID"),
1213
+ duration_ms: tool.schema
1214
+ .number()
1215
+ .int()
1216
+ .min(0)
1217
+ .describe("Duration in milliseconds"),
1218
+ error_count: tool.schema
1219
+ .number()
1220
+ .int()
1221
+ .min(0)
1222
+ .default(0)
1223
+ .describe("Number of errors encountered"),
1224
+ retry_count: tool.schema
1225
+ .number()
1226
+ .int()
1227
+ .min(0)
1228
+ .default(0)
1229
+ .describe("Number of retry attempts"),
1230
+ success: tool.schema.boolean().describe("Whether the subtask succeeded"),
1231
+ files_touched: tool.schema
1232
+ .array(tool.schema.string())
1233
+ .optional()
1234
+ .describe("Files that were modified"),
1235
+ criteria: tool.schema
1236
+ .array(tool.schema.string())
1237
+ .optional()
1238
+ .describe(
1239
+ "Criteria to generate feedback for (default: all default criteria)",
1240
+ ),
1241
+ strategy: tool.schema
1242
+ .enum(["file-based", "feature-based", "risk-based", "research-based"])
1243
+ .optional()
1244
+ .describe("Decomposition strategy used for this task"),
1245
+ failure_mode: tool.schema
1246
+ .enum([
1247
+ "timeout",
1248
+ "conflict",
1249
+ "validation",
1250
+ "tool_failure",
1251
+ "context_overflow",
1252
+ "dependency_blocked",
1253
+ "user_cancelled",
1254
+ "unknown",
1255
+ ])
1256
+ .optional()
1257
+ .describe(
1258
+ "Failure classification (only when success=false). Auto-classified if not provided.",
1259
+ ),
1260
+ failure_details: tool.schema
1261
+ .string()
1262
+ .optional()
1263
+ .describe("Detailed failure context (error message, stack trace, etc.)"),
1264
+ },
1265
+ async execute(args) {
1266
+ // Build outcome signals
1267
+ const signals: OutcomeSignals = {
1268
+ bead_id: args.bead_id,
1269
+ duration_ms: args.duration_ms,
1270
+ error_count: args.error_count ?? 0,
1271
+ retry_count: args.retry_count ?? 0,
1272
+ success: args.success,
1273
+ files_touched: args.files_touched ?? [],
1274
+ timestamp: new Date().toISOString(),
1275
+ strategy: args.strategy as LearningDecompositionStrategy | undefined,
1276
+ failure_mode: args.failure_mode,
1277
+ failure_details: args.failure_details,
1278
+ };
1279
+
1280
+ // If task failed but no failure_mode provided, try to classify from failure_details
1281
+ if (!args.success && !args.failure_mode && args.failure_details) {
1282
+ const classified = classifyFailure(args.failure_details);
1283
+ signals.failure_mode = classified as OutcomeSignals["failure_mode"];
1284
+ }
1285
+
1286
+ // Validate signals
1287
+ const validated = OutcomeSignalsSchema.parse(signals);
1288
+
1289
+ // Score the outcome
1290
+ const scored: ScoredOutcome = scoreImplicitFeedback(
1291
+ validated,
1292
+ DEFAULT_LEARNING_CONFIG,
1293
+ );
1294
+
1295
+ // Get error patterns from accumulator
1296
+ const errorStats = await globalErrorAccumulator.getErrorStats(args.bead_id);
1297
+
1298
+ // Generate feedback events for each criterion
1299
+ const criteriaToScore = args.criteria ?? [
1300
+ "type_safe",
1301
+ "no_bugs",
1302
+ "patterns",
1303
+ "readable",
1304
+ ];
1305
+ const feedbackEvents: FeedbackEvent[] = criteriaToScore.map((criterion) => {
1306
+ const event = outcomeToFeedback(scored, criterion);
1307
+ // Include strategy in feedback context for future analysis
1308
+ if (args.strategy) {
1309
+ event.context =
1310
+ `${event.context || ""} [strategy: ${args.strategy}]`.trim();
1311
+ }
1312
+ // Include error patterns in feedback context
1313
+ if (errorStats.total > 0) {
1314
+ const errorSummary = Object.entries(errorStats.by_type)
1315
+ .map(([type, count]) => `${type}:${count}`)
1316
+ .join(", ");
1317
+ event.context =
1318
+ `${event.context || ""} [errors: ${errorSummary}]`.trim();
1319
+ }
1320
+ return event;
1321
+ });
1322
+
1323
+ return JSON.stringify(
1324
+ {
1325
+ success: true,
1326
+ outcome: {
1327
+ signals: validated,
1328
+ scored: {
1329
+ type: scored.type,
1330
+ decayed_value: scored.decayed_value,
1331
+ reasoning: scored.reasoning,
1332
+ },
1333
+ },
1334
+ feedback_events: feedbackEvents,
1335
+ error_patterns: errorStats,
1336
+ summary: {
1337
+ feedback_type: scored.type,
1338
+ duration_seconds: Math.round(args.duration_ms / 1000),
1339
+ error_count: args.error_count ?? 0,
1340
+ retry_count: args.retry_count ?? 0,
1341
+ success: args.success,
1342
+ strategy: args.strategy,
1343
+ failure_mode: validated.failure_mode,
1344
+ failure_details: validated.failure_details,
1345
+ accumulated_errors: errorStats.total,
1346
+ unresolved_errors: errorStats.unresolved,
1347
+ },
1348
+ note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights.",
1349
+ },
1350
+ null,
1351
+ 2,
1352
+ );
1353
+ },
1354
+ });
1355
+
1356
+ /**
1357
+ * Record an error during subtask execution
1358
+ *
1359
+ * Implements pattern from "Patterns for Building AI Agents" p.40:
1360
+ * "Good agents examine and correct errors when something goes wrong"
1361
+ *
1362
+ * Errors are accumulated and can be fed into retry prompts to help
1363
+ * agents learn from past failures.
1364
+ */
1365
+ export const swarm_accumulate_error = tool({
1366
+ description:
1367
+ "Record an error during subtask execution. Errors feed into retry prompts.",
1368
+ args: {
1369
+ bead_id: tool.schema.string().describe("Bead ID where error occurred"),
1370
+ error_type: tool.schema
1371
+ .enum(["validation", "timeout", "conflict", "tool_failure", "unknown"])
1372
+ .describe("Category of error"),
1373
+ message: tool.schema.string().describe("Human-readable error message"),
1374
+ stack_trace: tool.schema
1375
+ .string()
1376
+ .optional()
1377
+ .describe("Stack trace for debugging"),
1378
+ tool_name: tool.schema.string().optional().describe("Tool that failed"),
1379
+ context: tool.schema
1380
+ .string()
1381
+ .optional()
1382
+ .describe("What was happening when error occurred"),
1383
+ },
1384
+ async execute(args) {
1385
+ const entry = await globalErrorAccumulator.recordError(
1386
+ args.bead_id,
1387
+ args.error_type as ErrorType,
1388
+ args.message,
1389
+ {
1390
+ stack_trace: args.stack_trace,
1391
+ tool_name: args.tool_name,
1392
+ context: args.context,
1393
+ },
1394
+ );
1395
+
1396
+ return JSON.stringify(
1397
+ {
1398
+ success: true,
1399
+ error_id: entry.id,
1400
+ bead_id: entry.bead_id,
1401
+ error_type: entry.error_type,
1402
+ message: entry.message,
1403
+ timestamp: entry.timestamp,
1404
+ note: "Error recorded for retry context. Use swarm_get_error_context to retrieve accumulated errors.",
1405
+ },
1406
+ null,
1407
+ 2,
1408
+ );
1409
+ },
1410
+ });
1411
+
1412
+ /**
1413
+ * Get accumulated errors for a bead to feed into retry prompts
1414
+ *
1415
+ * Returns formatted error context that can be injected into retry prompts
1416
+ * to help agents learn from past failures.
1417
+ */
1418
+ export const swarm_get_error_context = tool({
1419
+ description:
1420
+ "Get accumulated errors for a bead. Returns formatted context for retry prompts.",
1421
+ args: {
1422
+ bead_id: tool.schema.string().describe("Bead ID to get errors for"),
1423
+ include_resolved: tool.schema
1424
+ .boolean()
1425
+ .optional()
1426
+ .describe("Include resolved errors (default: false)"),
1427
+ },
1428
+ async execute(args) {
1429
+ const errorContext = await globalErrorAccumulator.getErrorContext(
1430
+ args.bead_id,
1431
+ args.include_resolved ?? false,
1432
+ );
1433
+
1434
+ const stats = await globalErrorAccumulator.getErrorStats(args.bead_id);
1435
+
1436
+ return JSON.stringify(
1437
+ {
1438
+ bead_id: args.bead_id,
1439
+ error_context: errorContext,
1440
+ stats: {
1441
+ total_errors: stats.total,
1442
+ unresolved: stats.unresolved,
1443
+ by_type: stats.by_type,
1444
+ },
1445
+ has_errors: errorContext.length > 0,
1446
+ usage:
1447
+ "Inject error_context into retry prompt using {error_context} placeholder",
1448
+ },
1449
+ null,
1450
+ 2,
1451
+ );
1452
+ },
1453
+ });
1454
+
1455
+ /**
1456
+ * Mark an error as resolved
1457
+ *
1458
+ * Call this after an agent successfully addresses an error to update
1459
+ * the accumulator state.
1460
+ */
1461
+ export const swarm_resolve_error = tool({
1462
+ description:
1463
+ "Mark an error as resolved after fixing it. Updates error accumulator state.",
1464
+ args: {
1465
+ error_id: tool.schema.string().describe("Error ID to mark as resolved"),
1466
+ },
1467
+ async execute(args) {
1468
+ await globalErrorAccumulator.resolveError(args.error_id);
1469
+
1470
+ return JSON.stringify(
1471
+ {
1472
+ success: true,
1473
+ error_id: args.error_id,
1474
+ resolved: true,
1475
+ },
1476
+ null,
1477
+ 2,
1478
+ );
1479
+ },
1480
+ });
1481
+
1482
+ /**
1483
+ * Check if a bead has struck out (3 consecutive failures)
1484
+ *
1485
+ * The 3-Strike Rule:
1486
+ * IF 3+ fixes have failed:
1487
+ * STOP → Question the architecture
1488
+ * DON'T attempt Fix #4
1489
+ * Discuss with human partner
1490
+ *
1491
+ * This is NOT a failed hypothesis.
1492
+ * This is a WRONG ARCHITECTURE.
1493
+ *
1494
+ * Use this tool to:
1495
+ * - Check strike count before attempting a fix
1496
+ * - Get architecture review prompt if struck out
1497
+ * - Record a strike when a fix fails
1498
+ * - Clear strikes when a fix succeeds
1499
+ */
1500
+ export const swarm_check_strikes = tool({
1501
+ description:
1502
+ "Check 3-strike status for a bead. Records failures, detects architectural problems, generates architecture review prompts.",
1503
+ args: {
1504
+ bead_id: tool.schema.string().describe("Bead ID to check"),
1505
+ action: tool.schema
1506
+ .enum(["check", "add_strike", "clear", "get_prompt"])
1507
+ .describe(
1508
+ "Action: check count, add strike, clear strikes, or get prompt",
1509
+ ),
1510
+ attempt: tool.schema
1511
+ .string()
1512
+ .optional()
1513
+ .describe("Description of fix attempt (required for add_strike)"),
1514
+ reason: tool.schema
1515
+ .string()
1516
+ .optional()
1517
+ .describe("Why the fix failed (required for add_strike)"),
1518
+ },
1519
+ async execute(args) {
1520
+ switch (args.action) {
1521
+ case "check": {
1522
+ const count = await getStrikes(args.bead_id, globalStrikeStorage);
1523
+ const strikedOut = await isStrikedOut(
1524
+ args.bead_id,
1525
+ globalStrikeStorage,
1526
+ );
1527
+
1528
+ return JSON.stringify(
1529
+ {
1530
+ bead_id: args.bead_id,
1531
+ strike_count: count,
1532
+ is_striked_out: strikedOut,
1533
+ message: strikedOut
1534
+ ? "⚠️ STRUCK OUT: 3 strikes reached. Use get_prompt action for architecture review."
1535
+ : count === 0
1536
+ ? "No strikes. Clear to proceed."
1537
+ : `${count} strike${count > 1 ? "s" : ""}. ${3 - count} remaining before architecture review required.`,
1538
+ next_action: strikedOut
1539
+ ? "Call with action=get_prompt to get architecture review questions"
1540
+ : "Continue with fix attempt",
1541
+ },
1542
+ null,
1543
+ 2,
1544
+ );
1545
+ }
1546
+
1547
+ case "add_strike": {
1548
+ if (!args.attempt || !args.reason) {
1549
+ return JSON.stringify(
1550
+ {
1551
+ error: "add_strike requires 'attempt' and 'reason' parameters",
1552
+ },
1553
+ null,
1554
+ 2,
1555
+ );
1556
+ }
1557
+
1558
+ const record = await addStrike(
1559
+ args.bead_id,
1560
+ args.attempt,
1561
+ args.reason,
1562
+ globalStrikeStorage,
1563
+ );
1564
+
1565
+ const strikedOut = record.strike_count >= 3;
1566
+
1567
+ // Build response with memory storage hint on 3-strike
1568
+ const response: Record<string, unknown> = {
1569
+ bead_id: args.bead_id,
1570
+ strike_count: record.strike_count,
1571
+ is_striked_out: strikedOut,
1572
+ failures: record.failures,
1573
+ message: strikedOut
1574
+ ? "⚠️ STRUCK OUT: 3 strikes reached. STOP and question the architecture."
1575
+ : `Strike ${record.strike_count} recorded. ${3 - record.strike_count} remaining.`,
1576
+ warning: strikedOut
1577
+ ? "DO NOT attempt Fix #4. Call with action=get_prompt for architecture review."
1578
+ : undefined,
1579
+ };
1580
+
1581
+ // Add semantic-memory storage hint on 3-strike
1582
+ if (strikedOut) {
1583
+ response.memory_store = formatMemoryStoreOn3Strike(
1584
+ args.bead_id,
1585
+ record.failures,
1586
+ );
1587
+ }
1588
+
1589
+ return JSON.stringify(response, null, 2);
1590
+ }
1591
+
1592
+ case "clear": {
1593
+ await clearStrikes(args.bead_id, globalStrikeStorage);
1594
+
1595
+ return JSON.stringify(
1596
+ {
1597
+ bead_id: args.bead_id,
1598
+ strike_count: 0,
1599
+ is_striked_out: false,
1600
+ message: "Strikes cleared. Fresh start.",
1601
+ },
1602
+ null,
1603
+ 2,
1604
+ );
1605
+ }
1606
+
1607
+ case "get_prompt": {
1608
+ const prompt = await getArchitecturePrompt(
1609
+ args.bead_id,
1610
+ globalStrikeStorage,
1611
+ );
1612
+
1613
+ if (!prompt) {
1614
+ return JSON.stringify(
1615
+ {
1616
+ bead_id: args.bead_id,
1617
+ has_prompt: false,
1618
+ message: "No architecture prompt (not struck out yet)",
1619
+ },
1620
+ null,
1621
+ 2,
1622
+ );
1623
+ }
1624
+
1625
+ return JSON.stringify(
1626
+ {
1627
+ bead_id: args.bead_id,
1628
+ has_prompt: true,
1629
+ architecture_review_prompt: prompt,
1630
+ message:
1631
+ "Architecture review required. Present this prompt to the human partner.",
1632
+ },
1633
+ null,
1634
+ 2,
1635
+ );
1636
+ }
1637
+
1638
+ default:
1639
+ return JSON.stringify(
1640
+ {
1641
+ error: `Unknown action: ${args.action}`,
1642
+ },
1643
+ null,
1644
+ 2,
1645
+ );
1646
+ }
1647
+ },
1648
+ });
1649
+
1650
+ /**
1651
+ * Learn from completed work and optionally create a skill
1652
+ *
1653
+ * This tool helps agents reflect on patterns, best practices, or domain
1654
+ * knowledge discovered during task execution and codify them into reusable
1655
+ * skills for future swarms.
1656
+ *
1657
+ * Implements the "learning swarm" pattern where swarms get smarter over time.
1658
+ */
1659
+ export const swarm_learn = tool({
1660
+ description: `Analyze completed work and optionally create a skill from learned patterns.
1661
+
1662
+ Use after completing a subtask when you've discovered:
1663
+ - Reusable code patterns or approaches
1664
+ - Domain-specific best practices
1665
+ - Gotchas or edge cases to warn about
1666
+ - Effective tool usage patterns
1667
+
1668
+ This tool helps you formalize learnings into a skill that future agents can discover and use.`,
1669
+ args: {
1670
+ summary: tool.schema
1671
+ .string()
1672
+ .describe("Brief summary of what was learned (1-2 sentences)"),
1673
+ pattern_type: tool.schema
1674
+ .enum([
1675
+ "code-pattern",
1676
+ "best-practice",
1677
+ "gotcha",
1678
+ "tool-usage",
1679
+ "domain-knowledge",
1680
+ "workflow",
1681
+ ])
1682
+ .describe("Category of the learning"),
1683
+ details: tool.schema
1684
+ .string()
1685
+ .describe("Detailed explanation of the pattern or practice"),
1686
+ example: tool.schema
1687
+ .string()
1688
+ .optional()
1689
+ .describe("Code example or concrete illustration"),
1690
+ when_to_use: tool.schema
1691
+ .string()
1692
+ .describe("When should an agent apply this knowledge?"),
1693
+ files_context: tool.schema
1694
+ .array(tool.schema.string())
1695
+ .optional()
1696
+ .describe("Files that exemplify this pattern"),
1697
+ create_skill: tool.schema
1698
+ .boolean()
1699
+ .optional()
1700
+ .describe(
1701
+ "Create a skill from this learning (default: false, just document)",
1702
+ ),
1703
+ skill_name: tool.schema
1704
+ .string()
1705
+ .regex(/^[a-z0-9-]+$/)
1706
+ .max(64)
1707
+ .optional()
1708
+ .describe("Skill name if creating (required if create_skill=true)"),
1709
+ skill_tags: tool.schema
1710
+ .array(tool.schema.string())
1711
+ .optional()
1712
+ .describe("Tags for the skill if creating"),
1713
+ },
1714
+ async execute(args) {
1715
+ // Format the learning as structured documentation
1716
+ const learning = {
1717
+ summary: args.summary,
1718
+ type: args.pattern_type,
1719
+ details: args.details,
1720
+ example: args.example,
1721
+ when_to_use: args.when_to_use,
1722
+ files_context: args.files_context,
1723
+ recorded_at: new Date().toISOString(),
1724
+ };
1725
+
1726
+ // If creating a skill, generate and create it
1727
+ if (args.create_skill) {
1728
+ if (!args.skill_name) {
1729
+ return JSON.stringify(
1730
+ {
1731
+ success: false,
1732
+ error: "skill_name is required when create_skill=true",
1733
+ learning: learning,
1734
+ },
1735
+ null,
1736
+ 2,
1737
+ );
1738
+ }
1739
+
1740
+ // Build skill body from learning
1741
+ const skillBody = `# ${args.summary}
1742
+
1743
+ ## When to Use
1744
+ ${args.when_to_use}
1745
+
1746
+ ## ${args.pattern_type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
1747
+
1748
+ ${args.details}
1749
+
1750
+ ${args.example ? `## Example\n\n\`\`\`\n${args.example}\n\`\`\`\n` : ""}
1751
+ ${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n${args.files_context.map((f) => `- \`${f}\``).join("\n")}\n` : ""}
1752
+
1753
+ ---
1754
+ *Learned from swarm execution on ${new Date().toISOString().split("T")[0]}*`;
1755
+
1756
+ // Import skills_create functionality
1757
+ const { getSkill, invalidateSkillsCache } = await import("./skills");
1758
+ const { mkdir, writeFile } = await import("node:fs/promises");
1759
+ const { join } = await import("node:path");
1760
+
1761
+ // Check if skill exists
1762
+ const existing = await getSkill(args.skill_name);
1763
+ if (existing) {
1764
+ return JSON.stringify(
1765
+ {
1766
+ success: false,
1767
+ error: `Skill '${args.skill_name}' already exists`,
1768
+ existing_path: existing.path,
1769
+ learning: learning,
1770
+ suggestion:
1771
+ "Use skills_update to add to existing skill, or choose a different name",
1772
+ },
1773
+ null,
1774
+ 2,
1775
+ );
1776
+ }
1777
+
1778
+ // Create skill directory and file
1779
+ const skillDir = join(
1780
+ process.cwd(),
1781
+ ".opencode",
1782
+ "skills",
1783
+ args.skill_name,
1784
+ );
1785
+ const skillPath = join(skillDir, "SKILL.md");
1786
+
1787
+ const frontmatter = [
1788
+ "---",
1789
+ `name: ${args.skill_name}`,
1790
+ `description: ${args.when_to_use.slice(0, 200)}${args.when_to_use.length > 200 ? "..." : ""}`,
1791
+ "tags:",
1792
+ ` - ${args.pattern_type}`,
1793
+ ` - learned`,
1794
+ ...(args.skill_tags || []).map((t) => ` - ${t}`),
1795
+ "---",
1796
+ ].join("\n");
1797
+
1798
+ try {
1799
+ await mkdir(skillDir, { recursive: true });
1800
+ await writeFile(skillPath, `${frontmatter}\n\n${skillBody}`, "utf-8");
1801
+ invalidateSkillsCache();
1802
+
1803
+ return JSON.stringify(
1804
+ {
1805
+ success: true,
1806
+ skill_created: true,
1807
+ skill: {
1808
+ name: args.skill_name,
1809
+ path: skillPath,
1810
+ type: args.pattern_type,
1811
+ },
1812
+ learning: learning,
1813
+ message: `Created skill '${args.skill_name}' from learned pattern. Future agents can discover it with skills_list.`,
1814
+ },
1815
+ null,
1816
+ 2,
1817
+ );
1818
+ } catch (error) {
1819
+ return JSON.stringify(
1820
+ {
1821
+ success: false,
1822
+ error: `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`,
1823
+ learning: learning,
1824
+ },
1825
+ null,
1826
+ 2,
1827
+ );
1828
+ }
1829
+ }
1830
+
1831
+ // Just document the learning without creating a skill
1832
+ return JSON.stringify(
1833
+ {
1834
+ success: true,
1835
+ skill_created: false,
1836
+ learning: learning,
1837
+ message:
1838
+ "Learning documented. Use create_skill=true to persist as a skill for future agents.",
1839
+ suggested_skill_name:
1840
+ args.skill_name ||
1841
+ args.summary
1842
+ .toLowerCase()
1843
+ .replace(/[^a-z0-9\s-]/g, "")
1844
+ .replace(/\s+/g, "-")
1845
+ .slice(0, 64),
1846
+ },
1847
+ null,
1848
+ 2,
1849
+ );
1850
+ },
1851
+ });
1852
+
1853
+ // ============================================================================
1854
+ // Export tools
1855
+ // ============================================================================
1856
+
1857
+ export const orchestrateTools = {
1858
+ swarm_init,
1859
+ swarm_status,
1860
+ swarm_progress,
1861
+ swarm_broadcast,
1862
+ swarm_complete,
1863
+ swarm_record_outcome,
1864
+ swarm_accumulate_error,
1865
+ swarm_get_error_context,
1866
+ swarm_resolve_error,
1867
+ swarm_check_strikes,
1868
+ swarm_learn,
1869
+ };