opencode-swarm-plugin 0.6.0 → 0.6.3

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/README.md CHANGED
@@ -20,49 +20,151 @@ This plugin provides intelligent, self-improving tools for multi-agent workflows
20
20
  - **Graceful degradation** - Works with whatever tools are available, degrades features when tools missing
21
21
  - **Swarm discipline** - Enforces beads tracking, aggressive planning, and agent communication
22
22
 
23
- ## Installation
23
+ ## Quick Start
24
+
25
+ ### 1. Install Dependencies
26
+
27
+ ```bash
28
+ # OpenCode (plugin host)
29
+ brew install sst/tap/opencode
30
+
31
+ # Beads CLI (issue tracking)
32
+ npm install -g @joelhooks/beads
33
+ ```
34
+
35
+ ### 2. Install Plugin
24
36
 
25
37
  ```bash
26
38
  npm install opencode-swarm-plugin
27
- # or
28
- bun add opencode-swarm-plugin
29
- # or
30
- pnpm add opencode-swarm-plugin
31
39
  ```
32
40
 
33
- Copy the plugin to your OpenCode plugins directory:
41
+ ### 3. Create Plugin Wrapper
42
+
43
+ Create a file at `~/.config/opencode/plugins/swarm.ts`:
44
+
45
+ ```ts
46
+ import { SwarmPlugin } from "opencode-swarm-plugin";
47
+ export default SwarmPlugin;
48
+ ```
49
+
50
+ That's it! OpenCode will load the plugin automatically.
51
+
52
+ ### 4. Copy Examples (Recommended)
53
+
54
+ ```bash
55
+ # Create directories
56
+ mkdir -p ~/.config/opencode/commands ~/.config/opencode/agents
57
+
58
+ # Copy /swarm command
59
+ cp node_modules/opencode-swarm-plugin/examples/commands/swarm.md ~/.config/opencode/commands/
60
+
61
+ # Copy @swarm-planner agent
62
+ cp node_modules/opencode-swarm-plugin/examples/agents/swarm-planner.md ~/.config/opencode/agents/
63
+ ```
64
+
65
+ ### 5. Initialize Beads in Your Project
34
66
 
35
67
  ```bash
36
- cp node_modules/opencode-swarm-plugin/dist/plugin.js ~/.config/opencode/plugin/swarm.js
68
+ cd your-project
69
+ bd init
37
70
  ```
38
71
 
39
- Plugins are automatically loaded from `~/.config/opencode/plugin/` - no config file changes needed.
72
+ ### Alternative: Direct Copy (No Wrapper)
40
73
 
