opencode-swarm 7.21.0 → 7.21.2

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/dist/cli/index.js CHANGED
@@ -34,7 +34,7 @@ var package_default;
34
34
  var init_package = __esm(() => {
35
35
  package_default = {
36
36
  name: "opencode-swarm",
37
- version: "7.21.0",
37
+ version: "7.21.2",
38
38
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
39
39
  main: "dist/index.js",
40
40
  types: "dist/index.d.ts",
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ var package_default;
33
33
  var init_package = __esm(() => {
34
34
  package_default = {
35
35
  name: "opencode-swarm",
36
- version: "7.21.0",
36
+ version: "7.21.2",
37
37
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
38
38
  main: "dist/index.js",
39
39
  types: "dist/index.d.ts",
@@ -24922,6 +24922,46 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
24922
24922
  if (/^mkfs[./]/.test(seg)) {
24923
24923
  throw new Error(`BLOCKED: Disk format command (mkfs) detected — disk formatting operation`);
24924
24924
  }
24925
+ if (/^\\?mv\s/i.test(seg)) {
24926
+ const mvMatch = seg.match(/^\\?mv\s+(.+)$/i);
24927
+ if (mvMatch) {
24928
+ const argsStr = mvMatch[1];
24929
+ const strippedArgs = argsStr.replace(/["']/g, "");
24930
+ if (/\.swarm(?:[\x5c/\s]|$)/.test(strippedArgs)) {
24931
+ throw new Error(`BLOCKED: "mv" targeting .swarm/ detected — move operations under .swarm/ are not allowed from shell commands`);
24932
+ }
24933
+ }
24934
+ }
24935
+ if (/^\\?(?:move|ren)(?:\.exe)?\s/i.test(seg)) {
24936
+ const moveMatch = seg.match(/^\\?(?:move|ren)(?:\.exe)?\s+(.+)$/i);
24937
+ if (moveMatch) {
24938
+ const argsStr = moveMatch[1].replace(/["']/g, "");
24939
+ if (/\.swarm(?:[\x5c/\s]|$)/i.test(argsStr)) {
24940
+ throw new Error(`BLOCKED: "move" or "ren" targeting .swarm/ detected — move/rename operations under .swarm/ are not allowed from shell commands`);
24941
+ }
24942
+ }
24943
+ }
24944
+ if (/^\\?(?:Move-Item|Rename-Item|move|mi|mv|ren|rni)\b.*\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24945
+ throw new Error(`BLOCKED: PowerShell Move-Item or Rename-Item targeting .swarm/ detected — move/rename operations under .swarm/ are not allowed from shell commands`);
24946
+ }
24947
+ if (/^\\?rm\b/i.test(seg) && !/^\\?rm\s+(?:-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24948
+ throw new Error(`BLOCKED: "rm" targeting .swarm/ detected — deleting files under .swarm/ is not allowed from shell commands`);
24949
+ }
24950
+ if (/\bcp\b.*\.swarm(?:[\x5c/\s]|$)/i.test(seg) && /\brm\b.*\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24951
+ throw new Error(`BLOCKED: "cp" of .swarm/ file followed by "rm" of .swarm/ source detected — copy-and-delete bypass is not allowed`);
24952
+ }
24953
+ if (/^rsync\b.*--remove-source-files\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24954
+ throw new Error(`BLOCKED: "rsync" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
24955
+ }
24956
+ if (/^tar\b.*--remove-files\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24957
+ throw new Error(`BLOCKED: "tar" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
24958
+ }
24959
+ if (/^zip\b.*\s-m\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24960
+ throw new Error(`BLOCKED: "zip" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
24961
+ }
24962
+ if (/^7z\b.*\s-sdel\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24963
+ throw new Error(`BLOCKED: "7z" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
24964
+ }
24925
24965
  }
24926
24966
  }
24927
24967
  async function checkGateLimits(params) {
@@ -62574,6 +62614,7 @@ If a tool modifies a file, it is a CODER tool. Delegate.
62574
62614
  - Before you delegate a coding task, call declare_scope with { taskId, files } where \`files\` is the exact list of paths the coder is allowed to write. Bundle any generated/lockfile paths that the change will produce (e.g. package-lock.json, Cargo.lock, dist/*).
62575
62615
  - If coder returns "WRITE BLOCKED" for a path outside the declared list: call declare_scope again with the missing path added. Do NOT instruct the coder to use bash, sed, echo, cat, tee, dd, or any interpreter eval (python -c, node -e, bun -e, ruby -e) to bypass the block. Those routes bypass the authority check and violate scope discipline.
62576
62616
  - Never wrap a file write in eval, bash -c, sh -c, a subshell, or a heredoc-to-file redirect. Those are bash workarounds and are banned even when scope appears to permit them — the write-authority guard is tool-scoped; bash is unguarded and must not be used as a write path.
62617
+ - Do NOT use mv, Move-Item, move, ren, Rename-Item, or cp-then-rm chains to relocate, rename, or delete files under \`.swarm/\` as a workaround for blocked destructive commands. Those are file-move shell bypasses and are banned. Use the tool's dedicated tools (\`.swarm/\` file management or evidence manager tools) instead.
62577
62618
  - If you cannot enumerate files up front (e.g. a broad refactor), declare the containing directories — declare_scope accepts directory entries and grants containment.
62578
62619
  - Rationale: declare_scope persists the allowed set to disk (.swarm/scopes/scope-\${taskId}.json) so it survives cross-process delegation. Without a call, the coder process reads an empty scope and every Edit/Write is denied.
62579
62620
  <!-- BEHAVIORAL_GUIDANCE_END -->
@@ -90226,6 +90267,7 @@ function phaseIsExplicitlyNonCode(directory, phase) {
90226
90267
  }
90227
90268
 
90228
90269
  // src/tools/phase-complete.ts
90270
+ init_gate_evidence();
90229
90271
  init_curator();
90230
90272
  init_knowledge_curator();
90231
90273
  init_knowledge_reader();
@@ -90761,6 +90803,23 @@ function safeWarn(message, error93) {
90761
90803
  warn(message, error93 instanceof Error ? error93.message : String(error93));
90762
90804
  } catch {}
90763
90805
  }
90806
+ var TASK_GATE_INFERABLE_AGENTS = new Set([
90807
+ "coder",
90808
+ "reviewer",
90809
+ "test_engineer"
90810
+ ]);
90811
+ function canInferMissingAgentsFromTaskGates(agentsMissing) {
90812
+ return agentsMissing.every((agent) => TASK_GATE_INFERABLE_AGENTS.has(agent));
90813
+ }
90814
+ async function allCompletedTasksHavePassedGateEvidence(directory, tasks) {
90815
+ for (const task of tasks) {
90816
+ if (task.status !== "completed")
90817
+ return false;
90818
+ if (!await hasPassedAllGates(directory, task.id))
90819
+ return false;
90820
+ }
90821
+ return tasks.length > 0;
90822
+ }
90764
90823
  function collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, callerSessionId) {
90765
90824
  const agents = new Set;
90766
90825
  const contributorSessionIds = [];
@@ -90772,12 +90831,9 @@ function collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, callerSess
90772
90831
  agents.add(agent);
90773
90832
  }
90774
90833
  }
90775
- const callerDelegations = swarmState.delegationChains.get(callerSessionId);
90776
- if (callerDelegations) {
90777
- for (const delegation of callerDelegations) {
90778
- agents.add(stripKnownSwarmPrefix(delegation.from));
90779
- agents.add(stripKnownSwarmPrefix(delegation.to));
90780
- }
90834
+ for (const delegation of _getDelegationsSince(callerSessionId, phaseReferenceTimestamp)) {
90835
+ agents.add(stripKnownSwarmPrefix(delegation.from));
90836
+ agents.add(stripKnownSwarmPrefix(delegation.to));
90781
90837
  }
90782
90838
  }
90783
90839
  for (const [sessionId, session] of swarmState.agentSessions) {
@@ -90796,17 +90852,24 @@ function collectCrossSessionDispatchedAgents(phaseReferenceTimestamp, callerSess
90796
90852
  agents.add(agent);
90797
90853
  }
90798
90854
  }
90799
- const delegations2 = swarmState.delegationChains.get(sessionId);
90800
- if (delegations2) {
90801
- for (const delegation of delegations2) {
90802
- agents.add(stripKnownSwarmPrefix(delegation.from));
90803
- agents.add(stripKnownSwarmPrefix(delegation.to));
90804
- }
90855
+ for (const delegation of _getDelegationsSince(sessionId, phaseReferenceTimestamp)) {
90856
+ agents.add(stripKnownSwarmPrefix(delegation.from));
90857
+ agents.add(stripKnownSwarmPrefix(delegation.to));
90805
90858
  }
90806
90859
  }
90807
90860
  }
90808
90861
  return { agents, contributorSessionIds };
90809
90862
  }
90863
+ function _getDelegationsSince(sessionID, sinceTimestamp) {
90864
+ const chain = swarmState.delegationChains.get(sessionID);
90865
+ if (!chain) {
90866
+ return [];
90867
+ }
90868
+ if (sinceTimestamp === 0) {
90869
+ return chain;
90870
+ }
90871
+ return chain.filter((entry) => entry.timestamp > sinceTimestamp);
90872
+ }
90810
90873
  function isValidRetroEntry(entry, phase) {
90811
90874
  return entry.type === "retrospective" && "phase_number" in entry && entry.phase_number === phase && "verdict" in entry && entry.verdict === "pass";
90812
90875
  }
@@ -91752,8 +91815,8 @@ Advisory notes: ${advisoryNotes.join("; ")}` : "";
91752
91815
  const planRaw = fs84.readFileSync(planPath, "utf-8");
91753
91816
  const plan = JSON.parse(planRaw);
91754
91817
  const targetPhase = plan.phases.find((p) => p.id === phase);
91755
- if (targetPhase && targetPhase.tasks.length > 0 && targetPhase.tasks.every((t) => t.status === "completed")) {
91756
- warnings.push(`Agent dispatch fallback: all ${targetPhase.tasks.length} tasks in phase ${phase} are completed in plan.json. Clearing missing agents: ${agentsMissing.join(", ")}.`);
91818
+ if (targetPhase && targetPhase.tasks.length > 0 && canInferMissingAgentsFromTaskGates(agentsMissing) && await allCompletedTasksHavePassedGateEvidence(dir, targetPhase.tasks)) {
91819
+ warnings.push(`Agent dispatch fallback: all ${targetPhase.tasks.length} tasks in phase ${phase} are completed in plan.json and durable gate evidence passed. Clearing missing agents: ${agentsMissing.join(", ")}.`);
91757
91820
  agentsMissing = [];
91758
91821
  }
91759
91822
  } catch {}
@@ -103343,6 +103406,13 @@ function matchesTier3Pattern2(files) {
103343
103406
  }
103344
103407
  return false;
103345
103408
  }
103409
+ function hasPassedDurableGateEvidence(workingDirectory, taskId) {
103410
+ const evidence = readTaskEvidenceRaw(workingDirectory, taskId);
103411
+ if (!evidence || !Array.isArray(evidence.required_gates) || evidence.required_gates.length === 0) {
103412
+ return false;
103413
+ }
103414
+ return evidence.required_gates.every((gate) => evidence.gates?.[gate] != null);
103415
+ }
103346
103416
  function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = false, sessionID) {
103347
103417
  try {
103348
103418
  let skipStandardTurboBypass = false;
@@ -103389,12 +103459,11 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103389
103459
  try {
103390
103460
  const evidence = readTaskEvidenceRaw(resolvedDir, taskId);
103391
103461
  if (evidence === null) {} else if (evidence.required_gates && Array.isArray(evidence.required_gates) && evidence.gates) {
103392
- const allGatesMet = evidence.required_gates.every((gate) => evidence.gates[gate] != null);
103393
- if (allGatesMet) {
103462
+ if (evidence.required_gates.length > 0 && evidence.required_gates.every((gate) => evidence.gates[gate] != null)) {
103394
103463
  return { blocked: false, reason: "" };
103395
103464
  }
103396
103465
  const missingGates = evidence.required_gates.filter((gate) => evidence.gates[gate] == null);
103397
- evidenceIncompleteReason = `Task ${taskId} is missing required gates: [${missingGates.join(", ")}]. ` + `Required: [${evidence.required_gates.join(", ")}]. ` + `Completed: [${Object.keys(evidence.gates).join(", ")}]. ` + `Delegate the missing gate agents before marking task as completed.`;
103466
+ evidenceIncompleteReason = evidence.required_gates.length === 0 ? `Task ${taskId} has an evidence file with no required gates. Delegate reviewer and test_engineer before marking task as completed.` : `Task ${taskId} is missing required gates: [${missingGates.join(", ")}]. ` + `Required: [${evidence.required_gates.join(", ")}]. ` + `Completed: [${Object.keys(evidence.gates).join(", ")}]. ` + `Delegate the missing gate agents before marking task as completed.`;
103398
103467
  }
103399
103468
  } catch (error93) {
103400
103469
  console.warn(`[gate-evidence] Evidence file for task ${taskId} is corrupt or unreadable:`, error93 instanceof Error ? error93.message : String(error93));
@@ -103404,7 +103473,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103404
103473
  reason: `Evidence file for task ${taskId} is corrupt or unreadable. ` + `Fix the file at .swarm/evidence/${taskId}.json or delete it to fall through to session state.`
103405
103474
  };
103406
103475
  }
103407
- if (swarmState.agentSessions.size === 0) {
103476
+ if (swarmState.agentSessions.size === 0 && !evidenceIncompleteReason) {
103408
103477
  return { blocked: false, reason: "" };
103409
103478
  }
103410
103479
  let validSessionCount = 0;
@@ -103421,7 +103490,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103421
103490
  return { blocked: false, reason: "" };
103422
103491
  }
103423
103492
  }
103424
- if (validSessionCount === 0) {
103493
+ if (validSessionCount === 0 && !evidenceIncompleteReason) {
103425
103494
  return { blocked: false, reason: "" };
103426
103495
  }
103427
103496
  const stateEntries = [];
@@ -103438,7 +103507,7 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103438
103507
  const plan = JSON.parse(planRaw);
103439
103508
  for (const planPhase of plan.phases ?? []) {
103440
103509
  for (const task of planPhase.tasks ?? []) {
103441
- if (task.id === taskId && task.status === "completed") {
103510
+ if (task.id === taskId && task.status === "completed" && hasPassedDurableGateEvidence(resolvedDir2, taskId)) {
103442
103511
  return { blocked: false, reason: "" };
103443
103512
  }
103444
103513
  }
@@ -103459,26 +103528,6 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103459
103528
  }
103460
103529
  }
103461
103530
  }
103462
- if (!hasReviewer && !hasTestEngineer) {
103463
- for (const [, chain] of swarmState.delegationChains) {
103464
- let lastCoderIndex = -1;
103465
- for (let i2 = chain.length - 1;i2 >= 0; i2--) {
103466
- const target = stripKnownSwarmPrefix(chain[i2].to);
103467
- if (target === "coder") {
103468
- lastCoderIndex = i2;
103469
- break;
103470
- }
103471
- }
103472
- const searchStart = lastCoderIndex === -1 ? 0 : lastCoderIndex + 1;
103473
- for (let i2 = searchStart;i2 < chain.length; i2++) {
103474
- const target = stripKnownSwarmPrefix(chain[i2].to);
103475
- if (target === "reviewer")
103476
- hasReviewer = true;
103477
- if (target === "test_engineer")
103478
- hasTestEngineer = true;
103479
- }
103480
- }
103481
- }
103482
103531
  if (hasReviewer && hasTestEngineer) {
103483
103532
  return { blocked: false, reason: "" };
103484
103533
  }
@@ -103521,26 +103570,6 @@ function recoverTaskStateFromDelegations(taskId) {
103521
103570
  }
103522
103571
  }
103523
103572
  }
103524
- if (!hasReviewer && !hasTestEngineer) {
103525
- for (const [, chain] of swarmState.delegationChains) {
103526
- let lastCoderIndex = -1;
103527
- for (let i2 = chain.length - 1;i2 >= 0; i2--) {
103528
- const target = stripKnownSwarmPrefix(chain[i2].to);
103529
- if (target === "coder") {
103530
- lastCoderIndex = i2;
103531
- break;
103532
- }
103533
- }
103534
- const searchStart = lastCoderIndex === -1 ? 0 : lastCoderIndex + 1;
103535
- for (let i2 = searchStart;i2 < chain.length; i2++) {
103536
- const target = stripKnownSwarmPrefix(chain[i2].to);
103537
- if (target === "reviewer")
103538
- hasReviewer = true;
103539
- if (target === "test_engineer")
103540
- hasTestEngineer = true;
103541
- }
103542
- }
103543
- }
103544
103573
  if (!hasReviewer && !hasTestEngineer)
103545
103574
  return;
103546
103575
  for (const [, session] of swarmState.agentSessions) {
@@ -66,11 +66,10 @@ export declare function checkReviewerGate(taskId: string, workingDirectory?: str
66
66
  export declare function checkReviewerGateWithScope(taskId: string, workingDirectory?: string, sessionID?: string): Promise<ReviewerGateResult>;
67
67
  /**
68
68
  * Recovery mechanism: reconcile task state with delegation history.
69
- * When reviewer/test_engineer delegations occurred but the state machine
70
- * was not advanced (e.g., toolAfter didn't fire, subagent_type missing,
71
- * cross-session gaps, or pure verification tasks without coder delegation),
72
- * this function walks all delegation chains and advances the task state
73
- * so that checkReviewerGate can make an accurate decision.
69
+ * When task-scoped reviewer/test_engineer delegations occurred but the state
70
+ * machine was not advanced (e.g., toolAfter didn't fire or subagent_type was
71
+ * missing), this function advances the task state so that checkReviewerGate can
72
+ * make an accurate decision without attributing unrelated delegation activity.
74
73
  *
75
74
  * @param taskId - The task ID to recover state for
76
75
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.21.0",
3
+ "version": "7.21.2",
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",