opencode-swarm 4.3.2 → 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
  */
package/dist/cli/index.js CHANGED
@@ -96,6 +96,66 @@ Next steps:`);
96
96
  console.log(" what expertise is needed and requests it dynamically.");
97
97
  return 0;
98
98
  }
99
+ async function uninstall() {
100
+ try {
101
+ console.log(`\uD83D\uDC1D Uninstalling OpenCode Swarm...
102
+ `);
103
+ const opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
104
+ if (!opencodeConfig) {
105
+ if (fs.existsSync(OPENCODE_CONFIG_PATH)) {
106
+ console.log(`\u2717 Could not parse opencode config at: ${OPENCODE_CONFIG_PATH}`);
107
+ return 1;
108
+ } else {
109
+ console.log(`\u26A0 No opencode config found at: ${OPENCODE_CONFIG_PATH}`);
110
+ console.log("Nothing to uninstall.");
111
+ return 0;
112
+ }
113
+ }
114
+ if (!opencodeConfig.plugin || opencodeConfig.plugin.length === 0) {
115
+ console.log("\u26A0 opencode-swarm is not installed (no plugins configured).");
116
+ return 0;
117
+ }
118
+ const pluginName = "opencode-swarm";
119
+ const filteredPlugins = opencodeConfig.plugin.filter((p) => p !== pluginName && !p.startsWith(`${pluginName}@`));
120
+ if (filteredPlugins.length === opencodeConfig.plugin.length) {
121
+ console.log("\u26A0 opencode-swarm is not installed.");
122
+ return 0;
123
+ }
124
+ opencodeConfig.plugin = filteredPlugins;
125
+ if (opencodeConfig.agent) {
126
+ delete opencodeConfig.agent.explore;
127
+ delete opencodeConfig.agent.general;
128
+ if (Object.keys(opencodeConfig.agent).length === 0) {
129
+ delete opencodeConfig.agent;
130
+ }
131
+ }
132
+ saveJson(OPENCODE_CONFIG_PATH, opencodeConfig);
133
+ console.log("\u2713 Removed opencode-swarm from OpenCode plugins");
134
+ console.log("\u2713 Re-enabled default OpenCode agents (explore, general)");
135
+ if (process.argv.includes("--clean")) {
136
+ let cleaned = false;
137
+ if (fs.existsSync(PLUGIN_CONFIG_PATH)) {
138
+ fs.unlinkSync(PLUGIN_CONFIG_PATH);
139
+ console.log(`\u2713 Removed plugin config: ${PLUGIN_CONFIG_PATH}`);
140
+ cleaned = true;
141
+ }
142
+ if (fs.existsSync(PROMPTS_DIR)) {
143
+ fs.rmSync(PROMPTS_DIR, { recursive: true });
144
+ console.log(`\u2713 Removed custom prompts: ${PROMPTS_DIR}`);
145
+ cleaned = true;
146
+ }
147
+ if (!cleaned) {
148
+ console.log("\u2713 No config files to clean up");
149
+ }
150
+ }
151
+ console.log(`
152
+ \u2705 Uninstall complete!`);
153
+ return 0;
154
+ } catch (error) {
155
+ console.log("\u2717 Uninstall failed: " + (error instanceof Error ? error.message : String(error)));
156
+ return 1;
157
+ }
158
+ }
99
159
  function printHelp() {
100
160
  console.log(`
101
161
  opencode-swarm - Architect-centric agentic swarm plugin for OpenCode
@@ -104,8 +164,10 @@ Usage: bunx opencode-swarm [command] [OPTIONS]
104
164
 
105
165
  Commands:
106
166
  install Install and configure the plugin (default)
167
+ uninstall Remove the plugin from OpenCode config
107
168
 
108
169
  Options:
170
+ --clean Also remove config files and custom prompts (with uninstall)
109
171
  -h, --help Show this help message
110
172
 
111
173
  Configuration:
@@ -122,6 +184,8 @@ Custom Prompts:
122
184
 
