opencode-swarm 4.4.0 → 4.5.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://img.shields.io/badge/version-4.3.1-blue" alt="Version">
2
+ <img src="https://img.shields.io/badge/version-4.5.0-blue" alt="Version">
3
3
  <img src="https://img.shields.io/badge/license-MIT-green" alt="License">
4
4
  <img src="https://img.shields.io/badge/opencode-plugin-purple" alt="OpenCode Plugin">
5
5
  <img src="https://img.shields.io/badge/agents-8-orange" alt="Agents">
6
- <img src="https://img.shields.io/badge/tests-483-brightgreen" alt="Tests">
6
+ <img src="https://img.shields.io/badge/tests-622-brightgreen" alt="Tests">
7
7
  </p>
8
8
 
9
9
  <h1 align="center">🐝 OpenCode Swarm</h1>
@@ -313,37 +313,47 @@ Each architect automatically delegates to its own swarm's agents.
313
313
  ## Installation
314
314
 
315
315
  ```bash
316
- # Add to opencode.json
317
- {
318
- "plugin": ["opencode-swarm"]
319
- }
320
-
321
- # Or install via CLI
316
+ # Install via CLI (recommended)
322
317
  bunx opencode-swarm install
323
318
  ```
324
319
 
320
+ ### Uninstall
321
+
322
+ ```bash
323
+ # Remove from opencode.json
324
+ bunx opencode-swarm uninstall
325
+
326
+ # Remove from opencode.json + clean up config files
327
+ bunx opencode-swarm uninstall --clean
328
+ ```
329
+
325
330
  ---
326
331
 
327
- ## What's New in v4.3.0
332
+ ## What's New
328
333
 
329
- ### 🔧 Hooks Pipeline
330
- - **`safeHook()`**Crash-safe wrapper for all hooks. Errors are caught and logged, never crash the plugin.
331
- - **`composeHandlers()`**Compose multiple handlers for the same hook type (Plugin API allows only one handler per type).
334
+ ### v4.5.0 Tech Debt + New Commands
335
+ - **Lint cleanup** Replaced string concatenation with template literals, documented `as any` casts with biome-ignore comments.
336
+ - **Code deduplication** Extracted `stripSwarmPrefix()` utility to eliminate 3 duplicate prefix-stripping blocks.
337
+ - **`/swarm diagnose`** — Health check for `.swarm/` files, plan structure, and plugin configuration.
338
+ - **`/swarm export`** — Export plan.md and context.md as portable JSON.
339
+ - **`/swarm reset --confirm`** — Clear swarm state files with safety confirmation.
332
340
 
333
- ### 📊 Context Pruning
334
- - **Token budget tracking** — Estimates context window usage and injects warnings at 70% and 90% thresholds.
335
- - **Enhanced session compaction** — Guides OpenCode's built-in compaction with plan.md phases and context.md decisions.
336
- - **System prompt enhancement** Injects current phase, task, and key decisions to keep agents focused post-compaction.
341
+ ### v4.4.0 DX & Quality
342
+ - **CLI `uninstall` command** — Remove plugin with optional `--clean` flag.
343
+ - **Custom error classes** — `SwarmError` hierarchy with actionable `guidance` messages.
344
+ - **`/swarm history`**View completed phases from plan.md.
345
+ - **`/swarm config`** — View current resolved plugin configuration.
337
346
 
338
- ### Slash Commands
339
- - **`/swarm status`**Current phase, progress, and agent count.
340
- - **`/swarm plan [N]`** View full plan or a specific phase.
341
- - **`/swarm agents`**List all registered agents with models and permissions.
347
+ ### v4.3.2 Security Hardening
348
+ - **Path validation**`validateSwarmPath()` prevents directory traversal in `.swarm/` file operations.
349
+ - **Fetch hardening**10s timeout, 5MB limit, retry logic for gitingest tool.
350
+ - **Config limits**Deep merge depth limit (10), config file size limit (100KB).
342
351
 
343
- ### 🤖 Agent Awareness
344
- - **Activity tracking** — Tool usage tracked per agent via `tool.execute.before/after` hooks. Activity summary flushed to context.md every 20 events.
345
- - **Delegation tracking** — Active agent per session tracked via `chat.message` hook. Optional delegation chain logging.
346
- - **Cross-agent context** — System enhancer injects relevant context from other agents' activity into system prompts.
352
+ ### v4.3.0 — Hooks & Agent Awareness
353
+ - **Hooks pipeline** — `safeHook()` crash-safe wrapper, `composeHandlers()` for multi-handler composition.
354
+ - **Context pruning** — Token budget tracking with 70%/90% threshold warnings.
355
+ - **Slash commands** — `/swarm status`, `/swarm plan`, `/swarm agents`.
356
+ - **Agent awareness** — Activity tracking, delegation tracking, cross-agent context injection.
347
357
 