41
- > **Note:** The package has two entry points:
42
- >
43
- > - `dist/index.js` - Full library exports (schemas, errors, utilities, learning modules)
44
- > - `dist/plugin.js` - Plugin entry point that only exports the `plugin` function for OpenCode
74
+ If you prefer not to use the wrapper pattern:
75
+
76
+ ```bash
77
+ mkdir -p ~/.config/opencode/plugins
78
+ cp node_modules/opencode-swarm-plugin/dist/plugin.js ~/.config/opencode/plugins/swarm.js
79
+ ```
80
+
81
+ ## Dependencies
82
+
83
+ ### Required
84
+
85
+ | Dependency | Purpose | Install |
86
+ | ----------------------------------------------- | ------------------------- | --------------------------------- |
87
+ | [OpenCode](https://opencode.ai) | Plugin host | `brew install sst/tap/opencode` |
88
+ | [Beads CLI](https://github.com/joelhooks/beads) | Git-backed issue tracking | `npm install -g @joelhooks/beads` |
89
+
90
+ ### Optional (Graceful Degradation)
45
91
 
46
- ## Prerequisites
92
+ The plugin works without these, but with reduced functionality:
47
93
 
48
- | Requirement | Purpose |
49
- | ---------------- | ------------------------------------------- |
50
- | OpenCode 1.0+ | Plugin host |
51
- | Agent Mail MCP | Multi-agent coordination (`localhost:8765`) |
52
- | Beads CLI (`bd`) | Git-backed issue tracking |
94
+ | Dependency | Purpose | Without It |
95
+ | ----------------------------------------------------- | ------------------------ | ---------------------------------------- |
96
+ | [Agent Mail](https://github.com/joelhooks/agent-mail) | Multi-agent coordination | No file reservations, no agent messaging |
97
+ | [Redis](https://redis.io) | Rate limiting | Falls back to SQLite |
98
+ | [CASS](https://github.com/Dicklesworthstone/cass) | Historical context | No "similar past tasks" in decomposition |
99
+ | [UBS](https://github.com/joelhooks/ubs) | Bug scanning | No pre-completion validation |
53
100
 
54
- ### Verify Agent Mail is running
101
+ ### Verify Installation
55
102
 
56
103
  ```bash
104
+ # Check OpenCode
105
+ opencode --version
106
+
107
+ # Check Beads
108
+ bd --version
109
+
110
+ # Check Agent Mail (if using multi-agent)
57
111
  curl http://127.0.0.1:8765/health/liveness
112
+
113
+ # Check Redis (optional)
114
+ redis-cli ping
58
115
  ```
59
116
 
60
- ### Verify beads is installed
117
+ ## Installation Details
118
+
119
+ ### From npm
61
120
 
62
121
  ```bash
63
- bd --version
122
+ npm install opencode-swarm-plugin
123
+ # or
124
+ bun add opencode-swarm-plugin
125
+ # or
126
+ pnpm add opencode-swarm-plugin
127
+ ```
128
+
129
+ ### Plugin Setup (Wrapper Pattern)
130
+
131
+ Create `~/.config/opencode/plugins/swarm.ts`:
132
+
133
+ ```ts
134
+ import { SwarmPlugin } from "opencode-swarm-plugin";
135
+ export default SwarmPlugin;
136
+ ```
137
+
138
+ OpenCode runs on Bun and loads TypeScript directly - no build step needed.
139
+
140
+ ### Plugin Setup (Direct Copy)
141
+
142
+ Alternatively, copy the pre-built bundle:
143
+
144
+ ```bash
145
+ mkdir -p ~/.config/opencode/plugins
146
+ cp node_modules/opencode-swarm-plugin/dist/plugin.js ~/.config/opencode/plugins/swarm.js
147
+ ```
148
+
149
+ ### Example Files
150
+
151
+ Copy the `/swarm` command and `@swarm-planner` agent:
152
+
153
+ ```bash
154
+ # Command
155
+ mkdir -p ~/.config/opencode/commands
156
+ cp node_modules/opencode-swarm-plugin/examples/commands/swarm.md ~/.config/opencode/commands/
157
+
158
+ # Agent
159
+ mkdir -p ~/.config/opencode/agents
160
+ cp node_modules/opencode-swarm-plugin/examples/agents/swarm-planner.md ~/.config/opencode/agents/
64
161
  ```
65
162
 
163
+ > **Note:** The package has two entry points:
164
+ >
165
+ > - `dist/index.js` - Full library exports (schemas, errors, utilities, learning modules)
166
+ > - `dist/plugin.js` - Plugin entry point that only exports the `plugin` function for OpenCode
167
+
66
168
  ## Tools Reference
67
169
 
68
170
  ### Beads Tools
package/dist/index.js CHANGED
@@ -23386,6 +23386,11 @@ var CriterionWeightSchema = exports_external.object({
23386
23386
  last_validated: exports_external.string().optional(),
23387
23387
  half_life_days: exports_external.number().positive().default(90)
23388
23388
  });
23389
+ var DecompositionStrategySchema = exports_external.enum([
23390
+ "file-based",
23391
+ "feature-based",
23392
+ "risk-based"
23393
+ ]);
23389
23394
  var OutcomeSignalsSchema = exports_external.object({
23390
23395
  bead_id: exports_external.string(),
23391
23396
  duration_ms: exports_external.number().int().min(0),
@@ -23393,7 +23398,8 @@ var OutcomeSignalsSchema = exports_external.object({
23393
23398
  retry_count: exports_external.number().int().min(0),
23394
23399
  success: exports_external.boolean(),
23395
23400
  files_touched: exports_external.array(exports_external.string()).default([]),
23396
- timestamp: exports_external.string()
23401
+ timestamp: exports_external.string(),
23402
+ strategy: DecompositionStrategySchema.optional()
23397
23403
  });
23398
23404
  var ScoredOutcomeSchema = exports_external.object({
23399
23405
  signals: OutcomeSignalsSchema,
@@ -23553,6 +23559,167 @@ function detectInstructionConflicts(subtasks) {
23553
23559
  }
23554
23560
  return conflicts;
23555
23561
  }
23562
+ var STRATEGIES = {
23563
+ "file-based": {
23564
+ name: "file-based",
23565
+ description: "Group by file type or directory. Best for refactoring, migrations, and pattern changes across codebase.",
23566
+ keywords: [
23567
+ "refactor",
23568
+ "migrate",
23569
+ "update all",
23570
+ "rename",
23571
+ "replace",
23572
+ "convert",
23573
+ "upgrade",
23574
+ "deprecate",
23575
+ "remove",
23576
+ "cleanup",
23577
+ "lint",
23578
+ "format"
23579
+ ],
23580
+ guidelines: [
23581
+ "Group files by directory or type (e.g., all components, all tests)",
23582
+ "Minimize cross-directory dependencies within a subtask",
23583
+ "Handle shared types/utilities first if they change",
23584
+ "Each subtask should be a complete transformation of its file set",
23585
+ "Consider import/export relationships when grouping"
23586
+ ],
23587
+ antiPatterns: [
23588
+ "Don't split tightly coupled files across subtasks",
23589
+ "Don't group files that have no relationship",
23590
+ "Don't forget to update imports when moving/renaming"
23591
+ ],
23592
+ examples: [
23593
+ "Migrate all components to new API \u2192 split by component directory",
23594
+ "Rename userId to accountId \u2192 split by module (types first, then consumers)",
23595
+ "Update all tests to use new matcher \u2192 split by test directory"
23596
+ ]
23597
+ },
23598
+ "feature-based": {
23599
+ name: "feature-based",
23600
+ description: "Vertical slices with UI + API + data. Best for new features and adding functionality.",
23601
+ keywords: [
23602
+ "add",
23603
+ "implement",
23604
+ "build",
23605
+ "create",
23606
+ "feature",
23607
+ "new",
23608
+ "integrate",
23609
+ "connect",
23610
+ "enable",
23611
+ "support"
23612
+ ],
23613
+ guidelines: [
23614
+ "Each subtask is a complete vertical slice (UI + logic + data)",
23615
+ "Start with data layer/types, then logic, then UI",
23616
+ "Keep related components together (form + validation + submission)",
23617
+ "Separate concerns that can be developed independently",
23618
+ "Consider user-facing features as natural boundaries"
23619
+ ],
23620
+ antiPatterns: [
23621
+ "Don't split a single feature across multiple subtasks",
23622
+ "Don't create subtasks that can't be tested independently",
23623
+ "Don't forget integration points between features"
23624
+ ],
23625
+ examples: [
23626
+ "Add user auth \u2192 [OAuth setup, Session management, Protected routes]",
23627
+ "Build dashboard \u2192 [Data fetching, Chart components, Layout/navigation]",
23628
+ "Add search \u2192 [Search API, Search UI, Results display]"
23629
+ ]
23630
+ },
23631
+ "risk-based": {
23632
+ name: "risk-based",
23633
+ description: "Isolate high-risk changes, add tests first. Best for bug fixes, security issues, and critical changes.",
23634
+ keywords: [
23635
+ "fix",
23636
+ "bug",
23637
+ "security",
23638
+ "vulnerability",
23639
+ "critical",
23640
+ "urgent",
23641
+ "hotfix",
23642
+ "patch",
23643
+ "audit",
23644
+ "review",
23645
+ "investigate"
23646
+ ],
23647
+ guidelines: [
23648
+ "Write tests FIRST to capture expected behavior",
23649
+ "Isolate the risky change to minimize blast radius",
23650
+ "Add monitoring/logging around the change",
23651
+ "Create rollback plan as part of the task",
23652
+ "Audit similar code for the same issue"
23653
+ ],
23654
+ antiPatterns: [
23655
+ "Don't make multiple risky changes in one subtask",
23656
+ "Don't skip tests for 'simple' fixes",
23657
+ "Don't forget to check for similar issues elsewhere"
23658
+ ],
23659
+ examples: [
23660
+ "Fix auth bypass \u2192 [Add regression test, Fix vulnerability, Audit similar endpoints]",
23661
+ "Fix race condition \u2192 [Add test reproducing issue, Implement fix, Add concurrency tests]",
23662
+ "Security audit \u2192 [Scan for vulnerabilities, Fix critical issues, Document remaining risks]"
23663
+ ]
23664
+ }
23665
+ };
23666
+ function selectStrategy(task) {
23667
+ const taskLower = task.toLowerCase();
23668
+ const scores = {
23669
+ "file-based": 0,
23670
+ "feature-based": 0,
23671
+ "risk-based": 0
23672
+ };
23673
+ for (const [strategyName, definition] of Object.entries(STRATEGIES)) {
23674
+ const name = strategyName;
23675
+ for (const keyword of definition.keywords) {
23676
+ if (taskLower.includes(keyword)) {
23677
+ scores[name] += 1;
23678
+ }
23679
+ }
23680
+ }
23681
+ const entries = Object.entries(scores);
23682
+ entries.sort((a, b) => b[1] - a[1]);
23683
+ const [winner, winnerScore] = entries[0];
23684
+ const [runnerUp, runnerUpScore] = entries[1] || [null, 0];
23685
+ const totalScore = entries.reduce((sum, [, score]) => sum + score, 0);
23686
+ const confidence = totalScore > 0 ? Math.min(0.95, 0.5 + (winnerScore - runnerUpScore) / totalScore) : 0.5;
23687
+ let reasoning;
23688
+ if (winnerScore === 0) {
23689
+ reasoning = `No strong keyword signals. Defaulting to feature-based as it's most versatile.`;
23690
+ } else {
23691
+ const matchedKeywords = STRATEGIES[winner].keywords.filter((k) => taskLower.includes(k));
23692
+ reasoning = `Matched keywords: ${matchedKeywords.join(", ")}. ${STRATEGIES[winner].description}`;
23693
+ }
23694
+ const finalStrategy = winnerScore === 0 ? "feature-based" : winner;
23695
+ return {
23696
+ strategy: finalStrategy,
23697
+ confidence,
23698
+ reasoning,
23699
+ alternatives: entries.filter(([s]) => s !== finalStrategy).map(([strategy, score]) => ({ strategy, score }))
23700
+ };
23701
+ }
23702
+ function formatStrategyGuidelines(strategy) {
23703
+ const def = STRATEGIES[strategy];
23704
+ const guidelines = def.guidelines.map((g) => `- ${g}`).join(`
23705
+ `);
23706
+ const antiPatterns = def.antiPatterns.map((a) => `- ${a}`).join(`
23707
+ `);
23708
+ const examples = def.examples.map((e) => `- ${e}`).join(`
23709
+ `);
23710
+ return `## Strategy: ${strategy}
23711
+
23712
+ ${def.description}
23713
+
23714
+ ### Guidelines
23715
+ ${guidelines}
23716
+
23717
+ ### Anti-Patterns (Avoid These)
23718
+ ${antiPatterns}
23719
+
23720
+ ### Examples
23721
+ ${examples}`;
23722
+ }
23556
23723
  var DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
23557
23724
 
23558
23725
  ## Task
@@ -23926,6 +24093,155 @@ function formatCassHistoryForPrompt(history) {
23926
24093
  return lines.join(`
23927
24094
  `);
23928
24095
  }
24096
+ var swarm_select_strategy = tool({
24097
+ description: "Analyze task and recommend decomposition strategy (file-based, feature-based, or risk-based)",
24098
+ args: {
24099
+ task: tool.schema.string().min(1).describe("Task description to analyze"),
24100
+ codebase_context: tool.schema.string().optional().describe("Optional codebase context (file structure, tech stack, etc.)")
24101
+ },
24102
+ async execute(args) {
24103
+ const result = selectStrategy(args.task);
24104
+ let enhancedReasoning = result.reasoning;
24105
+ if (args.codebase_context) {
24106
+ enhancedReasoning += `
24107
+
24108
+ Codebase context considered: ${args.codebase_context.slice(0, 200)}...`;
24109
+ }
24110
+ return JSON.stringify({
24111
+ strategy: result.strategy,
24112
+ confidence: Math.round(result.confidence * 100) / 100,
24113
+ reasoning: enhancedReasoning,
24114
+ description: STRATEGIES[result.strategy].description,
24115
+ guidelines: STRATEGIES[result.strategy].guidelines,
24116
+ anti_patterns: STRATEGIES[result.strategy].antiPatterns,
24117
+ alternatives: result.alternatives.map((alt) => ({
24118
+ strategy: alt.strategy,
24119
+ description: STRATEGIES[alt.strategy].description,
24120
+ score: alt.score
24121
+ }))
24122
+ }, null, 2);
24123
+ }
24124
+ });
24125
+ var STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
24126
+
24127
+ ## Task
24128
+ {task}
24129
+
24130
+ {strategy_guidelines}
24131
+
24132
+ {context_section}
24133
+
24134
+ {cass_history}
24135
+
24136
+ ## MANDATORY: Beads Issue Tracking
24137
+
24138
+ **Every subtask MUST become a bead.** This is non-negotiable.
24139
+
24140
+ After decomposition, the coordinator will:
24141
+ 1. Create an epic bead for the overall task
24142
+ 2. Create child beads for each subtask
24143
+ 3. Track progress through bead status updates
24144
+ 4. Close beads with summaries when complete
24145
+
24146
+ Agents MUST update their bead status as they work. No silent progress.
24147
+
24148
+ ## Requirements
24149
+
24150
+ 1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
24151
+ 2. **Assign files** - each subtask must specify which files it will modify
24152
+ 3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
24153
+ 4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
24154
+ 5. **Estimate complexity** - 1 (trivial) to 5 (complex)
24155
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
24156
+
24157
+ ## Response Format
24158
+
24159
+ Respond with a JSON object matching this schema:
24160
+
24161
+ \`\`\`typescript
24162
+ {
24163
+ epic: {
24164
+ title: string, // Epic title for the beads tracker
24165
+ description?: string // Brief description of the overall goal
24166
+ },
24167
+ subtasks: [
24168
+ {
24169
+ title: string, // What this subtask accomplishes
24170
+ description?: string, // Detailed instructions for the agent
24171
+ files: string[], // Files this subtask will modify (globs allowed)
24172
+ dependencies: number[], // Indices of subtasks this depends on (0-indexed)
24173
+ estimated_complexity: 1-5 // Effort estimate
24174
+ },
24175
+ // ... more subtasks
24176
+ ]
24177
+ }
24178
+ \`\`\`
24179
+
24180
+ Now decompose the task:`;
24181
+ var swarm_plan_prompt = tool({
24182
+ description: "Generate strategy-specific decomposition prompt. Auto-selects strategy or uses provided one. Queries CASS for similar tasks.",
24183
+ args: {
24184
+ task: tool.schema.string().min(1).describe("Task description to decompose"),
24185
+ strategy: tool.schema.enum(["file-based", "feature-based", "risk-based", "auto"]).optional().describe("Decomposition strategy (default: auto-detect)"),
24186
+ max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
24187
+ context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
24188
+ query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
24189
+ cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
24190
+ },
24191
+ async execute(args) {
24192
+ let selectedStrategy;
24193
+ let strategyReasoning;
24194
+ if (args.strategy && args.strategy !== "auto") {
24195
+ selectedStrategy = args.strategy;
24196
+ strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
24197
+ } else {
24198
+ const selection = selectStrategy(args.task);
24199
+ selectedStrategy = selection.strategy;
24200
+ strategyReasoning = selection.reasoning;
24201
+ }
24202
+ let cassContext = "";
24203
+ let cassResult = null;
24204
+ if (args.query_cass !== false) {
24205
+ cassResult = await queryCassHistory(args.task, args.cass_limit ?? 3);
24206
+ if (cassResult && cassResult.results.length > 0) {
24207
+ cassContext = formatCassHistoryForPrompt(cassResult);
24208
+ }
24209
+ }
24210
+ const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
24211
+ const contextSection = args.context ? `## Additional Context
24212
+ ${args.context}` : `## Additional Context
24213
+ (none provided)`;
24214
+ const prompt = STRATEGY_DECOMPOSITION_PROMPT.replace("{task}", args.task).replace("{strategy_guidelines}", strategyGuidelines).replace("{context_section}", contextSection).replace("{cass_history}", cassContext || "").replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
24215
+ return JSON.stringify({
24216
+ prompt,
24217
+ strategy: {
24218
+ selected: selectedStrategy,
24219
+ reasoning: strategyReasoning,
24220
+ guidelines: STRATEGIES[selectedStrategy].guidelines,
24221
+ anti_patterns: STRATEGIES[selectedStrategy].antiPatterns
24222
+ },
24223
+ expected_schema: "BeadTree",
24224
+ schema_hint: {
24225
+ epic: { title: "string", description: "string?" },
24226
+ subtasks: [
24227
+ {
24228
+ title: "string",
24229
+ description: "string?",
24230
+ files: "string[]",
24231
+ dependencies: "number[]",
24232
+ estimated_complexity: "1-5"
24233
+ }
24234
+ ]
24235
+ },
24236
+ validation_note: "Parse agent response as JSON and validate with swarm_validate_decomposition",
24237
+ cass_history: cassResult ? {
24238
+ queried: true,
24239
+ results_found: cassResult.results.length,
24240
+ included_in_context: cassResult.results.length > 0
24241
+ } : { queried: false, reason: "disabled or unavailable" }
24242
+ }, null, 2);
24243
+ }
24244
+ });
23929
24245
  var swarm_decompose = tool({
23930
24246
  description: "Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
23931
24247
  args: {
@@ -24285,7 +24601,8 @@ var swarm_record_outcome = tool({
24285
24601
  retry_count: tool.schema.number().int().min(0).default(0).describe("Number of retry attempts"),
24286
24602
  success: tool.schema.boolean().describe("Whether the subtask succeeded"),
24287
24603
  files_touched: tool.schema.array(tool.schema.string()).optional().describe("Files that were modified"),
24288
- criteria: tool.schema.array(tool.schema.string()).optional().describe("Criteria to generate feedback for (default: all default criteria)")
24604
+ criteria: tool.schema.array(tool.schema.string()).optional().describe("Criteria to generate feedback for (default: all default criteria)"),
24605
+ strategy: tool.schema.enum(["file-based", "feature-based", "risk-based"]).optional().describe("Decomposition strategy used for this task")
24289
24606
  },
24290
24607
  async execute(args) {
24291
24608
  const signals = {
@@ -24295,7 +24612,8 @@ var swarm_record_outcome = tool({
24295
24612
  retry_count: args.retry_count ?? 0,
24296
24613
  success: args.success,
24297
24614
  files_touched: args.files_touched ?? [],
24298
- timestamp: new Date().toISOString()
24615
+ timestamp: new Date().toISOString(),
24616
+ strategy: args.strategy
24299
24617
  };
24300
24618
  const validated = OutcomeSignalsSchema.parse(signals);
24301
24619
  const scored = scoreImplicitFeedback(validated, DEFAULT_LEARNING_CONFIG);
@@ -24305,7 +24623,13 @@ var swarm_record_outcome = tool({
24305
24623
  "patterns",
24306
24624
  "readable"
24307
24625
  ];
24308
- const feedbackEvents = criteriaToScore.map((criterion) => outcomeToFeedback(scored, criterion));
24626
+ const feedbackEvents = criteriaToScore.map((criterion) => {
24627
+ const event = outcomeToFeedback(scored, criterion);
24628
+ if (args.strategy) {
24629
+ event.context = `${event.context || ""} [strategy: ${args.strategy}]`.trim();
24630
+ }
24631
+ return event;
24632
+ });
24309
24633
  return JSON.stringify({
24310
24634
  success: true,
24311
24635
  outcome: {
@@ -24322,7 +24646,8 @@ var swarm_record_outcome = tool({
24322
24646
  duration_seconds: Math.round(args.duration_ms / 1000),
24323
24647
  error_count: args.error_count ?? 0,
24324
24648
  retry_count: args.retry_count ?? 0,
24325
- success: args.success
24649
+ success: args.success,
24650
+ strategy: args.strategy
24326
24651
  },
24327
24652
  note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights."
24328
24653
  }, null, 2);
@@ -24542,6 +24867,8 @@ var swarm_init = tool({
24542
24867
  });
24543
24868
  var swarmTools = {
24544
24869
  swarm_init,
24870
+ swarm_select_strategy,
24871
+ swarm_plan_prompt,
24545
24872
  swarm_decompose,
24546
24873
  swarm_validate_decomposition,
24547
24874
  swarm_status,
@@ -25027,6 +25354,7 @@ export {
25027
25354
  swarmTools,
25028
25355
  structuredTools,
25029
25356
  setStorage,
25357
+ selectStrategy,
25030
25358
  resetToolCache,
25031
25359
  resetStorage,
25032
25360
  requireTool,
@@ -25040,6 +25368,7 @@ export {
25040
25368
  formatToolAvailability,
25041
25369
  formatSubtaskPromptV2,
25042
25370
  formatSubtaskPrompt,
25371
+ formatStrategyGuidelines,
25043
25372
  formatEvaluationPrompt,
25044
25373
  extractJsonFromText,
25045
25374
  src_default as default,
@@ -25072,6 +25401,7 @@ export {
25072
25401
  SpawnedAgentSchema,
25073
25402
  SemanticMemoryStorage,
25074
25403
  SUBTASK_PROMPT_V2,
25404
+ STRATEGIES,
25075
25405
  InMemoryStorage,
25076
25406
  FileReservationConflictError,
25077
25407
  EvaluationSchema,
package/dist/plugin.js CHANGED
@@ -23360,6 +23360,11 @@ var CriterionWeightSchema = exports_external.object({
23360
23360
  last_validated: exports_external.string().optional(),
23361
23361
  half_life_days: exports_external.number().positive().default(90)
23362
23362
  });
23363
+ var DecompositionStrategySchema = exports_external.enum([
23364
+ "file-based",
23365
+ "feature-based",
23366
+ "risk-based"
23367
+ ]);
23363
23368
  var OutcomeSignalsSchema = exports_external.object({
23364
23369
  bead_id: exports_external.string(),
23365
23370
  duration_ms: exports_external.number().int().min(0),
@@ -23367,7 +23372,8 @@ var OutcomeSignalsSchema = exports_external.object({
23367
23372
  retry_count: exports_external.number().int().min(0),
23368
23373
  success: exports_external.boolean(),
23369
23374
  files_touched: exports_external.array(exports_external.string()).default([]),
23370
- timestamp: exports_external.string()
23375
+ timestamp: exports_external.string(),
23376
+ strategy: DecompositionStrategySchema.optional()
23371
23377
  });
23372
23378
  var ScoredOutcomeSchema = exports_external.object({
23373
23379
  signals: OutcomeSignalsSchema,
@@ -23512,6 +23518,167 @@ function detectInstructionConflicts(subtasks) {
23512
23518
  }
23513
23519
  return conflicts;
23514
23520
  }
23521
+ var STRATEGIES = {
23522
+ "file-based": {
23523
+ name: "file-based",
23524
+ description: "Group by file type or directory. Best for refactoring, migrations, and pattern changes across codebase.",
23525
+ keywords: [
23526
+ "refactor",
23527
+ "migrate",
23528
+ "update all",
23529
+ "rename",
23530
+ "replace",
23531
+ "convert",
23532
+ "upgrade",
23533
+ "deprecate",
23534
+ "remove",
23535
+ "cleanup",
23536
+ "lint",
23537
+ "format"
23538
+ ],
23539
+ guidelines: [
23540
+ "Group files by directory or type (e.g., all components, all tests)",
23541
+ "Minimize cross-directory dependencies within a subtask",
23542
+ "Handle shared types/utilities first if they change",
23543
+ "Each subtask should be a complete transformation of its file set",
23544
+ "Consider import/export relationships when grouping"
23545
+ ],
23546
+ antiPatterns: [
23547
+ "Don't split tightly coupled files across subtasks",
23548
+ "Don't group files that have no relationship",
23549
+ "Don't forget to update imports when moving/renaming"
23550
+ ],
23551
+ examples: [
23552
+ "Migrate all components to new API \u2192 split by component directory",
23553
+ "Rename userId to accountId \u2192 split by module (types first, then consumers)",
23554
+ "Update all tests to use new matcher \u2192 split by test directory"
23555
+ ]
23556
+ },
23557
+ "feature-based": {
23558
+ name: "feature-based",
23559
+ description: "Vertical slices with UI + API + data. Best for new features and adding functionality.",
23560
+ keywords: [
23561
+ "add",
23562
+ "implement",
23563
+ "build",
23564
+ "create",
23565
+ "feature",
23566
+ "new",
23567
+ "integrate",
23568
+ "connect",
23569
+ "enable",
23570
+ "support"
23571
+ ],
23572
+ guidelines: [
23573
+ "Each subtask is a complete vertical slice (UI + logic + data)",
23574
+ "Start with data layer/types, then logic, then UI",
23575
+ "Keep related components together (form + validation + submission)",
23576
+ "Separate concerns that can be developed independently",
23577
+ "Consider user-facing features as natural boundaries"
23578
+ ],
23579
+ antiPatterns: [
23580
+ "Don't split a single feature across multiple subtasks",
23581
+ "Don't create subtasks that can't be tested independently",
23582
+ "Don't forget integration points between features"
23583
+ ],
23584
+ examples: [
23585
+ "Add user auth \u2192 [OAuth setup, Session management, Protected routes]",
23586
+ "Build dashboard \u2192 [Data fetching, Chart components, Layout/navigation]",
23587
+ "Add search \u2192 [Search API, Search UI, Results display]"
23588
+ ]
23589
+ },
23590
+ "risk-based": {
23591
+ name: "risk-based",
23592
+ description: "Isolate high-risk changes, add tests first. Best for bug fixes, security issues, and critical changes.",
23593
+ keywords: [
23594
+ "fix",
23595
+ "bug",
23596
+ "security",
23597
+ "vulnerability",
23598
+ "critical",
23599
+ "urgent",
23600
+ "hotfix",
23601
+ "patch",
23602
+ "audit",
23603
+ "review",
23604
+ "investigate"
23605
+ ],
23606
+ guidelines: [
23607
+ "Write tests FIRST to capture expected behavior",
23608
+ "Isolate the risky change to minimize blast radius",
23609
+ "Add monitoring/logging around the change",
23610
+ "Create rollback plan as part of the task",
23611
+ "Audit similar code for the same issue"
23612
+ ],
23613
+ antiPatterns: [
23614
+ "Don't make multiple risky changes in one subtask",
23615
+ "Don't skip tests for 'simple' fixes",
23616
+ "Don't forget to check for similar issues elsewhere"
23617
+ ],
23618
+ examples: [
23619
+ "Fix auth bypass \u2192 [Add regression test, Fix vulnerability, Audit similar endpoints]",
23620
+ "Fix race condition \u2192 [Add test reproducing issue, Implement fix, Add concurrency tests]",
23621
+ "Security audit \u2192 [Scan for vulnerabilities, Fix critical issues, Document remaining risks]"
23622
+ ]
23623
+ }
23624
+ };
23625
+ function selectStrategy(task) {
23626
+ const taskLower = task.toLowerCase();
23627
+ const scores = {
23628
+ "file-based": 0,
23629
+ "feature-based": 0,
23630
+ "risk-based": 0
23631
+ };
23632
+ for (const [strategyName, definition] of Object.entries(STRATEGIES)) {
23633
+ const name = strategyName;
23634
+ for (const keyword of definition.keywords) {
23635
+ if (taskLower.includes(keyword)) {
23636
+ scores[name] += 1;
23637
+ }
23638
+ }
23639
+ }
23640
+ const entries = Object.entries(scores);
23641
+ entries.sort((a, b) => b[1] - a[1]);
23642
+ const [winner, winnerScore] = entries[0];
23643
+ const [runnerUp, runnerUpScore] = entries[1] || [null, 0];
23644
+ const totalScore = entries.reduce((sum, [, score]) => sum + score, 0);
23645
+ const confidence = totalScore > 0 ? Math.min(0.95, 0.5 + (winnerScore - runnerUpScore) / totalScore) : 0.5;
23646
+ let reasoning;
23647
+ if (winnerScore === 0) {
23648
+ reasoning = `No strong keyword signals. Defaulting to feature-based as it's most versatile.`;
23649
+ } else {
23650
+ const matchedKeywords = STRATEGIES[winner].keywords.filter((k) => taskLower.includes(k));
23651
+ reasoning = `Matched keywords: ${matchedKeywords.join(", ")}. ${STRATEGIES[winner].description}`;
23652
+ }
23653
+ const finalStrategy = winnerScore === 0 ? "feature-based" : winner;
23654
+ return {
23655
+ strategy: finalStrategy,
23656
+ confidence,
23657
+ reasoning,
23658
+ alternatives: entries.filter(([s]) => s !== finalStrategy).map(([strategy, score]) => ({ strategy, score }))
23659
+ };
23660
+ }
23661
+ function formatStrategyGuidelines(strategy) {
23662
+ const def = STRATEGIES[strategy];
23663
+ const guidelines = def.guidelines.map((g) => `- ${g}`).join(`
23664
+ `);
23665
+ const antiPatterns = def.antiPatterns.map((a) => `- ${a}`).join(`
23666
+ `);
23667
+ const examples = def.examples.map((e) => `- ${e}`).join(`
23668
+ `);
23669
+ return `## Strategy: ${strategy}
23670
+
23671
+ ${def.description}
23672
+
23673
+ ### Guidelines
23674
+ ${guidelines}
23675
+
23676
+ ### Anti-Patterns (Avoid These)
23677
+ ${antiPatterns}
23678
+
23679
+ ### Examples
23680
+ ${examples}`;
23681
+ }
23515
23682
  var DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
23516
23683
 
23517
23684
  ## Task
@@ -23877,6 +24044,155 @@ function formatCassHistoryForPrompt(history) {
23877
24044
  return lines.join(`
23878
24045
  `);
23879
24046
  }
24047
+ var swarm_select_strategy = tool({
24048
+ description: "Analyze task and recommend decomposition strategy (file-based, feature-based, or risk-based)",
24049
+ args: {
24050
+ task: tool.schema.string().min(1).describe("Task description to analyze"),
24051
+ codebase_context: tool.schema.string().optional().describe("Optional codebase context (file structure, tech stack, etc.)")
24052
+ },
24053
+ async execute(args) {
24054
+ const result = selectStrategy(args.task);
24055
+ let enhancedReasoning = result.reasoning;
24056
+ if (args.codebase_context) {
24057
+ enhancedReasoning += `
24058
+
24059
+ Codebase context considered: ${args.codebase_context.slice(0, 200)}...`;
24060
+ }
24061
+ return JSON.stringify({
24062
+ strategy: result.strategy,
24063
+ confidence: Math.round(result.confidence * 100) / 100,
24064
+ reasoning: enhancedReasoning,
24065
+ description: STRATEGIES[result.strategy].description,
24066
+ guidelines: STRATEGIES[result.strategy].guidelines,
24067
+ anti_patterns: STRATEGIES[result.strategy].antiPatterns,
24068
+ alternatives: result.alternatives.map((alt) => ({
24069
+ strategy: alt.strategy,
24070
+ description: STRATEGIES[alt.strategy].description,
24071
+ score: alt.score
24072
+ }))
24073
+ }, null, 2);
24074
+ }
24075
+ });
24076
+ var STRATEGY_DECOMPOSITION_PROMPT = `You are decomposing a task into parallelizable subtasks for a swarm of agents.
24077
+
24078
+ ## Task
24079
+ {task}
24080
+
24081
+ {strategy_guidelines}
24082
+
24083
+ {context_section}
24084
+
24085
+ {cass_history}
24086
+
24087
+ ## MANDATORY: Beads Issue Tracking
24088
+
24089
+ **Every subtask MUST become a bead.** This is non-negotiable.
24090
+
24091
+ After decomposition, the coordinator will:
24092
+ 1. Create an epic bead for the overall task
24093
+ 2. Create child beads for each subtask
24094
+ 3. Track progress through bead status updates
24095
+ 4. Close beads with summaries when complete
24096
+
24097
+ Agents MUST update their bead status as they work. No silent progress.
24098
+
24099
+ ## Requirements
24100
+
24101
+ 1. **Break into 2-{max_subtasks} independent subtasks** that can run in parallel
24102
+ 2. **Assign files** - each subtask must specify which files it will modify
24103
+ 3. **No file overlap** - files cannot appear in multiple subtasks (they get exclusive locks)
24104
+ 4. **Order by dependency** - if subtask B needs subtask A's output, A must come first in the array
24105
+ 5. **Estimate complexity** - 1 (trivial) to 5 (complex)
24106
+ 6. **Plan aggressively** - break down more than you think necessary, smaller is better
24107
+
24108
+ ## Response Format
24109
+
24110
+ Respond with a JSON object matching this schema:
24111
+
24112
+ \`\`\`typescript
24113
+ {
24114
+ epic: {
24115
+ title: string, // Epic title for the beads tracker
24116
+ description?: string // Brief description of the overall goal
24117
+ },
24118
+ subtasks: [
24119
+ {
24120
+ title: string, // What this subtask accomplishes
24121
+ description?: string, // Detailed instructions for the agent
24122
+ files: string[], // Files this subtask will modify (globs allowed)
24123
+ dependencies: number[], // Indices of subtasks this depends on (0-indexed)
24124
+ estimated_complexity: 1-5 // Effort estimate
24125
+ },
24126
+ // ... more subtasks
24127
+ ]
24128
+ }
24129
+ \`\`\`
24130
+
24131
+ Now decompose the task:`;
24132
+ var swarm_plan_prompt = tool({
24133
+ description: "Generate strategy-specific decomposition prompt. Auto-selects strategy or uses provided one. Queries CASS for similar tasks.",
24134
+ args: {
24135
+ task: tool.schema.string().min(1).describe("Task description to decompose"),
24136
+ strategy: tool.schema.enum(["file-based", "feature-based", "risk-based", "auto"]).optional().describe("Decomposition strategy (default: auto-detect)"),
24137
+ max_subtasks: tool.schema.number().int().min(2).max(10).default(5).describe("Maximum number of subtasks (default: 5)"),
24138
+ context: tool.schema.string().optional().describe("Additional context (codebase info, constraints, etc.)"),
24139
+ query_cass: tool.schema.boolean().optional().describe("Query CASS for similar past tasks (default: true)"),
24140
+ cass_limit: tool.schema.number().int().min(1).max(10).optional().describe("Max CASS results to include (default: 3)")
24141
+ },
24142
+ async execute(args) {
24143
+ let selectedStrategy;
24144
+ let strategyReasoning;
24145
+ if (args.strategy && args.strategy !== "auto") {
24146
+ selectedStrategy = args.strategy;
24147
+ strategyReasoning = `User-specified strategy: ${selectedStrategy}`;
24148
+ } else {
24149
+ const selection = selectStrategy(args.task);
24150
+ selectedStrategy = selection.strategy;
24151
+ strategyReasoning = selection.reasoning;
24152
+ }
24153
+ let cassContext = "";
24154
+ let cassResult = null;
24155
+ if (args.query_cass !== false) {
24156
+ cassResult = await queryCassHistory(args.task, args.cass_limit ?? 3);
24157
+ if (cassResult && cassResult.results.length > 0) {
24158
+ cassContext = formatCassHistoryForPrompt(cassResult);
24159
+ }
24160
+ }
24161
+ const strategyGuidelines = formatStrategyGuidelines(selectedStrategy);
24162
+ const contextSection = args.context ? `## Additional Context
24163
+ ${args.context}` : `## Additional Context
24164
+ (none provided)`;
24165
+ const prompt = STRATEGY_DECOMPOSITION_PROMPT.replace("{task}", args.task).replace("{strategy_guidelines}", strategyGuidelines).replace("{context_section}", contextSection).replace("{cass_history}", cassContext || "").replace("{max_subtasks}", (args.max_subtasks ?? 5).toString());
24166
+ return JSON.stringify({
24167
+ prompt,
24168
+ strategy: {
24169
+ selected: selectedStrategy,
24170
+ reasoning: strategyReasoning,
24171
+ guidelines: STRATEGIES[selectedStrategy].guidelines,
24172
+ anti_patterns: STRATEGIES[selectedStrategy].antiPatterns
24173
+ },
24174
+ expected_schema: "BeadTree",
24175
+ schema_hint: {
24176
+ epic: { title: "string", description: "string?" },
24177
+ subtasks: [
24178
+ {
24179
+ title: "string",
24180
+ description: "string?",
24181
+ files: "string[]",
24182
+ dependencies: "number[]",
24183
+ estimated_complexity: "1-5"
24184
+ }
24185
+ ]
24186
+ },
24187
+ validation_note: "Parse agent response as JSON and validate with swarm_validate_decomposition",
24188
+ cass_history: cassResult ? {
24189
+ queried: true,
24190
+ results_found: cassResult.results.length,
24191
+ included_in_context: cassResult.results.length > 0
24192
+ } : { queried: false, reason: "disabled or unavailable" }
24193
+ }, null, 2);
24194
+ }
24195
+ });
23880
24196
  var swarm_decompose = tool({
23881
24197
  description: "Generate decomposition prompt for breaking task into parallelizable subtasks. Optionally queries CASS for similar past tasks.",
23882
24198
  args: {
@@ -24236,7 +24552,8 @@ var swarm_record_outcome = tool({
24236
24552
  retry_count: tool.schema.number().int().min(0).default(0).describe("Number of retry attempts"),
24237
24553
  success: tool.schema.boolean().describe("Whether the subtask succeeded"),
24238
24554
  files_touched: tool.schema.array(tool.schema.string()).optional().describe("Files that were modified"),
24239
- criteria: tool.schema.array(tool.schema.string()).optional().describe("Criteria to generate feedback for (default: all default criteria)")
24555
+ criteria: tool.schema.array(tool.schema.string()).optional().describe("Criteria to generate feedback for (default: all default criteria)"),
24556
+ strategy: tool.schema.enum(["file-based", "feature-based", "risk-based"]).optional().describe("Decomposition strategy used for this task")
24240
24557
  },
24241
24558
  async execute(args) {
24242
24559
  const signals = {
@@ -24246,7 +24563,8 @@ var swarm_record_outcome = tool({
24246
24563
  retry_count: args.retry_count ?? 0,
24247
24564
  success: args.success,
24248
24565
  files_touched: args.files_touched ?? [],
24249
- timestamp: new Date().toISOString()
24566
+ timestamp: new Date().toISOString(),
24567
+ strategy: args.strategy
24250
24568
  };
24251
24569
  const validated = OutcomeSignalsSchema.parse(signals);
24252
24570
  const scored = scoreImplicitFeedback(validated, DEFAULT_LEARNING_CONFIG);
@@ -24256,7 +24574,13 @@ var swarm_record_outcome = tool({
24256
24574
  "patterns",
24257
24575
  "readable"
24258
24576
  ];
24259
- const feedbackEvents = criteriaToScore.map((criterion) => outcomeToFeedback(scored, criterion));
24577
+ const feedbackEvents = criteriaToScore.map((criterion) => {
24578
+ const event = outcomeToFeedback(scored, criterion);
24579
+ if (args.strategy) {
24580
+ event.context = `${event.context || ""} [strategy: ${args.strategy}]`.trim();
24581
+ }
24582
+ return event;
24583
+ });
24260
24584
  return JSON.stringify({
24261
24585
  success: true,
24262
24586
  outcome: {
@@ -24273,7 +24597,8 @@ var swarm_record_outcome = tool({
24273
24597
  duration_seconds: Math.round(args.duration_ms / 1000),
24274
24598
  error_count: args.error_count ?? 0,
24275
24599
  retry_count: args.retry_count ?? 0,
24276
- success: args.success
24600
+ success: args.success,
24601
+ strategy: args.strategy
24277
24602
  },
24278
24603
  note: "Feedback events should be stored for criterion weight calculation. Use learning.ts functions to apply weights."
24279
24604
  }, null, 2);
@@ -24493,6 +24818,8 @@ var swarm_init = tool({
24493
24818
  });
24494
24819
  var swarmTools = {
24495
24820
  swarm_init,
24821
+ swarm_select_strategy,
24822
+ swarm_plan_prompt,
24496
24823
  swarm_decompose,
24497
24824
  swarm_validate_decomposition,
24498
24825
  swarm_status,
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "opencode-swarm-setup": "./scripts/setup.ts"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "import": "./dist/index.js",
@@ -0,0 +1,371 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * OpenCode Swarm Plugin - Setup Script
4
+ *
5
+ * Checks for required dependencies and installs them if missing.
6
+ * Mac-specific (uses Homebrew).
7
+ *
8
+ * Usage:
9
+ * bunx opencode-swarm-plugin/scripts/setup.ts
10
+ * # or after cloning:
11
+ * bun scripts/setup.ts
12
+ */
13
+
14
+ import { $ } from "bun";
15
+ import { existsSync, mkdirSync, copyFileSync } from "fs";
16
+ import { homedir } from "os";
17
+ import { join } from "path";
18
+
19
+ // ANSI colors
20
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
21
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
22
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
23
+ const blue = (s: string) => `\x1b[34m${s}\x1b[0m`;
24
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
25
+
26
+ const CHECK = green("✓");
27
+ const CROSS = red("✗");
28
+ const WARN = yellow("⚠");
29
+ const INFO = blue("→");
30
+
31
+ interface Dependency {
32
+ name: string;
33
+ check: () => Promise<boolean>;
34
+ install: () => Promise<void>;
35
+ optional?: boolean;
36
+ purpose: string;
37
+ }
38
+
39
+ /**
40
+ * Check if a command exists
41
+ */
42
+ async function commandExists(cmd: string): Promise<boolean> {
43
+ try {
44
+ const result = await $`which ${cmd}`.quiet().nothrow();
45
+ return result.exitCode === 0;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check if a URL is reachable
53
+ */
54
+ async function urlReachable(url: string): Promise<boolean> {
55
+ try {
56
+ const response = await fetch(url, { method: "HEAD" });
57
+ return response.ok;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Run a shell command with output
65
+ */
66
+ async function run(cmd: string, args: string[] = []): Promise<boolean> {
67
+ try {
68
+ const proc = Bun.spawn([cmd, ...args], {
69
+ stdout: "inherit",
70
+ stderr: "inherit",
71
+ });
72
+ const exitCode = await proc.exited;
73
+ return exitCode === 0;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ const dependencies: Dependency[] = [
80
+ {
81
+ name: "Homebrew",
82
+ purpose: "Package manager for installing other dependencies",
83
+ check: async () => commandExists("brew"),
84
+ install: async () => {
85
+ console.log(`${INFO} Installing Homebrew...`);
86
+ const script = await fetch(
87
+ "https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh",
88
+ ).then((r) => r.text());
89
+ await $`/bin/bash -c ${script}`;
90
+ },
91
+ },
92
+ {
93
+ name: "OpenCode",
94
+ purpose: "AI coding assistant (plugin host)",
95
+ check: async () => commandExists("opencode"),
96
+ install: async () => {
97
+ console.log(`${INFO} Installing OpenCode...`);
98
+ await run("brew", ["install", "sst/tap/opencode"]);
99
+ },
100
+ },
101
+ {
102
+ name: "Beads CLI (bd)",
103
+ purpose: "Git-backed issue tracking",
104
+ check: async () => commandExists("bd"),
105
+ install: async () => {
106
+ console.log(`${INFO} Installing Beads...`);
107
+ // Try npm global install first
108
+ const npmResult = await $`npm install -g @joelhooks/beads`
109
+ .quiet()
110
+ .nothrow();
111
+ if (npmResult.exitCode !== 0) {
112
+ console.log(`${WARN} npm install failed, trying go install...`);
113
+ await run("go", [
114
+ "install",
115
+ "github.com/joelhooks/beads/cmd/bd@latest",
116
+ ]);
117
+ }
118
+ },
119
+ },
120
+ {
121
+ name: "Agent Mail MCP",
122
+ purpose: "Multi-agent coordination server",
123
+ check: async () => {
124
+ // Check if server is running
125
+ const running = await urlReachable(
126
+ "http://127.0.0.1:8765/health/liveness",
127
+ );
128
+ if (running) return true;
129
+ // Check if binary exists
130
+ return commandExists("agent-mail");
131
+ },
132
+ install: async () => {
133
+ console.log(`${INFO} Installing Agent Mail...`);
134
+ console.log(dim(" Agent Mail requires manual setup:"));
135
+ console.log(
136
+ dim(" 1. Clone: git clone https://github.com/joelhooks/agent-mail"),
137
+ );
138
+ console.log(
139
+ dim(" 2. Build: cd agent-mail && go build -o agent-mail ./cmd/server"),
140
+ );
141
+ console.log(dim(" 3. Run: ./agent-mail serve"));
142
+ console.log();
143
+ console.log(
144
+ `${WARN} Skipping automatic install - see instructions above`,
145
+ );
146
+ },
147
+ optional: true,
148
+ },
149
+ {
150
+ name: "Redis",
151
+ purpose: "Rate limiting (optional, falls back to SQLite)",
152
+ check: async () => {
153
+ // Check if redis-server is running or installed
154
+ const running = await $`redis-cli ping`.quiet().nothrow();
155
+ if (running.exitCode === 0) return true;
156
+ return commandExists("redis-server");
157
+ },
158
+ install: async () => {
159
+ console.log(`${INFO} Installing Redis...`);
160
+ await run("brew", ["install", "redis"]);
161
+ console.log(dim(" Start with: brew services start redis"));
162
+ },
163
+ optional: true,
164
+ },
165
+ {
166
+ name: "CASS (cass-memory)",
167
+ purpose: "Cross-agent session search for historical context",
168
+ check: async () => commandExists("cass"),
169
+ install: async () => {
170
+ console.log(`${INFO} CASS installation...`);
171
+ console.log(
172
+ dim(" Install from: https://github.com/Dicklesworthstone/cass"),
173
+ );
174
+ console.log(`${WARN} Skipping automatic install - see link above`);
175
+ },
176
+ optional: true,
177
+ },
178
+ {
179
+ name: "UBS (bug scanner)",
180
+ purpose: "Pre-completion bug scanning",
181
+ check: async () => commandExists("ubs"),
182
+ install: async () => {
183
+ console.log(`${INFO} UBS installation...`);
184
+ console.log(dim(" UBS is bundled with OpenCode plugins"));
185
+ console.log(
186
+ `${WARN} Skipping - should be available if OpenCode is installed`,
187
+ );
188
+ },
189
+ optional: true,
190
+ },
191
+ ];
192
+
193
+ /**
194
+ * Setup OpenCode directories and copy plugin
195
+ */
196
+ async function setupOpenCodeDirs(): Promise<void> {
197
+ const configDir = join(homedir(), ".config", "opencode");
198
+ const pluginsDir = join(configDir, "plugins");
199
+ const commandsDir = join(configDir, "commands");
200
+ const agentsDir = join(configDir, "agents");
201
+
202
+ // Create directories
203
+ for (const dir of [pluginsDir, commandsDir, agentsDir]) {
204
+ if (!existsSync(dir)) {
205
+ mkdirSync(dir, { recursive: true });
206
+ console.log(`${CHECK} Created ${dir}`);
207
+ }
208
+ }
209
+
210
+ // Find plugin files (either in node_modules or local)
211
+ const possiblePaths = [
212
+ join(process.cwd(), "dist", "plugin.js"),
213
+ join(
214
+ process.cwd(),
215
+ "node_modules",
216
+ "opencode-swarm-plugin",
217
+ "dist",
218
+ "plugin.js",
219
+ ),
220
+ ];
221
+
222
+ let pluginSrc: string | null = null;
223
+ for (const p of possiblePaths) {
224
+ if (existsSync(p)) {
225
+ pluginSrc = p;
226
+ break;
227
+ }
228
+ }
229
+
230
+ if (pluginSrc) {
231
+ const pluginDest = join(pluginsDir, "swarm.js");
232
+ copyFileSync(pluginSrc, pluginDest);
233
+ console.log(`${CHECK} Copied plugin to ${pluginDest}`);
234
+ } else {
235
+ console.log(
236
+ `${WARN} Plugin not found - run 'pnpm build' first or install from npm`,
237
+ );
238
+ }
239
+
240
+ // Copy example files if they exist
241
+ const examplesDir = join(process.cwd(), "examples");
242
+ const nodeModulesExamples = join(
243
+ process.cwd(),
244
+ "node_modules",
245
+ "opencode-swarm-plugin",
246
+ "examples",
247
+ );
248
+
249
+ const examplesSrc = existsSync(examplesDir)
250
+ ? examplesDir
251
+ : nodeModulesExamples;
252
+
253
+ if (existsSync(examplesSrc)) {
254
+ const swarmCmd = join(examplesSrc, "commands", "swarm.md");
255
+ const plannerAgent = join(examplesSrc, "agents", "swarm-planner.md");
256
+
257
+ if (existsSync(swarmCmd)) {
258
+ copyFileSync(swarmCmd, join(commandsDir, "swarm.md"));
259
+ console.log(`${CHECK} Copied /swarm command`);
260
+ }
261
+
262
+ if (existsSync(plannerAgent)) {
263
+ copyFileSync(plannerAgent, join(agentsDir, "swarm-planner.md"));
264
+ console.log(`${CHECK} Copied @swarm-planner agent`);
265
+ }
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Main setup function
271
+ */
272
+ async function main() {
273
+ console.log();
274
+ console.log(
275
+ blue("═══════════════════════════════════════════════════════════"),
276
+ );
277
+ console.log(blue(" OpenCode Swarm Plugin - Setup"));
278
+ console.log(
279
+ blue("═══════════════════════════════════════════════════════════"),
280
+ );
281
+ console.log();
282
+
283
+ // Check platform
284
+ if (process.platform !== "darwin") {
285
+ console.log(
286
+ `${WARN} This script is optimized for macOS. Some installs may not work.`,
287
+ );
288
+ console.log();
289
+ }
290
+
291
+ // Check dependencies
292
+ console.log(blue("Checking dependencies...\n"));
293
+
294
+ const missing: Dependency[] = [];
295
+ const optionalMissing: Dependency[] = [];
296
+
297
+ for (const dep of dependencies) {
298
+ const installed = await dep.check();
299
+ const status = installed ? CHECK : dep.optional ? WARN : CROSS;
300
+ const suffix = dep.optional ? dim(" (optional)") : "";
301
+
302
+ console.log(`${status} ${dep.name}${suffix}`);
303
+ console.log(dim(` ${dep.purpose}`));
304
+
305
+ if (!installed) {
306
+ if (dep.optional) {
307
+ optionalMissing.push(dep);
308
+ } else {
309
+ missing.push(dep);
310
+ }
311
+ }
312
+ }
313
+
314
+ console.log();
315
+
316
+ // Install missing required dependencies
317
+ if (missing.length > 0) {
318
+ console.log(blue("Installing missing required dependencies...\n"));
319
+
320
+ for (const dep of missing) {
321
+ try {
322
+ await dep.install();
323
+ const nowInstalled = await dep.check();
324
+ if (nowInstalled) {
325
+ console.log(`${CHECK} ${dep.name} installed successfully\n`);
326
+ } else {
327
+ console.log(`${CROSS} ${dep.name} installation may have failed\n`);
328
+ }
329
+ } catch (error) {
330
+ console.log(`${CROSS} Failed to install ${dep.name}: ${error}\n`);
331
+ }
332
+ }
333
+ }
334
+
335
+ // Offer to install optional dependencies
336
+ if (optionalMissing.length > 0) {
337
+ console.log(yellow("Optional dependencies not installed:"));
338
+ for (const dep of optionalMissing) {
339
+ console.log(` - ${dep.name}: ${dep.purpose}`);
340
+ }
341
+ console.log();
342
+ console.log(
343
+ dim("The plugin will work without these, with degraded features."),
344
+ );
345
+ console.log();
346
+ }
347
+
348
+ // Setup OpenCode directories
349
+ console.log(blue("Setting up OpenCode directories...\n"));
350
+ await setupOpenCodeDirs();
351
+
352
+ console.log();
353
+ console.log(
354
+ blue("═══════════════════════════════════════════════════════════"),
355
+ );
356
+ console.log(blue(" Setup Complete!"));
357
+ console.log(
358
+ blue("═══════════════════════════════════════════════════════════"),
359
+ );
360
+ console.log();
361
+ console.log("Next steps:");
362
+ console.log(
363
+ ` 1. ${dim("Start Agent Mail (if using multi-agent):")} agent-mail serve`,
364
+ );
365
+ console.log(` 2. ${dim("Initialize beads in your project:")} bd init`);
366
+ console.log(` 3. ${dim("Start OpenCode:")} opencode`);
367
+ console.log(` 4. ${dim("Try the swarm command:")} /swarm "your task here"`);
368
+ console.log();
369
+ }
370
+
371
+ main().catch(console.error);