opencode-swarm-plugin 0.12.19 → 0.12.22

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/src/swarm.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  type SpawnedAgent,
26
26
  type Bead,
27
27
  } from "./schemas";
28
- import { mcpCall, requireState } from "./agent-mail";
28
+ import { mcpCall, mcpCallWithAutoInit, requireState } from "./agent-mail";
29
29
  import {
30
30
  OutcomeSignalsSchema,
31
31
  DecompositionStrategySchema,
@@ -48,6 +48,11 @@ import {
48
48
  formatToolAvailability,
49
49
  type ToolName,
50
50
  } from "./tool-availability";
51
+ import {
52
+ getSkillsContextForSwarm,
53
+ findRelevantSkills,
54
+ listSkills,
55
+ } from "./skills";
51
56
 
52
57
  // ============================================================================
53
58
  // Conflict Detection
@@ -708,16 +713,27 @@ Only modify these files. Need others? Message the coordinator.
708
713
  ### Agent Mail
709
714
  - agentmail_send (thread_id: {epic_id})
710
715
 
716
+ ### Skills (if available)
717
+ - skills_list (discover available skills)
718
+ - skills_use (activate a skill for specialized guidance)
719
+
711
720
  ### Completion
712
721
  - swarm_complete (REQUIRED when done)
713
722
 
723
+ ## [LEARNING]
724
+ As you work, note reusable patterns, best practices, or domain insights:
725
+ - If you discover something that would help future agents, consider creating a skill
726
+ - Use skills_create to codify patterns for the project
727
+ - Good skills have clear "when to use" descriptions with actionable instructions
728
+ - Skills make swarms smarter over time
729
+
714
730
  ## [OUTPUT]
715
731
  1. Read files first
716
732
  2. Implement changes
717
733
  3. Verify (typecheck)
718
734
  4. Complete with swarm_complete
719
735
 
720
- Return: Summary of changes made
736
+ Return: Summary of changes made. Note any patterns worth preserving as skills.
721
737
 
722
738
  **Never work silently.** Communicate progress and blockers immediately.
723
739
 
@@ -1157,6 +1173,8 @@ const STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into paralleli
1157
1173
 
1158
1174
  {cass_history}
1159
1175
 
1176
+ {skills_context}
1177
+
1160
1178
  ## MANDATORY: Beads Issue Tracking
1161
1179
 
1162
1180
  **Every subtask MUST become a bead.** This is non-negotiable.
@@ -1222,7 +1240,7 @@ export const swarm_plan_prompt = tool({
1222
1240
  .number()
1223
1241
  .int()
1224
1242
  .min(2)
1225
-
1243
+
1226
1244
  .default(5)
1227
1245
  .describe("Maximum number of subtasks (default: 5)"),
1228
1246
  context: tool.schema
@@ -1237,9 +1255,13 @@ export const swarm_plan_prompt = tool({
1237
1255
  .number()
1238
1256
  .int()
1239
1257
  .min(1)
1240
-
1258
+
1241
1259
  .optional()
1242
1260
  .describe("Max CASS results to include (default: 3)"),
1261
+ include_skills: tool.schema
1262
+ .boolean()
1263
+ .optional()
1264
+ .describe("Include available skills in context (default: true)"),
1243
1265
  },
1244
1266
  async execute(args) {
1245
1267
  // Select strategy
@@ -1288,6 +1310,30 @@ export const swarm_plan_prompt = tool({
1288
1310
  cassResultInfo = { queried: false, reason: "disabled" };
1289
1311
  }
1290
1312
 
1313
+ // Fetch skills context
1314
+ let skillsContext = "";
1315
+ let skillsInfo: { included: boolean; count?: number; relevant?: string[] } = {
1316
+ included: false,
1317
+ };
1318
+
1319
+ if (args.include_skills !== false) {
1320
+ const allSkills = await listSkills();
1321
+ if (allSkills.length > 0) {
1322
+ skillsContext = await getSkillsContextForSwarm();
1323
+ const relevantSkills = await findRelevantSkills(args.task);
1324
+ skillsInfo = {
1325
+ included: true,
1326
+ count: allSkills.length,
1327
+ relevant: relevantSkills,
1328
+ };
1329
+
1330
+ // Add suggestion for relevant skills
1331
+ if (relevantSkills.length > 0) {
1332
+ skillsContext += `\n\n**Suggested skills for this task**: ${relevantSkills.join(", ")}`;
1333
+ }
1334
+ }
1335
+ }
1336
+
1291
1337
  // Format strategy guidelines
1292
1338
  const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
1293
1339
 
@@ -1301,6 +1347,7 @@ export const swarm_plan_prompt = tool({
1301
1347
  .replace("{strategy_guidelines}", strategyGuidelines)
1302
1348
  .replace("{context_section}", contextSection)
1303
1349
  .replace("{cass_history}", cassContext || "")
1350
+ .replace("{skills_context}", skillsContext || "")
1304
1351
  .replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
1305
1352
 
1306
1353
  return JSON.stringify(
@@ -1328,6 +1375,7 @@ export const swarm_plan_prompt = tool({
1328
1375
  validation_note:
1329
1376
  "Parse agent response as JSON and validate with swarm_validate_decomposition",
1330
1377
  cass_history: cassResultInfo,
1378
+ skills: skillsInfo,
1331
1379
  },
1332
1380
  null,
1333
1381
  2,
@@ -1352,7 +1400,7 @@ export const swarm_decompose = tool({
1352
1400
  .number()
1353
1401
  .int()
1354
1402
  .min(2)
1355
-
1403
+
1356
1404
  .default(5)
1357
1405
  .describe("Maximum number of subtasks (default: 5)"),
1358
1406
  context: tool.schema
@@ -1367,7 +1415,7 @@ export const swarm_decompose = tool({
1367
1415
  .number()
1368
1416
  .int()
1369
1417
  .min(1)
1370
-
1418
+
1371
1419
  .optional()
1372
1420
  .describe("Max CASS results to include (default: 3)"),
1373
1421
  },
@@ -1721,9 +1769,10 @@ export const swarm_progress = tool({
1721
1769
  ? args.bead_id.split(".")[0]
1722
1770
  : args.bead_id;
1723
1771
 
1724
- // Send progress message to thread
1725
- await mcpCall("send_message", {
1772
+ // Send progress message to thread (with auto-reinit on server restart)
1773
+ await mcpCallWithAutoInit("send_message", {
1726
1774
  project_key: args.project_key,
1775
+ agent_name: args.agent_name,
1727
1776
  sender_name: args.agent_name,
1728
1777
  to: [], // Coordinator will pick it up from thread
1729
1778
  subject: `Progress: ${args.bead_id} - ${args.status}`,
@@ -1892,8 +1941,10 @@ export const swarm_broadcast = tool({
1892
1941
  : "normal";
1893
1942
 
1894
1943
  // Send as broadcast to thread (empty 'to' = all agents in thread)
1895
- await mcpCall("send_message", {
1944
+ // Uses auto-reinit wrapper to handle server restarts gracefully
1945
+ await mcpCallWithAutoInit("send_message", {
1896
1946
  project_key: state.projectKey,
1947
+ agent_name: state.agentName,
1897
1948
  sender_name: state.agentName,
1898
1949
  to: [], // Broadcast to thread
1899
1950
  subject: `[${args.importance.toUpperCase()}] Context update from ${state.agentName}`,
@@ -2020,12 +2071,16 @@ export const swarm_complete = tool({
2020
2071
  }
2021
2072
 
2022
2073
  // Release file reservations for this agent
2074
+ // Uses auto-reinit wrapper to handle server restarts - this was the original
2075
+ // failure point that prompted the self-healing implementation
2023
2076
  try {
2024
- await mcpCall("release_file_reservations", {
2077
+ await mcpCallWithAutoInit("release_file_reservations", {
2025
2078
  project_key: args.project_key,
2026
2079
  agent_name: args.agent_name,
2027
2080
  });
2028
2081
  } catch (error) {
2082
+ // Even with auto-reinit, release might fail (e.g., no reservations existed)
2083
+ // This is non-fatal - log and continue
2029
2084
  console.warn(
2030
2085
  `[swarm] Failed to release file reservations for ${args.agent_name}:`,
2031
2086
  error,
@@ -2053,8 +2108,9 @@ export const swarm_complete = tool({
2053
2108
  .filter(Boolean)
2054
2109
  .join("\n");
2055
2110
 
2056
- await mcpCall("send_message", {
2111
+ await mcpCallWithAutoInit("send_message", {
2057
2112
  project_key: args.project_key,
2113
+ agent_name: args.agent_name,
2058
2114
  sender_name: args.agent_name,
2059
2115
  to: [], // Thread broadcast
2060
2116
  subject: `Complete: ${args.bead_id}`,
@@ -2083,6 +2139,18 @@ export const swarm_complete = tool({
2083
2139
  ? "skipped"
2084
2140
  : "no files or ubs unavailable",
2085
2141
  },
2142
+ learning_prompt: `## Reflection
2143
+
2144
+ Did you learn anything reusable during this subtask? Consider:
2145
+
2146
+ 1. **Patterns**: Any code patterns or approaches that worked well?
2147
+ 2. **Gotchas**: Edge cases or pitfalls to warn future agents about?
2148
+ 3. **Best Practices**: Domain-specific guidelines worth documenting?
2149
+ 4. **Tool Usage**: Effective ways to use tools for this type of task?
2150
+
2151
+ If you discovered something valuable, use \`swarm_learn\` or \`skills_create\` to preserve it as a skill for future swarms.
2152
+
2153
+ Files touched: ${args.files_touched?.join(", ") || "none recorded"}`,
2086
2154
  },
2087
2155
  null,
2088
2156
  2,
@@ -2554,6 +2622,196 @@ export const swarm_evaluation_prompt = tool({
2554
2622
  },
2555
2623
  });
2556
2624
 
2625
+ // ============================================================================
2626
+ // Swarm Learning
2627
+ // ============================================================================
2628
+
2629
+ /**
2630
+ * Learn from completed work and optionally create a skill
2631
+ *
2632
+ * This tool helps agents reflect on patterns, best practices, or domain
2633
+ * knowledge discovered during task execution and codify them into reusable
2634
+ * skills for future swarms.
2635
+ *
2636
+ * Implements the "learning swarm" pattern where swarms get smarter over time.
2637
+ */
2638
+ export const swarm_learn = tool({
2639
+ description: `Analyze completed work and optionally create a skill from learned patterns.
2640
+
2641
+ Use after completing a subtask when you've discovered:
2642
+ - Reusable code patterns or approaches
2643
+ - Domain-specific best practices
2644
+ - Gotchas or edge cases to warn about
2645
+ - Effective tool usage patterns
2646
+
2647
+ This tool helps you formalize learnings into a skill that future agents can discover and use.`,
2648
+ args: {
2649
+ summary: tool.schema
2650
+ .string()
2651
+ .describe("Brief summary of what was learned (1-2 sentences)"),
2652
+ pattern_type: tool.schema
2653
+ .enum(["code-pattern", "best-practice", "gotcha", "tool-usage", "domain-knowledge", "workflow"])
2654
+ .describe("Category of the learning"),
2655
+ details: tool.schema
2656
+ .string()
2657
+ .describe("Detailed explanation of the pattern or practice"),
2658
+ example: tool.schema
2659
+ .string()
2660
+ .optional()
2661
+ .describe("Code example or concrete illustration"),
2662
+ when_to_use: tool.schema
2663
+ .string()
2664
+ .describe("When should an agent apply this knowledge?"),
2665
+ files_context: tool.schema
2666
+ .array(tool.schema.string())
2667
+ .optional()
2668
+ .describe("Files that exemplify this pattern"),
2669
+ create_skill: tool.schema
2670
+ .boolean()
2671
+ .optional()
2672
+ .describe("Create a skill from this learning (default: false, just document)"),
2673
+ skill_name: tool.schema
2674
+ .string()
2675
+ .regex(/^[a-z0-9-]+$/)
2676
+ .max(64)
2677
+ .optional()
2678
+ .describe("Skill name if creating (required if create_skill=true)"),
2679
+ skill_tags: tool.schema
2680
+ .array(tool.schema.string())
2681
+ .optional()
2682
+ .describe("Tags for the skill if creating"),
2683
+ },
2684
+ async execute(args) {
2685
+ // Format the learning as structured documentation
2686
+ const learning = {
2687
+ summary: args.summary,
2688
+ type: args.pattern_type,
2689
+ details: args.details,
2690
+ example: args.example,
2691
+ when_to_use: args.when_to_use,
2692
+ files_context: args.files_context,
2693
+ recorded_at: new Date().toISOString(),
2694
+ };
2695
+
2696
+ // If creating a skill, generate and create it
2697
+ if (args.create_skill) {
2698
+ if (!args.skill_name) {
2699
+ return JSON.stringify(
2700
+ {
2701
+ success: false,
2702
+ error: "skill_name is required when create_skill=true",
2703
+ learning: learning,
2704
+ },
2705
+ null,
2706
+ 2,
2707
+ );
2708
+ }
2709
+
2710
+ // Build skill body from learning
2711
+ const skillBody = `# ${args.summary}
2712
+
2713
+ ## When to Use
2714
+ ${args.when_to_use}
2715
+
2716
+ ## ${args.pattern_type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}
2717
+
2718
+ ${args.details}
2719
+
2720
+ ${args.example ? `## Example\n\n\`\`\`\n${args.example}\n\`\`\`\n` : ""}
2721
+ ${args.files_context && args.files_context.length > 0 ? `## Reference Files\n\n${args.files_context.map((f) => `- \`${f}\``).join("\n")}\n` : ""}
2722
+
2723
+ ---
2724
+ *Learned from swarm execution on ${new Date().toISOString().split("T")[0]}*`;
2725
+
2726
+ // Import skills_create functionality
2727
+ const { getSkill, invalidateSkillsCache } = await import("./skills");
2728
+ const { mkdir, writeFile } = await import("fs/promises");
2729
+ const { join } = await import("path");
2730
+
2731
+ // Check if skill exists
2732
+ const existing = await getSkill(args.skill_name);
2733
+ if (existing) {
2734
+ return JSON.stringify(
2735
+ {
2736
+ success: false,
2737
+ error: `Skill '${args.skill_name}' already exists`,
2738
+ existing_path: existing.path,
2739
+ learning: learning,
2740
+ suggestion: "Use skills_update to add to existing skill, or choose a different name",
2741
+ },
2742
+ null,
2743
+ 2,
2744
+ );
2745
+ }
2746
+
2747
+ // Create skill directory and file
2748
+ const skillDir = join(process.cwd(), ".opencode", "skills", args.skill_name);
2749
+ const skillPath = join(skillDir, "SKILL.md");
2750
+
2751
+ const frontmatter = [
2752
+ "---",
2753
+ `name: ${args.skill_name}`,
2754
+ `description: ${args.when_to_use.slice(0, 200)}${args.when_to_use.length > 200 ? "..." : ""}`,
2755
+ "tags:",
2756
+ ` - ${args.pattern_type}`,
2757
+ ` - learned`,
2758
+ ...(args.skill_tags || []).map((t) => ` - ${t}`),
2759
+ "---",
2760
+ ].join("\n");
2761
+
2762
+ try {
2763
+ await mkdir(skillDir, { recursive: true });
2764
+ await writeFile(skillPath, `${frontmatter}\n\n${skillBody}`, "utf-8");
2765
+ invalidateSkillsCache();
2766
+
2767
+ return JSON.stringify(
2768
+ {
2769
+ success: true,
2770
+ skill_created: true,
2771
+ skill: {
2772
+ name: args.skill_name,
2773
+ path: skillPath,
2774
+ type: args.pattern_type,
2775
+ },
2776
+ learning: learning,
2777
+ message: `Created skill '${args.skill_name}' from learned pattern. Future agents can discover it with skills_list.`,
2778
+ },
2779
+ null,
2780
+ 2,
2781
+ );
2782
+ } catch (error) {
2783
+ return JSON.stringify(
2784
+ {
2785
+ success: false,
2786
+ error: `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`,
2787
+ learning: learning,
2788
+ },
2789
+ null,
2790
+ 2,
2791
+ );
2792
+ }
2793
+ }
2794
+
2795
+ // Just document the learning without creating a skill
2796
+ return JSON.stringify(
2797
+ {
2798
+ success: true,
2799
+ skill_created: false,
2800
+ learning: learning,
2801
+ message: "Learning documented. Use create_skill=true to persist as a skill for future agents.",
2802
+ suggested_skill_name: args.skill_name ||
2803
+ args.summary
2804
+ .toLowerCase()
2805
+ .replace(/[^a-z0-9\s-]/g, "")
2806
+ .replace(/\s+/g, "-")
2807
+ .slice(0, 64),
2808
+ },
2809
+ null,
2810
+ 2,
2811
+ );
2812
+ },
2813
+ });
2814
+
2557
2815
  // ============================================================================
2558
2816
  // Error Accumulator
2559
2817
  // ============================================================================
@@ -2695,12 +2953,17 @@ export const swarm_resolve_error = tool({
2695
2953
  /**
2696
2954
  * Initialize swarm and check tool availability
2697
2955
  *
2698
- * Call this at the start of a swarm session to see what tools are available
2699
- * and what features will be degraded.
2956
+ * Call this at the start of a swarm session to see what tools are available,
2957
+ * what skills exist in the project, and what features will be degraded.
2958
+ *
2959
+ * Skills are automatically discovered from:
2960
+ * - .opencode/skills/
2961
+ * - .claude/skills/
2962
+ * - skills/
2700
2963
  */
2701
2964
  export const swarm_init = tool({
2702
2965
  description:
2703
- "Initialize swarm session and check tool availability. Call at swarm start to see what features are available.",
2966
+ "Initialize swarm session: discovers available skills, checks tool availability. ALWAYS call at swarm start.",
2704
2967
  args: {
2705
2968
  project_path: tool.schema
2706
2969
  .string()
@@ -2749,6 +3012,26 @@ export const swarm_init = tool({
2749
3012
  degradedFeatures.push("persistent learning (using in-memory fallback)");
2750
3013
  }
2751
3014
 
3015
+ // Discover available skills
3016
+ const availableSkills = await listSkills();
3017
+ const skillsInfo = {
3018
+ count: availableSkills.length,
3019
+ available: availableSkills.length > 0,
3020
+ skills: availableSkills.map((s) => ({
3021
+ name: s.name,
3022
+ description: s.description,
3023
+ hasScripts: s.hasScripts,
3024
+ })),
3025
+ };
3026
+
3027
+ // Add skills guidance if available
3028
+ let skillsGuidance: string | undefined;
3029
+ if (availableSkills.length > 0) {
3030
+ skillsGuidance = `Found ${availableSkills.length} skill(s). Use skills_list to see details, skills_use to activate.`;
3031
+ } else {
3032
+ skillsGuidance = "No skills found. Add skills to .opencode/skills/ or .claude/skills/ for specialized guidance.";
3033
+ }
3034
+
2752
3035
  return JSON.stringify(
2753
3036
  {
2754
3037
  ready: true,
@@ -2761,10 +3044,12 @@ export const swarm_init = tool({
2761
3044
  },
2762
3045
  ]),
2763
3046
  ),
3047
+ skills: skillsInfo,
2764
3048
  warnings: warnings.length > 0 ? warnings : undefined,
2765
3049
  degraded_features:
2766
3050
  degradedFeatures.length > 0 ? degradedFeatures : undefined,
2767
3051
  recommendations: {
3052
+ skills: skillsGuidance,
2768
3053
  beads: beadsAvailable
2769
3054
  ? "✓ Use beads for all task tracking"
2770
3055
  : "Install beads: npm i -g @joelhooks/beads",
@@ -2794,6 +3079,7 @@ export const swarmTools = {
2794
3079
  swarm_progress: swarm_progress,
2795
3080
  swarm_broadcast: swarm_broadcast,
2796
3081
  swarm_complete: swarm_complete,
3082
+ swarm_learn: swarm_learn,
2797
3083
  swarm_record_outcome: swarm_record_outcome,
2798
3084
  swarm_subtask_prompt: swarm_subtask_prompt,
2799
3085
  swarm_spawn_subtask: swarm_spawn_subtask,