348
358
  All features are opt-in via configuration. See [Installation Guide](docs/installation.md) for config options.
349
359
 
@@ -380,6 +390,21 @@ All features are opt-in via configuration. See [Installation Guide](docs/install
380
390
 
381
391
  ---
382
392
 
393
+ ## Slash Commands
394
+
395
+ | Command | Description |
396
+ |---------|-------------|
397
+ | `/swarm status` | Current phase, task progress, and agent count |
398
+ | `/swarm plan [N]` | View full plan or filter by phase number |
399
+ | `/swarm agents` | List all registered agents with models and permissions |
400
+ | `/swarm history` | View completed phases with status icons |
401
+ | `/swarm config` | View current resolved plugin configuration |
402
+ | `/swarm diagnose` | Health check for .swarm/ files and config |
403
+ | `/swarm export` | Export plan and context as portable JSON |
404
+ | `/swarm reset --confirm` | Clear swarm state files (with safety gate) |
405
+
406
+ ---
407
+
383
408
  ## Configuration
384
409
 
385
410
  Create `~/.config/opencode/opencode-swarm.json`:
@@ -448,7 +473,22 @@ bun test
448
473
  bun test tests/unit/config/schema.test.ts
449
474
  ```
450
475
 
451
- 447 unit tests across 21 files covering config, tools, agents, hooks, commands, and state. Uses Bun's built-in test runner — zero additional test dependencies.
476
+ 622 unit tests across 29 files covering config, tools, agents, hooks, commands, and state. Uses Bun's built-in test runner — zero additional test dependencies.
477
+
478
+ ## Troubleshooting
479
+
480
+ ### Plugin not loading
481
+ 1. Verify `opencode-swarm` is listed in your `opencode.json` plugins array
482
+ 2. Run `bunx opencode-swarm install` to auto-configure
483
+ 3. Run `/swarm diagnose` to check health status
484
+
485
+ ### Commands not working
486
+ - Ensure you're using `/swarm <command>`, not `/swarm/<command>`
487
+ - Run `/swarm` with no arguments to see available commands
488
+
489
+ ### Resuming a project
490
+ - Swarm automatically detects `.swarm/plan.md` and resumes where you left off
491
+ - If you get unexpected behavior, run `/swarm export` to backup, then `/swarm reset --confirm` to start fresh
452
492
 
453
493
  ---
454
494
 
@@ -2,6 +2,12 @@ import type { AgentConfig as SDKAgentConfig } from '@opencode-ai/sdk';
2
2
  import { type PluginConfig } from '../config';
3
3
  import { type AgentDefinition } from './architect';
4
4
  export type { AgentDefinition } from './architect';
5
+ /**
6
+ * Strip the swarm prefix from an agent name to get the base name.
7
+ * e.g., "local_coder" with prefix "local" → "coder"
8
+ * Returns the name unchanged if no prefix matches.
9
+ */
10
+ export declare function stripSwarmPrefix(agentName: string, swarmPrefix?: string): string;
5
11
  /**
6
12
  * Create all agent definitions with configuration applied
7
13
  */
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm diagnose command.
3
+ * Performs health checks on swarm state files and configuration.
4
+ */
5
+ export declare function handleDiagnoseCommand(directory: string, _args: string[]): Promise<string>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm export command.
3
+ * Exports plan.md and context.md as a portable JSON object.
4
+ */
5
+ export declare function handleExportCommand(directory: string, _args: string[]): Promise<string>;
@@ -1,8 +1,11 @@
1
1
  import type { AgentDefinition } from '../agents';
2
2
  export { handleAgentsCommand } from './agents';
3
3
  export { handleConfigCommand } from './config';
4
+ export { handleDiagnoseCommand } from './diagnose';
5
+ export { handleExportCommand } from './export';
4
6
  export { handleHistoryCommand } from './history';
5
7
  export { handlePlanCommand } from './plan';
8
+ export { handleResetCommand } from './reset';
6
9
  export { handleStatusCommand } from './status';
7
10
  /**
8
11
  * Creates a command.execute.before handler for /swarm commands.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Handles the /swarm reset command.
3
+ * Clears plan.md and context.md from .swarm/ directory.
4
+ * Requires --confirm flag as a safety gate.
5
+ */
6
+ export declare function handleResetCommand(directory: string, args: string[]): Promise<string>;
package/dist/index.js CHANGED
@@ -14246,28 +14246,28 @@ ${customAppendPrompt}`;
14246
14246
  }
