opencode-swarm 6.19.3 → 6.19.5
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 +35 -0
- package/dist/cli/index.js +7 -4
- package/dist/index.js +94 -39
- package/dist/tools/save-plan.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Your AI writes the code. Swarm makes sure it actually works.**
|
|
4
4
|
|
|
5
|
+
https://swarmai.site/
|
|
6
|
+
|
|
5
7
|
OpenCode Swarm is a plugin for [OpenCode](https://opencode.ai) that turns a single AI coding agent into a team of nine. One agent writes the code. A different agent reviews it. Another writes and runs tests. Another catches security issues. Nothing ships until every check passes. Your project state is saved to disk, so you can close your laptop, come back tomorrow, and pick up exactly where you left off.
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -321,6 +323,39 @@ Every completed task writes structured evidence to `.swarm/evidence/`:
|
|
|
321
323
|
|
|
322
324
|
</details>
|
|
323
325
|
|
|
326
|
+
<details>
|
|
327
|
+
<summary><strong>Save Plan Tool: Target Workspace Requirement</strong></summary>
|
|
328
|
+
|
|
329
|
+
The `save_plan` tool requires an explicit target workspace path. It does **not** fall back to `process.cwd()`.
|
|
330
|
+
|
|
331
|
+
### Explicit Workspace Requirement
|
|
332
|
+
|
|
333
|
+
- The `working_directory` parameter must be provided
|
|
334
|
+
- Providing no value or relying on implicit directory resolution will result in deterministic failure
|
|
335
|
+
|
|
336
|
+
### Failure Conditions
|
|
337
|
+
|
|
338
|
+
| Condition | Behavior |
|
|
339
|
+
|-----------|----------|
|
|
340
|
+
| Missing (`undefined` / `null`) | Fails with: "Target workspace is required" |
|
|
341
|
+
| Empty or whitespace-only | Fails with: "Target workspace cannot be empty or whitespace" |
|
|
342
|
+
| Path traversal (`..`) | Fails with: "Target workspace cannot contain path traversal" |
|
|
343
|
+
|
|
344
|
+
### Usage Contract
|
|
345
|
+
|
|
346
|
+
When using `save_plan`, always pass a valid `working_directory`:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
save_plan({
|
|
350
|
+
title: "My Project",
|
|
351
|
+
swarm_id: "mega",
|
|
352
|
+
phases: [{ id: 1, name: "Setup", tasks: [{ id: "1.1", description: "Initialize project" }] }],
|
|
353
|
+
working_directory: "/path/to/project" // Required - no fallback
|
|
354
|
+
})
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
</details>
|
|
358
|
+
|
|
324
359
|
<details>
|
|
325
360
|
<summary><strong>Guardrails and Circuit Breakers</strong></summary>
|
|
326
361
|
|
package/dist/cli/index.js
CHANGED
|
@@ -16160,6 +16160,9 @@ async function loadPlan(directory) {
|
|
|
16160
16160
|
return null;
|
|
16161
16161
|
}
|
|
16162
16162
|
async function savePlan(directory, plan) {
|
|
16163
|
+
if (directory === null || directory === undefined || typeof directory !== "string" || directory.trim().length === 0) {
|
|
16164
|
+
throw new Error(`Invalid directory: directory must be a non-empty string`);
|
|
16165
|
+
}
|
|
16163
16166
|
const validated = PlanSchema.parse(plan);
|
|
16164
16167
|
const swarmDir = path6.resolve(directory, ".swarm");
|
|
16165
16168
|
const planPath = path6.join(swarmDir, "plan.json");
|
|
@@ -31634,14 +31637,14 @@ function parseContextMd(content) {
|
|
|
31634
31637
|
function splitIntoSections(content) {
|
|
31635
31638
|
const sections = [];
|
|
31636
31639
|
const headingRegex = /^(#{1,3})\s+(.+)/gm;
|
|
31637
|
-
const lastIndex = 0;
|
|
31638
|
-
let match;
|
|
31639
31640
|
const matches = [];
|
|
31640
|
-
|
|
31641
|
+
let match = headingRegex.exec(content);
|
|
31642
|
+
while (match !== null) {
|
|
31641
31643
|
matches.push({
|
|
31642
31644
|
index: match.index,
|
|
31643
31645
|
heading: match[0]
|
|
31644
31646
|
});
|
|
31647
|
+
match = headingRegex.exec(content);
|
|
31645
31648
|
}
|
|
31646
31649
|
for (let i = 0;i < matches.length; i++) {
|
|
31647
31650
|
const current = matches[i];
|
|
@@ -31691,7 +31694,7 @@ function inferCategoryFromText(text) {
|
|
|
31691
31694
|
function truncateLesson(text) {
|
|
31692
31695
|
if (text.length <= 280)
|
|
31693
31696
|
return text;
|
|
31694
|
-
return text.slice(0, 277)
|
|
31697
|
+
return `${text.slice(0, 277)}...`;
|
|
31695
31698
|
}
|
|
31696
31699
|
function inferProjectName(directory) {
|
|
31697
31700
|
const packageJsonPath = path8.join(directory, "package.json");
|
package/dist/index.js
CHANGED
|
@@ -14700,6 +14700,9 @@ async function loadPlan(directory) {
|
|
|
14700
14700
|
return null;
|
|
14701
14701
|
}
|
|
14702
14702
|
async function savePlan(directory, plan) {
|
|
14703
|
+
if (directory === null || directory === undefined || typeof directory !== "string" || directory.trim().length === 0) {
|
|
14704
|
+
throw new Error(`Invalid directory: directory must be a non-empty string`);
|
|
14705
|
+
}
|
|
14703
14706
|
const validated = PlanSchema.parse(plan);
|
|
14704
14707
|
const swarmDir = path4.resolve(directory, ".swarm");
|
|
14705
14708
|
const planPath = path4.join(swarmDir, "plan.json");
|
|
@@ -43139,14 +43142,14 @@ function parseContextMd(content) {
|
|
|
43139
43142
|
function splitIntoSections(content) {
|
|
43140
43143
|
const sections = [];
|
|
43141
43144
|
const headingRegex = /^(#{1,3})\s+(.+)/gm;
|
|
43142
|
-
const lastIndex = 0;
|
|
43143
|
-
let match;
|
|
43144
43145
|
const matches = [];
|
|
43145
|
-
|
|
43146
|
+
let match = headingRegex.exec(content);
|
|
43147
|
+
while (match !== null) {
|
|
43146
43148
|
matches.push({
|
|
43147
43149
|
index: match.index,
|
|
43148
43150
|
heading: match[0]
|
|
43149
43151
|
});
|
|
43152
|
+
match = headingRegex.exec(content);
|
|
43150
43153
|
}
|
|
43151
43154
|
for (let i2 = 0;i2 < matches.length; i2++) {
|
|
43152
43155
|
const current = matches[i2];
|
|
@@ -43196,7 +43199,7 @@ function inferCategoryFromText(text) {
|
|
|
43196
43199
|
function truncateLesson(text) {
|
|
43197
43200
|
if (text.length <= 280)
|
|
43198
43201
|
return text;
|
|
43199
|
-
return text.slice(0, 277)
|
|
43202
|
+
return `${text.slice(0, 277)}...`;
|
|
43200
43203
|
}
|
|
43201
43204
|
function inferProjectName(directory) {
|
|
43202
43205
|
const packageJsonPath = path12.join(directory, "package.json");
|
|
@@ -45795,11 +45798,10 @@ function createGuardrailsHooks(directory, config3) {
|
|
|
45795
45798
|
const patchPathPattern = /\*\*\*\s+(?:Update|Add|Delete)\s+File:\s*(.+)/gi;
|
|
45796
45799
|
const diffPathPattern = /\+\+\+\s+b\/(.+)/gm;
|
|
45797
45800
|
const paths = new Set;
|
|
45798
|
-
|
|
45799
|
-
while ((match = patchPathPattern.exec(patchText)) !== null) {
|
|
45801
|
+
for (const match of patchText.matchAll(patchPathPattern)) {
|
|
45800
45802
|
paths.add(match[1].trim());
|
|
45801
45803
|
}
|
|
45802
|
-
|
|
45804
|
+
for (const match of patchText.matchAll(diffPathPattern)) {
|
|
45803
45805
|
const p = match[1].trim();
|
|
45804
45806
|
if (p !== "/dev/null")
|
|
45805
45807
|
paths.add(p);
|
|
@@ -48356,7 +48358,10 @@ function formatStars(confidence) {
|
|
|
48356
48358
|
return "\u2605\u2606\u2606";
|
|
48357
48359
|
}
|
|
48358
48360
|
function sanitizeLessonForContext(text) {
|
|
48359
|
-
return text.
|
|
48361
|
+
return text.split("").filter((char) => {
|
|
48362
|
+
const code = char.charCodeAt(0);
|
|
48363
|
+
return code === 9 || code === 10 || code === 13 || code > 31 && code !== 127;
|
|
48364
|
+
}).join("").replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/[\u202A-\u202E\u2066-\u2069]/g, "").replace(/```/g, "` ` `").replace(/^system\s*:/gim, "[BLOCKED]:");
|
|
48360
48365
|
}
|
|
48361
48366
|
function isOrchestratorAgent(agentName) {
|
|
48362
48367
|
const stripped = stripKnownSwarmPrefix(agentName);
|
|
@@ -50549,25 +50554,50 @@ import * as path30 from "path";
|
|
|
50549
50554
|
init_manager();
|
|
50550
50555
|
init_utils2();
|
|
50551
50556
|
init_create_tool();
|
|
50552
|
-
function
|
|
50553
|
-
const chain = swarmState.delegationChains.get(sessionID);
|
|
50554
|
-
if (!chain) {
|
|
50555
|
-
return [];
|
|
50556
|
-
}
|
|
50557
|
-
if (sinceTimestamp === 0) {
|
|
50558
|
-
return chain;
|
|
50559
|
-
}
|
|
50560
|
-
return chain.filter((entry) => entry.timestamp > sinceTimestamp);
|
|
50561
|
-
}
|
|
50562
|
-
function normalizeAgentsFromDelegations(delegations) {
|
|
50557
|
+
function collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, callerSessionId) {
|
|
50563
50558
|
const agents = new Set;
|
|
50564
|
-
|
|
50565
|
-
|
|
50566
|
-
|
|
50567
|
-
|
|
50568
|
-
|
|
50559
|
+
const contributorSessionIds = [];
|
|
50560
|
+
const callerSession = swarmState.agentSessions.get(callerSessionId);
|
|
50561
|
+
if (callerSession) {
|
|
50562
|
+
contributorSessionIds.push(callerSessionId);
|
|
50563
|
+
if (callerSession.phaseAgentsDispatched) {
|
|
50564
|
+
for (const agent of callerSession.phaseAgentsDispatched) {
|
|
50565
|
+
agents.add(agent);
|
|
50566
|
+
}
|
|
50567
|
+
}
|
|
50568
|
+
const callerDelegations = swarmState.delegationChains.get(callerSessionId);
|
|
50569
|
+
if (callerDelegations) {
|
|
50570
|
+
for (const delegation of callerDelegations) {
|
|
50571
|
+
agents.add(stripKnownSwarmPrefix(delegation.from));
|
|
50572
|
+
agents.add(stripKnownSwarmPrefix(delegation.to));
|
|
50573
|
+
}
|
|
50574
|
+
}
|
|
50569
50575
|
}
|
|
50570
|
-
|
|
50576
|
+
for (const [sessionId, session] of swarmState.agentSessions) {
|
|
50577
|
+
if (sessionId === callerSessionId) {
|
|
50578
|
+
continue;
|
|
50579
|
+
}
|
|
50580
|
+
const hasRecentToolCalls = session.lastToolCallTime >= phaseReferenceTimestamp;
|
|
50581
|
+
const delegations = swarmState.delegationChains.get(sessionId);
|
|
50582
|
+
const hasRecentDelegations = delegations?.some((d) => d.timestamp >= phaseReferenceTimestamp) ?? false;
|
|
50583
|
+
const hasActivity = hasRecentToolCalls || hasRecentDelegations;
|
|
50584
|
+
if (hasActivity) {
|
|
50585
|
+
contributorSessionIds.push(sessionId);
|
|
50586
|
+
if (session.phaseAgentsDispatched) {
|
|
50587
|
+
for (const agent of session.phaseAgentsDispatched) {
|
|
50588
|
+
agents.add(agent);
|
|
50589
|
+
}
|
|
50590
|
+
}
|
|
50591
|
+
const delegations2 = swarmState.delegationChains.get(sessionId);
|
|
50592
|
+
if (delegations2) {
|
|
50593
|
+
for (const delegation of delegations2) {
|
|
50594
|
+
agents.add(stripKnownSwarmPrefix(delegation.from));
|
|
50595
|
+
agents.add(stripKnownSwarmPrefix(delegation.to));
|
|
50596
|
+
}
|
|
50597
|
+
}
|
|
50598
|
+
}
|
|
50599
|
+
}
|
|
50600
|
+
return { agents, contributorSessionIds };
|
|
50571
50601
|
}
|
|
50572
50602
|
function isValidRetroEntry(entry, phase) {
|
|
50573
50603
|
return entry.type === "retrospective" && "phase_number" in entry && entry.phase_number === phase && "verdict" in entry && entry.verdict === "pass";
|
|
@@ -50597,12 +50627,9 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50597
50627
|
}, null, 2);
|
|
50598
50628
|
}
|
|
50599
50629
|
const session = ensureAgentSession(sessionID);
|
|
50600
|
-
const
|
|
50601
|
-
const
|
|
50602
|
-
const
|
|
50603
|
-
const trackedAgents = session.phaseAgentsDispatched ?? new Set;
|
|
50604
|
-
const allAgents = new Set([...delegationAgents, ...trackedAgents]);
|
|
50605
|
-
const agentsDispatched = Array.from(allAgents).sort();
|
|
50630
|
+
const phaseReferenceTimestamp = session.lastPhaseCompleteTimestamp ?? 0;
|
|
50631
|
+
const crossSessionResult = collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, sessionID);
|
|
50632
|
+
const agentsDispatched = Array.from(crossSessionResult.agents).sort();
|
|
50606
50633
|
const dir = workingDirectory ?? process.cwd();
|
|
50607
50634
|
const { config: config3 } = loadPluginConfigWithMeta(dir);
|
|
50608
50635
|
let phaseCompleteConfig;
|
|
@@ -50668,7 +50695,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50668
50695
|
status: "blocked",
|
|
50669
50696
|
reason: "RETROSPECTIVE_MISSING",
|
|
50670
50697
|
message: `Phase ${phase} cannot be completed: no valid retrospective evidence found.${schemaErrorDetail} Write a retrospective bundle at .swarm/evidence/retro-${phase}/evidence.json before calling phase_complete.`,
|
|
50671
|
-
agentsDispatched
|
|
50698
|
+
agentsDispatched,
|
|
50672
50699
|
agentsMissing: [],
|
|
50673
50700
|
warnings: [
|
|
50674
50701
|
`Retrospective missing for phase ${phase}.${schemaErrorDetail} Use this template:`,
|
|
@@ -50731,7 +50758,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50731
50758
|
if (phaseCompleteConfig.require_docs && !effectiveRequired.includes("docs")) {
|
|
50732
50759
|
effectiveRequired.push("docs");
|
|
50733
50760
|
}
|
|
50734
|
-
const agentsMissing = effectiveRequired.filter((req) => !
|
|
50761
|
+
const agentsMissing = effectiveRequired.filter((req) => !crossSessionResult.agents.has(req));
|
|
50735
50762
|
const warnings = [];
|
|
50736
50763
|
let success3 = true;
|
|
50737
50764
|
let status = "success";
|
|
@@ -50748,7 +50775,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50748
50775
|
}
|
|
50749
50776
|
}
|
|
50750
50777
|
const now = Date.now();
|
|
50751
|
-
const durationMs = now -
|
|
50778
|
+
const durationMs = now - phaseReferenceTimestamp;
|
|
50752
50779
|
const event = {
|
|
50753
50780
|
event: "phase_complete",
|
|
50754
50781
|
phase,
|
|
@@ -50766,9 +50793,14 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50766
50793
|
warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
|
|
50767
50794
|
}
|
|
50768
50795
|
if (success3) {
|
|
50769
|
-
|
|
50770
|
-
|
|
50771
|
-
|
|
50796
|
+
for (const contributorSessionId of crossSessionResult.contributorSessionIds) {
|
|
50797
|
+
const contributorSession = swarmState.agentSessions.get(contributorSessionId);
|
|
50798
|
+
if (contributorSession) {
|
|
50799
|
+
contributorSession.phaseAgentsDispatched = new Set;
|
|
50800
|
+
contributorSession.lastPhaseCompleteTimestamp = now;
|
|
50801
|
+
contributorSession.lastPhaseCompletePhase = phase;
|
|
50802
|
+
}
|
|
50803
|
+
}
|
|
50772
50804
|
}
|
|
50773
50805
|
const result = {
|
|
50774
50806
|
success: success3,
|
|
@@ -54175,6 +54207,20 @@ function detectPlaceholderContent(args2) {
|
|
|
54175
54207
|
}
|
|
54176
54208
|
return issues;
|
|
54177
54209
|
}
|
|
54210
|
+
function validateTargetWorkspace(target, source) {
|
|
54211
|
+
if (target === undefined || target === null) {
|
|
54212
|
+
return `Target workspace is required: ${source} not provided`;
|
|
54213
|
+
}
|
|
54214
|
+
const trimmed = target.trim();
|
|
54215
|
+
if (trimmed.length === 0) {
|
|
54216
|
+
return `Target workspace cannot be empty or whitespace: ${source}`;
|
|
54217
|
+
}
|
|
54218
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
54219
|
+
if (normalized.includes("..")) {
|
|
54220
|
+
return `Target workspace cannot contain path traversal: ${source} contains ".."`;
|
|
54221
|
+
}
|
|
54222
|
+
return;
|
|
54223
|
+
}
|
|
54178
54224
|
async function executeSavePlan(args2, fallbackDir) {
|
|
54179
54225
|
const placeholderIssues = detectPlaceholderContent(args2);
|
|
54180
54226
|
if (placeholderIssues.length > 0) {
|
|
@@ -54184,6 +54230,15 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
54184
54230
|
errors: placeholderIssues
|
|
54185
54231
|
};
|
|
54186
54232
|
}
|
|
54233
|
+
const targetWorkspace = args2.working_directory ?? fallbackDir;
|
|
54234
|
+
const workspaceError = validateTargetWorkspace(targetWorkspace, args2.working_directory ? "working_directory" : "fallbackDir");
|
|
54235
|
+
if (workspaceError) {
|
|
54236
|
+
return {
|
|
54237
|
+
success: false,
|
|
54238
|
+
message: "Target workspace validation failed",
|
|
54239
|
+
errors: [workspaceError]
|
|
54240
|
+
};
|
|
54241
|
+
}
|
|
54187
54242
|
const plan = {
|
|
54188
54243
|
schema_version: "1.0.0",
|
|
54189
54244
|
title: args2.title,
|
|
@@ -54211,7 +54266,7 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
54211
54266
|
})
|
|
54212
54267
|
};
|
|
54213
54268
|
const tasksCount = plan.phases.reduce((acc, phase) => acc + phase.tasks.length, 0);
|
|
54214
|
-
const dir =
|
|
54269
|
+
const dir = targetWorkspace;
|
|
54215
54270
|
try {
|
|
54216
54271
|
await savePlan(dir, plan);
|
|
54217
54272
|
return {
|
|
@@ -54245,7 +54300,7 @@ var save_plan = createSwarmTool({
|
|
|
54245
54300
|
acceptance: tool.schema.string().optional().describe("Acceptance criteria for this task")
|
|
54246
54301
|
})).min(1).describe("Tasks in this phase")
|
|
54247
54302
|
})).min(1).describe("Implementation phases"),
|
|
54248
|
-
working_directory: tool.schema.string().optional().describe("Working directory (
|
|
54303
|
+
working_directory: tool.schema.string().optional().describe("Working directory (explicit path, required - no fallback)")
|
|
54249
54304
|
},
|
|
54250
54305
|
execute: async (args2, _directory) => {
|
|
54251
54306
|
return JSON.stringify(await executeSavePlan(args2, _directory), null, 2);
|
|
@@ -41,6 +41,14 @@ export interface SavePlanResult {
|
|
|
41
41
|
* @returns Array of issue strings describing found placeholders
|
|
42
42
|
*/
|
|
43
43
|
export declare function detectPlaceholderContent(args: SavePlanArgs): string[];
|
|
44
|
+
/**
|
|
45
|
+
* Validate target workspace path.
|
|
46
|
+
* Rejects missing, empty, whitespace-only, and traversal-style paths.
|
|
47
|
+
* @param target - The target workspace path to validate
|
|
48
|
+
* @param source - Description of the source (for error messages)
|
|
49
|
+
* @returns Error message if invalid, undefined if valid
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateTargetWorkspace(target: string | undefined, source: string): string | undefined;
|
|
44
52
|
/**
|
|
45
53
|
* Execute the save_plan tool.
|
|
46
54
|
* Validates for placeholder content, builds a Plan object, and saves to disk.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-swarm",
|
|
3
|
-
"version": "6.19.
|
|
3
|
+
"version": "6.19.5",
|
|
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",
|