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 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
- while ((match = headingRegex.exec(content)) !== null) {
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
- while ((match = headingRegex.exec(content)) !== null) {
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
- let match;
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
- while ((match = diffPathPattern.exec(patchText)) !== null) {
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.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "").replace(/[\u200B-\u200D\uFEFF]/g, "").replace(/[\u202A-\u202E\u2066-\u2069]/g, "").replace(/```/g, "` ` `").replace(/^system\s*:/gim, "[BLOCKED]:");
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 getDelegationsSince(sessionID, sinceTimestamp) {
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
- for (const delegation of delegations) {
50565
- const normalizedFrom = stripKnownSwarmPrefix(delegation.from);
50566
- const normalizedTo = stripKnownSwarmPrefix(delegation.to);
50567
- agents.add(normalizedFrom);
50568
- 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
+ }
50569
50575
  }
50570
- 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 };
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 lastCompletionTimestamp = session.lastPhaseCompleteTimestamp ?? 0;
50601
- const recentDelegations = getDelegationsSince(sessionID, lastCompletionTimestamp);
50602
- const delegationAgents = normalizeAgentsFromDelegations(recentDelegations);
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) => !allAgents.has(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 - lastCompletionTimestamp;
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
- session.phaseAgentsDispatched = new Set;
50770
- session.lastPhaseCompleteTimestamp = now;
50771
- 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
+ }
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 = args2.working_directory ?? fallbackDir ?? process.cwd();
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 (defaults to process.cwd())")
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",
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",