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 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 getDelegationsSince(sessionID, sinceTimestamp) {
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
- for (const delegation of delegations) {
50567
- const normalizedFrom = stripKnownSwarmPrefix(delegation.from);
50568
- const normalizedTo = stripKnownSwarmPrefix(delegation.to);
50569
- agents.add(normalizedFrom);
50570
- agents.add(normalizedTo);
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
- return agents;
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 lastCompletionTimestamp = session.lastPhaseCompleteTimestamp ?? 0;
50603
- const recentDelegations = getDelegationsSince(sessionID, lastCompletionTimestamp);
50604
- const delegationAgents = normalizeAgentsFromDelegations(recentDelegations);
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) => !allAgents.has(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 - lastCompletionTimestamp;
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
- session.phaseAgentsDispatched = new Set;
50772
- session.lastPhaseCompleteTimestamp = now;
50773
- session.lastPhaseCompletePhase = phase;
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 = args2.working_directory ?? fallbackDir ?? process.cwd();
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 (defaults to process.cwd())")
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.4",
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",