123
185
  Examples:
124
186
  bunx opencode-swarm install
187
+ bunx opencode-swarm uninstall
188
+ bunx opencode-swarm uninstall --clean
125
189
  bunx opencode-swarm --help
126
190
  `);
127
191
  }
@@ -135,6 +199,9 @@ async function main() {
135
199
  if (command === "install") {
136
200
  const exitCode = await install();
137
201
  process.exit(exitCode);
202
+ } else if (command === "uninstall") {
203
+ const exitCode = await uninstall();
204
+ process.exit(exitCode);
138
205
  } else {
139
206
  console.error(`Unknown command: ${command}`);
140
207
  console.error("Run with --help for usage information");
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm config command.
3
+ * Loads and displays the current resolved plugin configuration.
4
+ */
5
+ export declare function handleConfigCommand(directory: string, _args: string[]): Promise<string>;
@@ -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>;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Handles the /swarm history command.
3
+ * Reads plan.md and displays a summary of all phases and their status.
4
+ */
5
+ export declare function handleHistoryCommand(directory: string, _args: string[]): Promise<string>;
@@ -1,6 +1,11 @@
1
1
  import type { AgentDefinition } from '../agents';
2
2
  export { handleAgentsCommand } from './agents';
3
+ export { handleConfigCommand } from './config';
4
+ export { handleDiagnoseCommand } from './diagnose';
5
+ export { handleExportCommand } from './export';
6
+ export { handleHistoryCommand } from './history';
3
7
  export { handlePlanCommand } from './plan';
8
+ export { handleResetCommand } from './reset';
4
9
  export { handleStatusCommand } from './status';
5
10
  /**
6
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) {
@@ -14410,9 +14410,46 @@ function handleAgentsCommand(agents) {
14410
14410
  `);
14411
14411
  }
14412
14412
 
14413
- // src/hooks/utils.ts
14413
+ // src/commands/config.ts
14414
+ import * as os2 from "os";
14414
14415
  import * as path2 from "path";
14416
+ function getUserConfigDir2() {
14417
+ return process.env.XDG_CONFIG_HOME || path2.join(os2.homedir(), ".config");
14418
+ }
14419
+ async function handleConfigCommand(directory, _args) {
14420
+ const config2 = loadPluginConfig(directory);
14421
+ const userConfigPath = path2.join(getUserConfigDir2(), "opencode", "opencode-swarm.json");
14422
+ const projectConfigPath = path2.join(directory, ".opencode", "opencode-swarm.json");
14423
+ const lines = [
14424
+ "## Swarm Configuration",
14425
+ "",
14426
+ "### Config Files",
14427
+ `- User: \`${userConfigPath}\``,
14428
+ `- Project: \`${projectConfigPath}\``,
14429
+ "",
14430
+ "### Resolved Config",
14431
+ "```json",
14432
+ JSON.stringify(config2, null, 2),
14433
+ "```"
14434
+ ];
14435
+ return lines.join(`
14436
+ `);
14437
+ }
14438
+
14439
+ // src/hooks/utils.ts
14440
+ import * as path3 from "path";
14415
14441
 
14442
+ // src/utils/errors.ts
14443
+ class SwarmError extends Error {
14444
+ code;
14445
+ guidance;
14446
+ constructor(message, code, guidance) {
14447
+ super(message);
14448
+ this.name = "SwarmError";
14449
+ this.code = code;
14450
+ this.guidance = guidance;
14451
+ }
14452
+ }
14416
14453
  // src/utils/logger.ts
14417
14454
  var DEBUG = process.env.OPENCODE_SWARM_DEBUG === "1";
