opencode-swarm-plugin 0.5.0 → 0.6.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/src/swarm.ts CHANGED
@@ -28,11 +28,13 @@ import {
28
28
  import { mcpCall } from "./agent-mail";
29
29
  import {
30
30
  OutcomeSignalsSchema,
31
+ DecompositionStrategySchema,
31
32
  scoreImplicitFeedback,
32
33
  outcomeToFeedback,
33
34
  type OutcomeSignals,
34
35
  type ScoredOutcome,
35
36
  type FeedbackEvent,
37
+ type DecompositionStrategy as LearningDecompositionStrategy,
36
38
  DEFAULT_LEARNING_CONFIG,
37
39
  } from "./learning";
38
40
  import {
@@ -193,6 +195,244 @@ export function detectInstructionConflicts(
193
195
  return conflicts;
194
196
  }
195
197
 
198
+ // ============================================================================
199
+ // Strategy Definitions
200
+ // ============================================================================
201
+
202
+ /**
203
+ * Decomposition strategy types
204
+ */
205
+ export type DecompositionStrategy =
206
+ | "file-based"
207
+ | "feature-based"
208
+ | "risk-based"
209
+ | "auto";
210
+
211
+ /**
212
+ * Strategy definition with keywords, guidelines, and anti-patterns
213
+ */
214
+ export interface StrategyDefinition {
215
+ name: DecompositionStrategy;
216
+ description: string;
217
+ keywords: string[];
218
+ guidelines: string[];
219
+ antiPatterns: string[];
220
+ examples: string[];
221
+ }
222
+
223
+ /**
224
+ * Strategy definitions for task decomposition
225
+ */
226
+ export const STRATEGIES: Record<
227
+ Exclude<DecompositionStrategy, "auto">,
228
+ StrategyDefinition
229
+ > = {
230
+ "file-based": {
231
+ name: "file-based",
232
+ description:
233
+ "Group by file type or directory. Best for refactoring, migrations, and pattern changes across codebase.",
234
+ keywords: [
235
+ "refactor",
236
+ "migrate",
237
+ "update all",
238
+ "rename",
239
+ "replace",
240
+ "convert",
241
+ "upgrade",
242
+ "deprecate",
243
+ "remove",
244
+ "cleanup",
245
+ "lint",
246
+ "format",
247
+ ],
248
+ guidelines: [
249
+ "Group files by directory or type (e.g., all components, all tests)",
250
+ "Minimize cross-directory dependencies within a subtask",
251
+ "Handle shared types/utilities first if they change",
252
+ "Each subtask should be a complete transformation of its file set",
253
+ "Consider import/export relationships when grouping",
254
+ ],
255
+ antiPatterns: [
256
+ "Don't split tightly coupled files across subtasks",
257
+ "Don't group files that have no relationship",
258
+ "Don't forget to update imports when moving/renaming",
259
+ ],
260
+ examples: [
261
+ "Migrate all components to new API → split by component directory",
262
+ "Rename userId to accountId → split by module (types first, then consumers)",
263
+ "Update all tests to use new matcher → split by test directory",
264
+ ],
265
+ },
266
+ "feature-based": {
267
+ name: "feature-based",
268
+ description:
269
+ "Vertical slices with UI + API + data. Best for new features and adding functionality.",
270
+ keywords: [
271
+ "add",
272
+ "implement",
273
+ "build",
274
+ "create",
275
+ "feature",
276
+ "new",
277
+ "integrate",
278
+ "connect",
279
+ "enable",
280
+ "support",
281
+ ],
282
+ guidelines: [
283
+ "Each subtask is a complete vertical slice (UI + logic + data)",
284
+ "Start with data layer/types, then logic, then UI",
285
+ "Keep related components together (form + validation + submission)",
286
+ "Separate concerns that can be developed independently",
287
+ "Consider user-facing features as natural boundaries",
288
+ ],
289
+ antiPatterns: [
290
+ "Don't split a single feature across multiple subtasks",
291
+ "Don't create subtasks that can't be tested independently",
292
+ "Don't forget integration points between features",
293
+ ],
294
+ examples: [
295
+ "Add user auth → [OAuth setup, Session management, Protected routes]",
296
+ "Build dashboard → [Data fetching, Chart components, Layout/navigation]",
297
+ "Add search → [Search API, Search UI, Results display]",
298
+ ],
299
+ },
300
+ "risk-based": {
301
+ name: "risk-based",
302
+ description:
303
+ "Isolate high-risk changes, add tests first. Best for bug fixes, security issues, and critical changes.",
304
+ keywords: [
305
+ "fix",
306
+ "bug",
307
+ "security",
308
+ "vulnerability",
309
+ "critical",
310
+ "urgent",
311
+ "hotfix",
312
+ "patch",
313
+ "audit",
314
+ "review",
315
+ "investigate",
316
+ ],
317
+ guidelines: [
318
+ "Write tests FIRST to capture expected behavior",
319
+ "Isolate the risky change to minimize blast radius",
320
+ "Add monitoring/logging around the change",
321
+ "Create rollback plan as part of the task",
322
+ "Audit similar code for the same issue",
323
+ ],
324
+ antiPatterns: [
325
+ "Don't make multiple risky changes in one subtask",
326
+ "Don't skip tests for 'simple' fixes",
327
+ "Don't forget to check for similar issues elsewhere",
328
+ ],
329
+ examples: [
330
+ "Fix auth bypass → [Add regression test, Fix vulnerability, Audit similar endpoints]",
331
+ "Fix race condition → [Add test reproducing issue, Implement fix, Add concurrency tests]",
332
+ "Security audit → [Scan for vulnerabilities, Fix critical issues, Document remaining risks]",
333
+ ],
334
+ },
335
+ };
336
+
337
+ /**
338
+ * Analyze task description and select best decomposition strategy
339
+ *
340
+ * @param task - Task description
341
+ * @returns Selected strategy with reasoning
342
+ */
343
+ export function selectStrategy(task: string): {
344
+ strategy: Exclude<DecompositionStrategy, "auto">;
345
+ confidence: number;
346
+ reasoning: string;
347
+ alternatives: Array<{
348
+ strategy: Exclude<DecompositionStrategy, "auto">;
349
+ score: number;
350
+ }>;
351
+ } {
352
+ const taskLower = task.toLowerCase();
353
+
354
+ // Score each strategy based on keyword matches
355
+ const scores: Record<Exclude<DecompositionStrategy, "auto">, number> = {
356
+ "file-based": 0,
357
+ "feature-based": 0,
358
+ "risk-based": 0,
359
+ };
360
+
361
+ for (const [strategyName, definition] of Object.entries(STRATEGIES)) {
362
+ const name = strategyName as Exclude<DecompositionStrategy, "auto">;
363
+ for (const keyword of definition.keywords) {
364
+ if (taskLower.includes(keyword)) {
365
+ scores[name] += 1;
366
+ }
367
+ }
368
+ }
369
+
370
+ // Find the winner
371
+ const entries = Object.entries(scores) as Array<
372
+ [Exclude<DecompositionStrategy, "auto">, number]
373
+ >;
374
+ entries.sort((a, b) => b[1] - a[1]);
375
+
376
+ const [winner, winnerScore] = entries[0];
377
+ const [runnerUp, runnerUpScore] = entries[1] || [null, 0];
378
+
379
+ // Calculate confidence based on margin
380
+ const totalScore = entries.reduce((sum, [, score]) => sum + score, 0);
381
+ const confidence =
382
+ totalScore > 0
383
+ ? Math.min(0.95, 0.5 + (winnerScore - runnerUpScore) / totalScore)
384
+ : 0.5; // Default to 50% if no keywords matched
385
+
386
+ // Build reasoning
387
+ let reasoning: string;
388
+ if (winnerScore === 0) {
389
+ reasoning = `No strong keyword signals. Defaulting to feature-based as it's most versatile.`;
390
+ } else {
391
+ const matchedKeywords = STRATEGIES[winner].keywords.filter((k) =>
392
+ taskLower.includes(k),
393
+ );
394
+ reasoning = `Matched keywords: ${matchedKeywords.join(", ")}. ${STRATEGIES[winner].description}`;
395
+ }
396
+
397
+ // If no keywords matched, default to feature-based
398
+ const finalStrategy = winnerScore === 0 ? "feature-based" : winner;
399
+
400
+ return {
401
+ strategy: finalStrategy,
402
+ confidence,
403
+ reasoning,
404
+ alternatives: entries
405
+ .filter(([s]) => s !== finalStrategy)
406
+ .map(([strategy, score]) => ({ strategy, score })),
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Format strategy-specific guidelines for the decomposition prompt
412
+ */
413
+ export function formatStrategyGuidelines(
414
+ strategy: Exclude<DecompositionStrategy, "auto">,
415
+ ): string {
416
+ const def = STRATEGIES[strategy];
417
+
418
+ const guidelines = def.guidelines.map((g) => `- ${g}`).join("\n");
419
+ const antiPatterns = def.antiPatterns.map((a) => `- ${a}`).join("\n");
420
+ const examples = def.examples.map((e) => `- ${e}`).join("\n");
421
+
422
+ return `## Strategy: ${strategy}
423
+
424
+ ${def.description}
425
+
426
+ ### Guidelines
427
+ ${guidelines}
428
+
429
+ ### Anti-Patterns (Avoid These)
430
+ ${antiPatterns}
431
+
432
+ ### Examples
433
+ ${examples}`;
434
+ }
435
+
196
436
  // ============================================================================
197
437
  // Prompt Templates
198
438
  // ============================================================================
@@ -744,6 +984,227 @@ function formatCassHistoryForPrompt(history: CassSearchResult): string {
744
984
  // Tool Definitions
745
985
  // ============================================================================
746
986
 
987
+ /**
988
+ * Select the best decomposition strategy for a task
989
+ *
990
+ * Analyzes task description and recommends a strategy with reasoning.
991
+ * Use this before swarm_plan_prompt to understand the recommended approach.
992
+ */
993
+ export const swarm_select_strategy = tool({
994
+ description:
995
+ "Analyze task and recommend decomposition strategy (file-based, feature-based, or risk-based)",
996
+ args: {
997
+ task: tool.schema.string().min(1).describe("Task description to analyze"),
998
+ codebase_context: tool.schema
999
+ .string()
1000
+ .optional()
1001
+ .describe("Optional codebase context (file structure, tech stack, etc.)"),
1002
+ },
1003
+ async execute(args) {
1004
+ const result = selectStrategy(args.task);
1005
+
1006
+ // Enhance reasoning with codebase context if provided
1007
+ let enhancedReasoning = result.reasoning;
1008
+ if (args.codebase_context) {
1009
+ enhancedReasoning += `\n\nCodebase context considered: ${args.codebase_context.slice(0, 200)}...`;
1010
+ }
1011
+
1012
+ return JSON.stringify(
1013
+ {
1014
+ strategy: result.strategy,
1015
+ confidence: Math.round(result.confidence * 100) / 100,
1016
+ reasoning: enhancedReasoning,
1017
+ description: STRATEGIES[result.strategy].description,
1018
+ guidelines: STRATEGIES[result.strategy].guidelines,
1019
+ anti_patterns: STRATEGIES[result.strategy].antiPatterns,
1020
+ alternatives: result.alternatives.map((alt) => ({
1021
+ strategy: alt.strategy,
1022
+ description: STRATEGIES[alt.strategy].description,
1023
+ score: alt.score,
1024
+ })),
1025
+ },
1026
+ null,
1027
+ 2,
1028
+ );
1029
+ },
1030
+ });
1031
+
1032
+ /**
1033
+ * Strategy-specific decomposition prompt template
1034
+ */
1035
+ const STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
1036
+
1037
+ ## Task
1038
+ {task}
1039
+
1040
+ {strategy_guidelines}
1041
+
1042
+ {context_section}
1043
+
1044
+ {cass_history}
1045
+
1046
+ ## MANDATORY: Beads Issue Tracking
1047
+
1048
+ **Every subtask MUST become a bead.** This is non-negotiable.
1049
+
1050
+ After decomposition, the coordinator will:
1051
+ 1. Create an epic bead for the overall task
1052
+ 2. Create child beads for each subtask
1053
+ 3. Track progress through bead status updates
1054
+ 4. Close beads with summaries when complete
1055
+
1056
+ Agents MUST update their bead status as they work. No silent progress.
1057
+
1058
+ ## Requirements
1059
+
1060
+ 1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
1061
+ 2. **Assign files** - each subtask must specify which files it will modify
1062
+ 3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
1063
+ 4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
1064
+ 5. **Estimate complexity** - 1 (trivial) to 5 (complex)
1065
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
1066
+
1067
+ ## Response Format
1068
+
1069
+ Respond with a JSON object matching this schema:
1070
+
1071
+ \`\`\`typescript
1072
+ {
1073
+ epic: {
1074
+ title: string, // Epic title for the beads tracker
1075
+ description?: string // Brief description of the overall goal
1076
+ },
1077
+ subtasks: [
1078
+ {
1079
+ title: string, // What this subtask accomplishes
1080
+ description?: string, // Detailed instructions for the agent
1081
+ files: string[], // Files this subtask will modify (globs allowed)
1082
+ dependencies: number[], // Indices of subtasks this depends on (0-indexed)
1083
+ estimated_complexity: 1-5 // Effort estimate
1084
+ },
1085
+ // ... more subtasks
1086
+ ]
1087
+ }
1088
+ \`\`\`
1089
+
1090
+ Now decompose the task:`;
1091
+
1092
+ /**
1093
+ * Generate a strategy-specific planning prompt
1094
+ *
1095
+ * Higher-level than swarm_decompose - includes strategy selection and guidelines.
1096
+ * Use this when you want the full planning experience with strategy-specific advice.
1097
+ */
1098
+ export const swarm_plan_prompt = tool({
1099
+ description:
1100
+ "Generate strategy-specific decomposition prompt. Auto-selects strategy or uses provided one. Queries CASS for similar tasks.",
1101
+ args: {
1102
+ task: tool.schema.string().min(1).describe("Task description to decompose"),
1103
+ strategy: tool.schema
1104
+ .enum(["file-based", "feature-based", "risk-based", "auto"])
1105
+ .optional()
1106
+ .describe("Decomposition strategy (default: auto-detect)"),
1107
+ max_subtasks: tool.schema
1108
+ .number()
1109
+ .int()
1110
+ .min(2)
1111
+ .max(10)
1112
+ .default(5)
1113
+ .describe("Maximum number of subtasks (default: 5)"),
1114
+ context: tool.schema
1115
+ .string()
1116
+ .optional()
1117
+ .describe("Additional context (codebase info, constraints, etc.)"),
1118
+ query_cass: tool.schema
1119
+ .boolean()
1120
+ .optional()
1121
+ .describe("Query CASS for similar past tasks (default: true)"),
1122
+ cass_limit: tool.schema
1123
+ .number()
1124
+ .int()
1125
+ .min(1)
1126
+ .max(10)
1127
+ .optional()
1128
+ .describe("Max CASS results to include (default: 3)"),
1129
+ },
1130
+ async execute(args) {
1131
+ // Select strategy
1132
+ let selectedStrategy: Exclude<DecompositionStrategy, "auto">;
1133
+ let strategyReasoning: string;
1134
+
1135
+ if (args.strategy && args.strategy !== "auto") {
1136
+ selectedStrategy = args.strategy;
1137
+ strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
1138
+ } else {
1139
+ const selection = selectStrategy(args.task);
1140
+ selectedStrategy = selection.strategy;
1141
+ strategyReasoning = selection.reasoning;
1142
+ }
1143
+
1144
+ // Query CASS for similar past tasks
1145
+ let cassContext = "";
1146
+ let cassResult: CassSearchResult | null = null;
1147
+
1148
+ if (args.query_cass !== false) {
1149
+ cassResult = await queryCassHistory(args.task, args.cass_limit ?? 3);
1150
+ if (cassResult && cassResult.results.length > 0) {
1151
+ cassContext = formatCassHistoryForPrompt(cassResult);
1152
+ }
1153
+ }
1154
+
1155
+ // Format strategy guidelines
1156
+ const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
1157
+
1158
+ // Combine user context
1159
+ const contextSection = args.context
1160
+ ? `## Additional Context\n${args.context}`
1161
+ : "## Additional Context\n(none provided)";
1162
+
1163
+ // Build the prompt
1164
+ const prompt = STRATEGY_DECOMPOSITION_PROMPT.replace("{task}", args.task)
1165
+ .replace("{strategy_guidelines}", strategyGuidelines)
1166
+ .replace("{context_section}", contextSection)
1167
+ .replace("{cass_history}", cassContext || "")
1168
+ .replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
1169
+
1170
+ return JSON.stringify(
1171
+ {
1172
+ prompt,
1173
+ strategy: {
1174
+ selected: selectedStrategy,
1175
+ reasoning: strategyReasoning,
1176
+ guidelines: STRATEGIES[selectedStrategy].guidelines,
1177
+ anti_patterns: STRATEGIES[selectedStrategy].antiPatterns,
1178
+ },
1179
+ expected_schema: "BeadTree",
1180
+ schema_hint: {
1181
+ epic: { title: "string", description: "string?" },
1182
+ subtasks: [
1183
+ {
1184
+ title: "string",
1185
+ description: "string?",
1186
+ files: "string[]",
1187
+ dependencies: "number[]",
1188
+ estimated_complexity: "1-5",
1189
+ },
1190
+ ],
1191
+ },
1192
+ validation_note:
1193
+ "Parse agent response as JSON and validate with swarm_validate_decomposition",
1194
+ cass_history: cassResult
1195
+ ? {
1196
+ queried: true,
1197
+ results_found: cassResult.results.length,
1198
+ included_in_context: cassResult.results.length > 0,
1199
+ }
1200
+ : { queried: false, reason: "disabled or unavailable" },
1201
+ },
1202
+ null,
1203
+ 2,
1204
+ );
1205
+ },
1206
+ });
1207
+
747
1208
  /**
748
1209
  * Decompose a task into a bead tree
749
1210
  *
@@ -1367,6 +1828,9 @@ export const swarm_complete = tool({
1367
1828
  * decomposition quality over time. This data feeds into criterion
1368
1829
  * weight calculations.
1369
1830
  *
1831
+ * Strategy tracking enables learning about which decomposition strategies
1832
+ * work best for different task types.
1833
+ *
1370
1834
  * @see src/learning.ts for scoring logic
1371
1835
  */
1372
1836
  export const swarm_record_outcome = tool({
@@ -1402,6 +1866,10 @@ export const swarm_record_outcome = tool({
1402
1866
  .describe(
1403
1867
  "Criteria to generate feedback for (default: all default criteria)",
1404
1868
  ),
1869
+ strategy: tool.schema
1870
+ .enum(["file-based", "feature-based", "risk-based"])
1871
+ .optional()
1872
+ .describe("Decomposition strategy used for this task"),
1405
1873
  },
1406
1874
  async execute(args) {
1407
1875
  // Build outcome signals
@@ -1413,6 +1881,7 @@ export const swarm_record_outcome = tool({
1413
1881
  success: args.success,
1414
1882
  files_touched: args.files_touched ?? [],
1415
1883
  timestamp: new Date().toISOString(),
1884
+ strategy: args.strategy as LearningDecompositionStrategy | undefined,
1416
1885
  };
1417
1886
 
1418
1887
  // Validate signals
@@ -1431,9 +1900,15 @@ export const swarm_record_outcome = tool({
1431
1900
  "patterns",
1432
1901
  "readable",
1433
1902
  ];
1434
- const feedbackEvents: FeedbackEvent[] = criteriaToScore.map((criterion) =>
1435
- outcomeToFeedback(scored, criterion),
1436
- );
1903
+ const feedbackEvents: FeedbackEvent[] = criteriaToScore.map((criterion) => {
1904
+ const event = outcomeToFeedback(scored, criterion);
1905
+ // Include strategy in feedback context for future analysis
1906
+ if (args.strategy) {
1907
+ event.context =
1908
+ `${event.context || ""} [strategy: ${args.strategy}]`.trim();
1909
+ }
1910
+ return event;
1911
+ });
1437
1912
 
1438
1913
  return JSON.stringify(
1439
1914
  {
@@ -1453,6 +1928,7 @@ export const swarm_record_outcome = tool({
1453
1928
  error_count: args.error_count ?? 0,
1454
1929
  retry_count: args.retry_count ?? 0,
1455
1930
  success: args.success,
1931
+ strategy: args.strategy,
1456
1932
  },
1457
1933
  note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights.",
1458
1934
  },
@@ -1827,6 +2303,8 @@ export const swarm_init = tool({
1827
2303
 
1828
2304
  export const swarmTools = {
1829
2305
  swarm_init: swarm_init,
2306
+ swarm_select_strategy: swarm_select_strategy,
2307
+ swarm_plan_prompt: swarm_plan_prompt,
1830
2308
  swarm_decompose: swarm_decompose,
1831
2309
  swarm_validate_decomposition: swarm_validate_decomposition,
1832
2310
  swarm_status: swarm_status,