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 +65 -25
- package/dist/agents/index.d.ts +6 -0
- package/dist/cli/index.js +67 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/diagnose.d.ts +5 -0
- package/dist/commands/export.d.ts +5 -0
- package/dist/commands/history.d.ts +5 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/reset.d.ts +6 -0
- package/dist/index.js +286 -46
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/index.d.ts +2 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://img.shields.io/badge/version-4.
|
|
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-
|
|
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
|
-
#
|
|
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
|
|
332
|
+
## What's New
|
|
328
333
|
|
|
329
|
-
###
|
|
330
|
-
-
|
|
331
|
-
-
|
|
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
|
-
###
|
|
334
|
-
- **
|
|
335
|
-
- **
|
|
336
|
-
-
|
|
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
|
-
###
|
|
339
|
-
-
|
|
340
|
-
-
|
|
341
|
-
-
|
|
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
|
-
###
|
|
344
|
-
- **
|
|
345
|
-
- **
|
|
346
|
-
- **
|
|
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
|
-
|
|
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
|
|
package/dist/agents/index.d.ts
CHANGED
|
@@ -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");
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -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.
|
package/dist/index.js
CHANGED
|
@@ -14246,28 +14246,28 @@ ${customAppendPrompt}`;
|
|
|
14246
14246
|
}
|
|
14247
14247
|
|
|
14248
14248
|
// src/agents/index.ts
|
|
14249
|
-
function
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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 =
|
|
14466
|
-
const resolved =
|
|
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 +
|
|
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 +
|
|
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
|
|
14821
|
-
await Bun.write(
|
|
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
|
-
|
|
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)
|
|
14857
|
-
|
|
15096
|
+
return `${content.substring(0, headingIndex)}${newSection}
|
|
15097
|
+
${content.substring(endIndex + 1)}`;
|
|
14858
15098
|
}
|
|
14859
|
-
return content.substring(0, headingIndex)
|
|
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,
|
|
15841
|
-
if (!
|
|
16080
|
+
function getElementAtPath2(obj, path4) {
|
|
16081
|
+
if (!path4)
|
|
15842
16082
|
return obj;
|
|
15843
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
16417
|
-
for (const seg of
|
|
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
|
|
27613
|
-
import * as
|
|
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 (!
|
|
27676
|
-
|
|
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 =
|
|
27692
|
-
const base =
|
|
27693
|
-
const ext =
|
|
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 (
|
|
27696
|
-
filepath =
|
|
27935
|
+
while (fs3.existsSync(filepath)) {
|
|
27936
|
+
filepath = path4.join(targetDir, `${base}_${counter}${ext}`);
|
|
27697
27937
|
counter++;
|
|
27698
27938
|
}
|
|
27699
27939
|
try {
|
|
27700
|
-
|
|
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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
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
|
+
"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.
|
|
39
|
-
"@opencode-ai/sdk": "^1.1.
|
|
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.
|
|
43
|
+
"@biomejs/biome": "2.3.14",
|
|
44
44
|
"bun-types": "latest",
|
|
45
45
|
"typescript": "^5.7.3"
|
|
46
46
|
}
|