14418
14455
  function log(message, data) {
@@ -14440,7 +14477,12 @@ function safeHook(fn) {
14440
14477
  await fn(input, output);
14441
14478
  } catch (_error) {
14442
14479
  const functionName = fn.name || "unknown";
14443
- warn(`Hook function '${functionName}' failed:`, _error);
14480
+ if (_error instanceof SwarmError) {
14481
+ warn(`Hook '${functionName}' failed: ${_error.message}
14482
+ \u2192 ${_error.guidance}`);
14483
+ } else {
14484
+ warn(`Hook function '${functionName}' failed:`, _error);
14485
+ }
14444
14486
  }
14445
14487
  };
14446
14488
  }
@@ -14462,14 +14504,14 @@ function validateSwarmPath(directory, filename) {
14462
14504
  if (/\.\.[/\\]/.test(filename)) {
14463
14505
  throw new Error("Invalid filename: path traversal detected");
14464
14506
  }
14465
- const baseDir = path2.normalize(path2.resolve(directory, ".swarm"));
14466
- const resolved = path2.normalize(path2.resolve(baseDir, filename));
14507
+ const baseDir = path3.normalize(path3.resolve(directory, ".swarm"));
14508
+ const resolved = path3.normalize(path3.resolve(baseDir, filename));
14467
14509
  if (process.platform === "win32") {
14468
- if (!resolved.toLowerCase().startsWith((baseDir + path2.sep).toLowerCase())) {
14510
+ if (!resolved.toLowerCase().startsWith((baseDir + path3.sep).toLowerCase())) {
14469
14511
  throw new Error("Invalid filename: path escapes .swarm directory");
14470
14512
  }
14471
14513
  } else {
14472
- if (!resolved.startsWith(baseDir + path2.sep)) {
14514
+ if (!resolved.startsWith(baseDir + path3.sep)) {
14473
14515
  throw new Error("Invalid filename: path escapes .swarm directory");
14474
14516
  }
14475
14517
  }
@@ -14492,6 +14534,143 @@ function estimateTokens(text) {
14492
14534
  return Math.ceil(text.length * 0.33);
14493
14535
  }
14494
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
+
14623
+ // src/commands/history.ts
14624
+ async function handleHistoryCommand(directory, _args) {
14625
+ const planContent = await readSwarmFileAsync(directory, "plan.md");
14626
+ if (!planContent) {
14627
+ return "No history available.";
14628
+ }
14629
+ const phaseRegex = /^## Phase (\d+):?\s*(.+?)(?:\s*\[(COMPLETE|IN PROGRESS|PENDING)\])?\s*$/gm;
14630
+ const phases = [];
14631
+ const lines = planContent.split(`
14632
+ `);
14633
+ for (let match = phaseRegex.exec(planContent);match !== null; match = phaseRegex.exec(planContent)) {
14634
+ const num = parseInt(match[1], 10);
14635
+ const name = match[2].trim();
14636
+ const status = match[3] || "PENDING";
14637
+ const headerLineIndex = lines.indexOf(match[0]);
14638
+ let completed = 0;
14639
+ let total = 0;
14640
+ if (headerLineIndex !== -1) {
14641
+ for (let i = headerLineIndex + 1;i < lines.length; i++) {
14642
+ const line = lines[i];
14643
+ if (/^## Phase \d+/.test(line) || line.trim() === "---" && total > 0) {
14644
+ break;
14645
+ }
14646
+ if (/^- \[x\]/.test(line)) {
14647
+ completed++;
14648
+ total++;
14649
+ } else if (/^- \[ \]/.test(line)) {
14650
+ total++;
14651
+ }
14652
+ }
14653
+ }
14654
+ phases.push({ num, name, status, completed, total });
14655
+ }
14656
+ if (phases.length === 0) {
14657
+ return "No history available.";
14658
+ }
14659
+ const tableLines = [
14660
+ "## Swarm History",
14661
+ "",
14662
+ "| Phase | Name | Status | Tasks |",
14663
+ "|-------|------|--------|-------|"
14664
+ ];
14665
+ for (const phase of phases) {
14666
+ const statusIcon = phase.status === "COMPLETE" ? "\u2705" : phase.status === "IN PROGRESS" ? "\uD83D\uDD04" : "\u23F3";
14667
+ const tasks = phase.total > 0 ? `${phase.completed}/${phase.total}` : "-";
14668
+ tableLines.push(`| ${phase.num} | ${phase.name} | ${statusIcon} ${phase.status} | ${tasks} |`);
14669
+ }
14670
+ return tableLines.join(`
14671
+ `);
14672
+ }
14673
+
14495
14674
  // src/commands/plan.ts
14496
14675
  async function handlePlanCommand(directory, args) {
14497
14676
  const planContent = await readSwarmFileAsync(directory, "plan.md");
@@ -14535,6 +14714,47 @@ async function handlePlanCommand(directory, args) {
14535
14714
  `).trim();
14536
14715
  }
14537
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
+
14538
14758
  // src/hooks/extractors.ts
14539
14759
  function extractCurrentPhase(planContent) {
14540
14760
  if (!planContent) {
@@ -14709,7 +14929,12 @@ var HELP_TEXT = [
14709
14929
  "",
14710
14930
  "- `/swarm status` \u2014 Show current swarm state",
14711
14931
  "- `/swarm plan [phase]` \u2014 Show plan (optionally filter by phase number)",
14712
- "- `/swarm agents` \u2014 List registered agents"
14932
+ "- `/swarm agents` \u2014 List registered agents",
14933
+ "- `/swarm history` \u2014 Show completed phases summary",
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"
14713
14938
  ].join(`
14714
14939
  `);
14715
14940
  function createSwarmCommandHandler(directory, agents) {
@@ -14730,6 +14955,21 @@ function createSwarmCommandHandler(directory, agents) {
14730
14955
  case "agents":
14731
14956
  text = handleAgentsCommand(agents);
14732
14957
  break;
14958
+ case "history":
14959
+ text = await handleHistoryCommand(directory, args);
14960
+ break;
14961
+ case "config":
14962
+ text = await handleConfigCommand(directory, args);
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;
14733
14973
  default:
14734
14974
  text = HELP_TEXT;
14735
14975
  break;
@@ -14817,8 +15057,8 @@ async function doFlush(directory) {
14817
15057
  const activitySection = renderActivitySection();
14818
15058
  const updated = replaceOrAppendSection(existing, "## Agent Activity", activitySection);
14819
15059
  const flushedCount = swarmState.pendingEvents;
14820
- const path3 = `${directory}/.swarm/context.md`;
14821
- await Bun.write(path3, updated);
15060
+ const path4 = `${directory}/.swarm/context.md`;
15061
+ await Bun.write(path4, updated);
14822
15062
  swarmState.pendingEvents = Math.max(0, swarmState.pendingEvents - flushedCount);
14823
15063
  } catch (error49) {
14824
15064
  warn("Agent activity flush failed:", error49);
@@ -14844,19 +15084,19 @@ function renderActivitySection() {
14844
15084
  function replaceOrAppendSection(content, heading, newSection) {
14845
15085
  const headingIndex = content.indexOf(heading);
14846
15086
  if (headingIndex === -1) {
14847
- return content.trimEnd() + `
15087
+ return `${content.trimEnd()}
14848
15088
 
14849
- ` + newSection + `
15089
+ ${newSection}
14850
15090
  `;
14851
15091
  }
14852
15092
  const afterHeading = content.substring(headingIndex + heading.length);
14853
15093
  const nextHeadingMatch = afterHeading.match(/\n## /);
14854
15094
  if (nextHeadingMatch && nextHeadingMatch.index !== undefined) {
14855
15095
  const endIndex = headingIndex + heading.length + nextHeadingMatch.index;
14856
- return content.substring(0, headingIndex) + newSection + `
14857
- ` + content.substring(endIndex + 1);
15096
+ return `${content.substring(0, headingIndex)}${newSection}
15097
+ ${content.substring(endIndex + 1)}`;
14858
15098
  }
14859
- return content.substring(0, headingIndex) + newSection + `
15099
+ return `${content.substring(0, headingIndex)}${newSection}
14860
15100
  `;
14861
15101
  }
14862
15102
  // src/hooks/compaction-customizer.ts
@@ -15104,7 +15344,7 @@ ${activitySection}`;
15104
15344
  break;
15105
15345
  }
15106
15346
  if (contextSummary.length > maxChars) {
15107
- return contextSummary.substring(0, maxChars - 3) + "...";
15347
+ return `${contextSummary.substring(0, maxChars - 3)}...`;
15108
15348
  }
15109
15349
  return contextSummary;
15110
15350
  }
@@ -15837,10 +16077,10 @@ function mergeDefs2(...defs) {
15837
16077
  function cloneDef2(schema) {
15838
16078
  return mergeDefs2(schema._zod.def);
15839
16079
  }
15840
- function getElementAtPath2(obj, path3) {
15841
- if (!path3)
16080
+ function getElementAtPath2(obj, path4) {
16081
+ if (!path4)
15842
16082
  return obj;
15843
- return path3.reduce((acc, key) => acc?.[key], obj);
16083
+ return path4.reduce((acc, key) => acc?.[key], obj);
15844
16084
  }
15845
16085
  function promiseAllObject2(promisesObj) {
15846
16086
  const keys = Object.keys(promisesObj);
@@ -16199,11 +16439,11 @@ function aborted2(x, startIndex = 0) {
16199
16439
  }
16200
16440
  return false;
16201
16441
  }
16202
- function prefixIssues2(path3, issues) {
16442
+ function prefixIssues2(path4, issues) {
16203
16443
  return issues.map((iss) => {
16204
16444
  var _a2;
16205
16445
  (_a2 = iss).path ?? (_a2.path = []);
16206
- iss.path.unshift(path3);
16446
+ iss.path.unshift(path4);
16207
16447
  return iss;
16208
16448
  });
16209
16449
  }
@@ -16371,7 +16611,7 @@ function treeifyError2(error49, _mapper) {
16371
16611
  return issue3.message;
16372
16612
  };
16373
16613
  const result = { errors: [] };
16374
- const processError = (error50, path3 = []) => {
16614
+ const processError = (error50, path4 = []) => {
16375
16615
  var _a2, _b;
16376
16616
  for (const issue3 of error50.issues) {
16377
16617
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -16381,7 +16621,7 @@ function treeifyError2(error49, _mapper) {
16381
16621
  } else if (issue3.code === "invalid_element") {
16382
16622
  processError({ issues: issue3.issues }, issue3.path);
16383
16623
  } else {
16384
- const fullpath = [...path3, ...issue3.path];
16624
+ const fullpath = [...path4, ...issue3.path];
16385
16625
  if (fullpath.length === 0) {
16386
16626
  result.errors.push(mapper(issue3));
16387
16627
  continue;
@@ -16413,8 +16653,8 @@ function treeifyError2(error49, _mapper) {
16413
16653
  }
16414
16654
  function toDotPath2(_path) {
16415
16655
  const segs = [];
16416
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16417
- for (const seg of path3) {
16656
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16657
+ for (const seg of path4) {
16418
16658
  if (typeof seg === "number")
16419
16659
  segs.push(`[${seg}]`);
16420
16660
  else if (typeof seg === "symbol")
@@ -27609,8 +27849,8 @@ Use these as DOMAIN values when delegating to @sme.`;
27609
27849
  }
27610
27850
  });
27611
27851
  // src/tools/file-extractor.ts
27612
- import * as fs2 from "fs";
27613
- import * as path3 from "path";
27852
+ import * as fs3 from "fs";
27853
+ import * as path4 from "path";
27614
27854
  var EXT_MAP = {
27615
27855
  python: ".py",
27616
27856
  py: ".py",
@@ -27672,8 +27912,8 @@ var extract_code_blocks = tool({
27672
27912
  execute: async (args) => {
27673
27913
  const { content, output_dir, prefix } = args;
27674
27914
  const targetDir = output_dir || process.cwd();
27675
- if (!fs2.existsSync(targetDir)) {
27676
- fs2.mkdirSync(targetDir, { recursive: true });
27915
+ if (!fs3.existsSync(targetDir)) {
27916
+ fs3.mkdirSync(targetDir, { recursive: true });
27677
27917
  }
27678
27918
  const pattern = /```(\w*)\n([\s\S]*?)```/g;
27679
27919
  const matches = [...content.matchAll(pattern)];
@@ -27688,16 +27928,16 @@ var extract_code_blocks = tool({
27688
27928
  if (prefix) {
27689
27929
  filename = `${prefix}_${filename}`;
27690
27930
  }
27691
- let filepath = path3.join(targetDir, filename);
27692
- const base = path3.basename(filepath, path3.extname(filepath));
27693
- const ext = path3.extname(filepath);
27931
+ let filepath = path4.join(targetDir, filename);
27932
+ const base = path4.basename(filepath, path4.extname(filepath));
27933
+ const ext = path4.extname(filepath);
27694
27934
  let counter = 1;
27695
- while (fs2.existsSync(filepath)) {
27696
- filepath = path3.join(targetDir, `${base}_${counter}${ext}`);
27935
+ while (fs3.existsSync(filepath)) {
27936
+ filepath = path4.join(targetDir, `${base}_${counter}${ext}`);
27697
27937
  counter++;
27698
27938
  }
27699
27939
  try {
27700
- fs2.writeFileSync(filepath, code.trim(), "utf-8");
27940
+ fs3.writeFileSync(filepath, code.trim(), "utf-8");
27701
27941
  savedFiles.push(filepath);
27702
27942
  } catch (error93) {
27703
27943
  errors5.push(`Failed to save ${filename}: ${error93 instanceof Error ? error93.message : String(error93)}`);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Base error class for all swarm errors.
3
+ * Includes a machine-readable `code` and a user-facing `guidance` string.
4
+ */
5
+ export declare class SwarmError extends Error {
6
+ readonly code: string;
7
+ readonly guidance: string;
8
+ constructor(message: string, code: string, guidance: string);
9
+ }
10
+ /**
11
+ * Error thrown when configuration loading or validation fails.
12
+ */
13
+ export declare class ConfigError extends SwarmError {
14
+ constructor(message: string, guidance: string);
15
+ }
16
+ /**
17
+ * Error thrown when a hook execution fails.
18
+ */
19
+ export declare class HookError extends SwarmError {
20
+ constructor(message: string, guidance: string);
21
+ }
22
+ /**
23
+ * Error thrown when a tool execution fails.
24
+ */
25
+ export declare class ToolError extends SwarmError {
26
+ constructor(message: string, guidance: string);
27
+ }
28
+ /**
29
+ * Error thrown when CLI operations fail.
30
+ */
31
+ export declare class CLIError extends SwarmError {
32
+ constructor(message: string, guidance: string);
33
+ }
@@ -1 +1,2 @@
1
- export { log, warn, error } from './logger';
1
+ export { CLIError, ConfigError, HookError, SwarmError, ToolError, } from './errors';
2
+ export { error, log, warn } from './logger';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "4.3.2",
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",
@@ -35,12 +35,12 @@
35
35
  "prepublishOnly": "bun run build"
36
36
  },
37
37
  "dependencies": {
38
- "@opencode-ai/plugin": "^1.1.19",
39
- "@opencode-ai/sdk": "^1.1.19",
38
+ "@opencode-ai/plugin": "^1.1.53",
39
+ "@opencode-ai/sdk": "^1.1.53",
40
40
  "zod": "^4.1.8"
41
41
  },
42
42
  "devDependencies": {
43
- "@biomejs/biome": "2.3.11",
43
+ "@biomejs/biome": "2.3.14",
44
44
  "bun-types": "latest",
45
45
  "typescript": "^5.7.3"
46
46
  }