opencode-swarm 6.19.4 → 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 +3 -0
- package/dist/index.js +84 -31
- 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");
|
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");
|
|
@@ -50551,25 +50554,50 @@ import * as path30 from "path";
|
|
|
50551
50554
|
init_manager();
|
|
50552
50555
|
init_utils2();
|
|
50553
50556
|
init_create_tool();
|
|
50554
|
-
function
|
|
50555
|
-
const chain = swarmState.delegationChains.get(sessionID);
|
|
50556
|
-
if (!chain) {
|
|
50557
|
-
return [];
|
|
50558
|
-
}
|
|
50559
|
-
if (sinceTimestamp === 0) {
|
|
50560
|
-
return chain;
|
|
50561
|
-
}
|
|
50562
|
-
return chain.filter((entry) => entry.timestamp > sinceTimestamp);
|
|
50563
|
-
}
|
|
50564
|
-
function normalizeAgentsFromDelegations(delegations) {
|
|
50557
|
+
function collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, callerSessionId) {
|
|
50565
50558
|
const agents = new Set;
|
|
50566
|
-
|
|
50567
|
-
|
|
50568
|
-
|
|
50569
|
-
|
|
50570
|
-
|
|
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
|
+
}
|
|
50571
50575
|
}
|
|
50572
|
-
|
|
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 };
|
|
50573
50601
|
}
|
|
50574
50602
|
function isValidRetroEntry(entry, phase) {
|
|
50575
50603
|
return entry.type === "retrospective" && "phase_number" in entry && entry.phase_number === phase && "verdict" in entry && entry.verdict === "pass";
|
|
@@ -50599,12 +50627,9 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50599
50627
|
}, null, 2);
|
|
50600
50628
|
}
|
|
50601
50629
|
const session = ensureAgentSession(sessionID);
|
|
50602
|
-
const
|
|
50603
|
-
const
|
|
50604
|
-
const
|
|
50605
|
-
const trackedAgents = session.phaseAgentsDispatched ?? new Set;
|
|
50606
|
-
const allAgents = new Set([...delegationAgents, ...trackedAgents]);
|
|
50607
|
-
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();
|
|
50608
50633
|
const dir = workingDirectory ?? process.cwd();
|
|
50609
50634
|
const { config: config3 } = loadPluginConfigWithMeta(dir);
|
|
50610
50635
|
let phaseCompleteConfig;
|
|
@@ -50670,7 +50695,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50670
50695
|
status: "blocked",
|
|
50671
50696
|
reason: "RETROSPECTIVE_MISSING",
|
|
50672
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.`,
|
|
50673
|
-
agentsDispatched
|
|
50698
|
+
agentsDispatched,
|
|
50674
50699
|
agentsMissing: [],
|
|
50675
50700
|
warnings: [
|
|
50676
50701
|
`Retrospective missing for phase ${phase}.${schemaErrorDetail} Use this template:`,
|
|
@@ -50733,7 +50758,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50733
50758
|
if (phaseCompleteConfig.require_docs && !effectiveRequired.includes("docs")) {
|
|
50734
50759
|
effectiveRequired.push("docs");
|
|
50735
50760
|
}
|
|
50736
|
-
const agentsMissing = effectiveRequired.filter((req) => !
|
|
50761
|
+
const agentsMissing = effectiveRequired.filter((req) => !crossSessionResult.agents.has(req));
|
|
50737
50762
|
const warnings = [];
|
|
50738
50763
|
let success3 = true;
|
|
50739
50764
|
let status = "success";
|
|
@@ -50750,7 +50775,7 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50750
50775
|
}
|
|
50751
50776
|
}
|
|
50752
50777
|
const now = Date.now();
|
|
50753
|
-
const durationMs = now -
|
|
50778
|
+
const durationMs = now - phaseReferenceTimestamp;
|
|
50754
50779
|
const event = {
|
|
50755
50780
|
event: "phase_complete",
|
|
50756
50781
|
phase,
|
|
@@ -50768,9 +50793,14 @@ async function executePhaseComplete(args2, workingDirectory) {
|
|
|
50768
50793
|
warnings.push(`Warning: failed to write phase complete event: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
|
|
50769
50794
|
}
|
|
50770
50795
|
if (success3) {
|
|
50771
|
-
|
|
50772
|
-
|
|
50773
|
-
|
|
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
|
+
}
|
|
50774
50804
|
}
|
|
50775
50805
|
const result = {
|
|
50776
50806
|
success: success3,
|
|
@@ -54177,6 +54207,20 @@ function detectPlaceholderContent(args2) {
|
|
|
54177
54207
|
}
|
|
54178
54208
|
return issues;
|
|
54179
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
|
+
}
|
|
54180
54224
|
async function executeSavePlan(args2, fallbackDir) {
|
|
54181
54225
|
const placeholderIssues = detectPlaceholderContent(args2);
|
|
54182
54226
|
if (placeholderIssues.length > 0) {
|
|
@@ -54186,6 +54230,15 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
54186
54230
|
errors: placeholderIssues
|
|
54187
54231
|
};
|
|
54188
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
|
+
}
|
|
54189
54242
|
const plan = {
|
|
54190
54243
|
schema_version: "1.0.0",
|
|
54191
54244
|
title: args2.title,
|
|
@@ -54213,7 +54266,7 @@ async function executeSavePlan(args2, fallbackDir) {
|
|
|
54213
54266
|
})
|
|
54214
54267
|
};
|
|
54215
54268
|
const tasksCount = plan.phases.reduce((acc, phase) => acc + phase.tasks.length, 0);
|
|
54216
|
-
const dir =
|
|
54269
|
+
const dir = targetWorkspace;
|
|
54217
54270
|
try {
|
|
54218
54271
|
await savePlan(dir, plan);
|
|
54219
54272
|
return {
|
|
@@ -54247,7 +54300,7 @@ var save_plan = createSwarmTool({
|
|
|
54247
54300
|
acceptance: tool.schema.string().optional().describe("Acceptance criteria for this task")
|
|
54248
54301
|
})).min(1).describe("Tasks in this phase")
|
|
54249
54302
|
})).min(1).describe("Implementation phases"),
|
|
54250
|
-
working_directory: tool.schema.string().optional().describe("Working directory (
|
|
54303
|
+
working_directory: tool.schema.string().optional().describe("Working directory (explicit path, required - no fallback)")
|
|
54251
54304
|
},
|
|
54252
54305
|
execute: async (args2, _directory) => {
|
|
54253
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",
|