14247
14247
 
14248
14248
  // src/agents/index.ts
14249
- function getModelForAgent(agentName, swarmAgents, swarmPrefix) {
14250
- let baseAgentName = agentName;
14251
- if (swarmPrefix && agentName.startsWith(`${swarmPrefix}_`)) {
14252
- baseAgentName = agentName.substring(swarmPrefix.length + 1);
14249
+ function stripSwarmPrefix(agentName, swarmPrefix) {
14250
+ if (!swarmPrefix || !agentName)
14251
+ return agentName;
14252
+ const prefixWithUnderscore = `${swarmPrefix}_`;
14253
+ if (agentName.startsWith(prefixWithUnderscore)) {
14254
+ return agentName.substring(prefixWithUnderscore.length);
14253
14255
  }
14256
+ return agentName;
14257
+ }
14258
+ function getModelForAgent(agentName, swarmAgents, swarmPrefix) {
14259
+ const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
14254
14260
  const explicit = swarmAgents?.[baseAgentName]?.model;
14255
14261
  if (explicit)
14256
14262
  return explicit;
14257
14263
  return DEFAULT_MODELS[baseAgentName] ?? DEFAULT_MODELS.default;
14258
14264
  }
14259
14265
  function isAgentDisabled(agentName, swarmAgents, swarmPrefix) {
14260
- let baseAgentName = agentName;
14261
- if (swarmPrefix && agentName.startsWith(`${swarmPrefix}_`)) {
14262
- baseAgentName = agentName.substring(swarmPrefix.length + 1);
14263
- }
14266
+ const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
14264
14267
  return swarmAgents?.[baseAgentName]?.disabled === true;
14265
14268
  }
14266
14269
  function getTemperatureOverride(agentName, swarmAgents, swarmPrefix) {
14267
- let baseAgentName = agentName;
14268
- if (swarmPrefix && agentName.startsWith(`${swarmPrefix}_`)) {
14269
- baseAgentName = agentName.substring(swarmPrefix.length + 1);
14270
- }
14270
+ const baseAgentName = stripSwarmPrefix(agentName, swarmPrefix);
14271
14271
  return swarmAgents?.[baseAgentName]?.temperature;
14272
14272
  }
14273
14273
  function applyOverrides(agent, swarmAgents, swarmPrefix) {
@@ -14534,6 +14534,92 @@ function estimateTokens(text) {
14534
14534
  return Math.ceil(text.length * 0.33);
14535
14535
  }
14536
14536
 
14537
+ // src/commands/diagnose.ts
14538
+ async function handleDiagnoseCommand(directory, _args) {
14539
+ const checks3 = [];
14540
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
14541
+ const contextContent = await readSwarmFileAsync(directory, "context.md");
14542
+ if (planContent) {
14543
+ const hasPhases = /^## Phase \d+/m.test(planContent);
14544
+ const hasTasks = /^- \[[ x]\]/m.test(planContent);
14545
+ if (hasPhases && hasTasks) {
14546
+ checks3.push({
14547
+ name: "plan.md",
14548
+ status: "\u2705",
14549
+ detail: "Found with valid phase structure"
14550
+ });
14551
+ } else {
14552
+ checks3.push({
14553
+ name: "plan.md",
14554
+ status: "\u274C",
14555
+ detail: "Found but missing phase/task structure"
14556
+ });
14557
+ }
14558
+ } else {
14559
+ checks3.push({ name: "plan.md", status: "\u274C", detail: "Not found" });
14560
+ }
14561
+ if (contextContent) {
14562
+ checks3.push({ name: "context.md", status: "\u2705", detail: "Found" });
14563
+ } else {
14564
+ checks3.push({ name: "context.md", status: "\u274C", detail: "Not found" });
14565
+ }
14566
+ try {
14567
+ const config2 = loadPluginConfig(directory);
14568
+ if (config2) {
14569
+ checks3.push({
14570
+ name: "Plugin config",
14571
+ status: "\u2705",
14572
+ detail: "Valid configuration loaded"
14573
+ });
14574
+ } else {
14575
+ checks3.push({
14576
+ name: "Plugin config",
14577
+ status: "\u2705",
14578
+ detail: "Using defaults (no custom config)"
14579
+ });
14580
+ }
14581
+ } catch {
14582
+ checks3.push({
14583
+ name: "Plugin config",
14584
+ status: "\u274C",
14585
+ detail: "Invalid configuration"
14586
+ });
14587
+ }
14588
+ const passCount = checks3.filter((c) => c.status === "\u2705").length;
14589
+ const totalCount = checks3.length;
14590
+ const allPassed = passCount === totalCount;
14591
+ const lines = [
14592
+ "## Swarm Health Check",
14593
+ "",
14594
+ ...checks3.map((c) => `- ${c.status} **${c.name}**: ${c.detail}`),
14595
+ "",
14596
+ `**Result**: ${allPassed ? "\u2705 All checks passed" : `\u26A0\uFE0F ${passCount}/${totalCount} checks passed`}`
14597
+ ];
14598
+ return lines.join(`
14599
+ `);
14600
+ }
14601
+
14602
+ // src/commands/export.ts
14603
+ async function handleExportCommand(directory, _args) {
14604
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
14605
+ const contextContent = await readSwarmFileAsync(directory, "context.md");
14606
+ const exportData = {
14607
+ version: "4.5.0",
14608
+ exported: new Date().toISOString(),
14609
+ plan: planContent,
14610
+ context: contextContent
14611
+ };
14612
+ const lines = [
14613
+ "## Swarm Export",
14614
+ "",
14615
+ "```json",
14616
+ JSON.stringify(exportData, null, 2),
14617
+ "```"
14618
+ ];
14619
+ return lines.join(`
14620
+ `);
14621
+ }
14622
+
14537
14623
  // src/commands/history.ts
14538
14624
  async function handleHistoryCommand(directory, _args) {
14539
14625
  const planContent = await readSwarmFileAsync(directory, "plan.md");
@@ -14628,6 +14714,47 @@ async function handlePlanCommand(directory, args) {
14628
14714
  `).trim();
14629
14715
  }
14630
14716
 
14717
+ // src/commands/reset.ts
14718
+ import * as fs2 from "fs";
14719
+ async function handleResetCommand(directory, args) {
14720
+ const hasConfirm = args.includes("--confirm");
14721
+ if (!hasConfirm) {
14722
+ return [
14723
+ "## Swarm Reset",
14724
+ "",
14725
+ "\u26A0\uFE0F This will delete plan.md and context.md from .swarm/",
14726
+ "",
14727
+ "**Tip**: Run `/swarm export` first to backup your state.",
14728
+ "",
14729
+ "To confirm, run: `/swarm reset --confirm`"
14730
+ ].join(`
14731
+ `);
14732
+ }
14733
+ const filesToReset = ["plan.md", "context.md"];
14734
+ const results = [];
14735
+ for (const filename of filesToReset) {
14736
+ try {
14737
+ const resolvedPath = validateSwarmPath(directory, filename);
14738
+ if (fs2.existsSync(resolvedPath)) {
14739
+ fs2.unlinkSync(resolvedPath);
14740
+ results.push(`- \u2705 Deleted ${filename}`);
14741
+ } else {
14742
+ results.push(`- \u23ED\uFE0F ${filename} not found (skipped)`);
14743
+ }
14744
+ } catch {
14745
+ results.push(`- \u274C Failed to delete ${filename}`);
14746
+ }
14747
+ }
14748
+ return [
14749
+ "## Swarm Reset Complete",
14750
+ "",
14751
+ ...results,
14752
+ "",
14753
+ "Swarm state has been cleared. Start fresh with a new plan."
14754
+ ].join(`
14755
+ `);
14756
+ }
14757
+
14631
14758
  // src/hooks/extractors.ts
14632
14759
  function extractCurrentPhase(planContent) {
14633
14760
  if (!planContent) {
@@ -14804,7 +14931,10 @@ var HELP_TEXT = [
14804
14931
  "- `/swarm plan [phase]` \u2014 Show plan (optionally filter by phase number)",
14805
14932
  "- `/swarm agents` \u2014 List registered agents",
14806
14933
  "- `/swarm history` \u2014 Show completed phases summary",
14807
- "- `/swarm config` \u2014 Show current resolved configuration"
14934
+ "- `/swarm config` \u2014 Show current resolved configuration",
14935
+ "- `/swarm diagnose` \u2014 Run health check on swarm state",
14936
+ "- `/swarm export` \u2014 Export plan and context as JSON",
14937
+ "- `/swarm reset --confirm` \u2014 Clear swarm state files"
14808
14938
  ].join(`
14809
14939
  `);
14810
14940
  function createSwarmCommandHandler(directory, agents) {
@@ -14831,6 +14961,15 @@ function createSwarmCommandHandler(directory, agents) {
14831
14961
  case "config":
14832
14962
  text = await handleConfigCommand(directory, args);
14833
14963
  break;
14964
+ case "diagnose":
14965
+ text = await handleDiagnoseCommand(directory, args);
14966
+ break;
14967
+ case "export":
14968
+ text = await handleExportCommand(directory, args);
14969
+ break;
14970
+ case "reset":
14971
+ text = await handleResetCommand(directory, args);
14972
+ break;
14834
14973
  default:
14835
14974
  text = HELP_TEXT;
14836
14975
  break;
@@ -14945,19 +15084,19 @@ function renderActivitySection() {
14945
15084
  function replaceOrAppendSection(content, heading, newSection) {
14946
15085
  const headingIndex = content.indexOf(heading);
14947
15086
  if (headingIndex === -1) {
14948
- return content.trimEnd() + `
15087
+ return `${content.trimEnd()}
14949
15088
 
14950
- ` + newSection + `
15089
+ ${newSection}
14951
15090
  `;
14952
15091
  }
14953
15092
  const afterHeading = content.substring(headingIndex + heading.length);
14954
15093
  const nextHeadingMatch = afterHeading.match(/\n## /);
14955
15094
  if (nextHeadingMatch && nextHeadingMatch.index !== undefined) {
14956
15095
  const endIndex = headingIndex + heading.length + nextHeadingMatch.index;
14957
- return content.substring(0, headingIndex) + newSection + `
14958
- ` + content.substring(endIndex + 1);
15096
+ return `${content.substring(0, headingIndex)}${newSection}
15097
+ ${content.substring(endIndex + 1)}`;
14959
15098
  }
14960
- return content.substring(0, headingIndex) + newSection + `
15099
+ return `${content.substring(0, headingIndex)}${newSection}
14961
15100
  `;
14962
15101
  }
14963
15102
  // src/hooks/compaction-customizer.ts
@@ -15205,7 +15344,7 @@ ${activitySection}`;
15205
15344
  break;
15206
15345
  }
15207
15346
  if (contextSummary.length > maxChars) {
15208
- return contextSummary.substring(0, maxChars - 3) + "...";
15347
+ return `${contextSummary.substring(0, maxChars - 3)}...`;
15209
15348
  }
15210
15349
  return contextSummary;
15211
15350
  }
@@ -27710,7 +27849,7 @@ Use these as DOMAIN values when delegating to @sme.`;
27710
27849
  }
27711
27850
  });
27712
27851
  // src/tools/file-extractor.ts
27713
- import * as fs2 from "fs";
27852
+ import * as fs3 from "fs";
27714
27853
  import * as path4 from "path";
27715
27854
  var EXT_MAP = {
27716
27855
  python: ".py",
@@ -27773,8 +27912,8 @@ var extract_code_blocks = tool({
27773
27912
  execute: async (args) => {
27774
27913
  const { content, output_dir, prefix } = args;
27775
27914
  const targetDir = output_dir || process.cwd();
27776
- if (!fs2.existsSync(targetDir)) {
27777
- fs2.mkdirSync(targetDir, { recursive: true });
27915
+ if (!fs3.existsSync(targetDir)) {
27916
+ fs3.mkdirSync(targetDir, { recursive: true });
27778
27917
  }
27779
27918
  const pattern = /```(\w*)\n([\s\S]*?)```/g;
27780
27919
  const matches = [...content.matchAll(pattern)];
@@ -27793,12 +27932,12 @@ var extract_code_blocks = tool({
27793
27932
  const base = path4.basename(filepath, path4.extname(filepath));
27794
27933
  const ext = path4.extname(filepath);
27795
27934
  let counter = 1;
27796
- while (fs2.existsSync(filepath)) {
27935
+ while (fs3.existsSync(filepath)) {
27797
27936
  filepath = path4.join(targetDir, `${base}_${counter}${ext}`);
27798
27937
  counter++;
27799
27938
  }
27800
27939
  try {
27801
- fs2.writeFileSync(filepath, code.trim(), "utf-8");
27940
+ fs3.writeFileSync(filepath, code.trim(), "utf-8");
27802
27941
  savedFiles.push(filepath);
27803
27942
  } catch (error93) {
27804
27943
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",