opencode-swarm 7.21.2 → 7.21.4

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.2",
37
+ version: "7.21.4",
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",
@@ -16043,7 +16043,7 @@ var init_manager = __esm(() => {
16043
16043
 
16044
16044
  // src/commands/acknowledge-spec-drift.ts
16045
16045
  import { promises as fsPromises3 } from "fs";
16046
- async function handleAcknowledgeSpecDriftCommand(directory, _args) {
16046
+ async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedBy = "unknown") {
16047
16047
  const specStalenessPath = validateSwarmPath(directory, "spec-staleness.json");
16048
16048
  let stalenessContent;
16049
16049
  try {
@@ -16084,7 +16084,7 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
16084
16084
  timestamp: new Date().toISOString(),
16085
16085
  phase,
16086
16086
  planTitle,
16087
- acknowledgedBy: "architect",
16087
+ acknowledgedBy,
16088
16088
  previousHash: stalenessData.specHash_plan,
16089
16089
  newHash: currentHash
16090
16090
  };
@@ -52031,7 +52031,8 @@ async function executeSwarmCommand(args) {
52031
52031
  directory,
52032
52032
  args: resolved.remainingArgs,
52033
52033
  sessionID,
52034
- agents
52034
+ agents,
52035
+ source: "chat"
52035
52036
  });
52036
52037
  } catch (_err) {
52037
52038
  const cmdName = tokens[0] || "unknown";
@@ -52063,6 +52064,12 @@ function classifySwarmCommandToolUse(resolved) {
52063
52064
  const canonicalKey = canonicalCommandKey(resolved);
52064
52065
  const args = resolved.remainingArgs;
52065
52066
  if (!SWARM_COMMAND_TOOL_ALLOWLIST.has(canonicalKey)) {
52067
+ if (HUMAN_ONLY_SWARM_COMMANDS.has(canonicalKey)) {
52068
+ return {
52069
+ allowed: false,
52070
+ message: `/swarm ${canonicalKey} is a human-only command. ` + `Present the situation to the user and ask them to run \`/swarm ${canonicalKey}\` themselves ` + `(or \`bunx opencode-swarm run ${canonicalKey}\` from a terminal). ` + `You MUST NOT run it yourself via Bash, swarm_command, or any other tool \u2014 ` + `the runtime guardrail will block such attempts.`
52071
+ };
52072
+ }
52066
52073
  return {
52067
52074
  allowed: false,
52068
52075
  message: `/swarm ${canonicalKey} is not available through the chat tool yet.
@@ -52152,7 +52159,7 @@ function classifySwarmCommandChatFallbackUse(resolved) {
52152
52159
  }
52153
52160
  return { allowed: true };
52154
52161
  }
52155
- var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
52162
+ var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, HUMAN_ONLY_SWARM_COMMANDS, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
52156
52163
  var init_tool_policy = __esm(() => {
52157
52164
  init_command_dispatch();
52158
52165
  SWARM_COMMAND_TOOL_COMMANDS = [
@@ -52198,6 +52205,13 @@ var init_tool_policy = __esm(() => {
52198
52205
  "sync-plan",
52199
52206
  "export"
52200
52207
  ]);
52208
+ HUMAN_ONLY_SWARM_COMMANDS = new Set([
52209
+ "acknowledge-spec-drift",
52210
+ "reset",
52211
+ "reset-session",
52212
+ "rollback",
52213
+ "checkpoint"
52214
+ ]);
52201
52215
  NO_ARGS = new Set([
52202
52216
  "agents",
52203
52217
  "config",
@@ -52755,7 +52769,7 @@ var init_registry = __esm(() => {
52755
52769
  init_write_retro2();
52756
52770
  COMMAND_REGISTRY = {
52757
52771
  "acknowledge-spec-drift": {
52758
- handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args),
52772
+ handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args, ctx.source === "cli" ? "cli" : ctx.source === "chat" ? "user" : "unknown"),
52759
52773
  description: "Acknowledge that the spec has drifted from the plan and suppress further warnings",
52760
52774
  args: "",
52761
52775
  category: "diagnostics"
@@ -53601,7 +53615,8 @@ Valid commands: ${VALID_COMMANDS.join(", ")}`);
53601
53615
  directory: cwd,
53602
53616
  args: resolved.remainingArgs,
53603
53617
  sessionID: "",
53604
- agents: {}
53618
+ agents: {},
53619
+ source: "cli"
53605
53620
  });
53606
53621
  console.log(result);
53607
53622
  return 0;
@@ -1,5 +1,17 @@
1
+ /**
2
+ * Caller identification for spec-drift acknowledgment audit trail.
3
+ * Previously hardcoded as 'architect' — see issue #890, where the architect
4
+ * could shell out to `bunx opencode-swarm run acknowledge-spec-drift` and
5
+ * the resulting event mis-attributed the action. Callers now pass an
6
+ * explicit actor so events.jsonl can distinguish the legitimate paths
7
+ * ('user' from chat slash command, 'cli' from a real terminal) from any
8
+ * unidentified caller ('unknown'). The Bash guardrail
9
+ * (`src/hooks/guardrails.ts` section 23) blocks the agent-shell bypass at
10
+ * the runtime layer; this parameter exists for forensic clarity.
11
+ */
12
+ export type SpecDriftAcknowledgedBy = 'user' | 'cli' | 'unknown';
1
13
  /**
2
14
  * Handle /swarm acknowledge-spec-drift command
3
15
  * Acknowledges and clears a previously detected spec drift staleness warning
4
16
  */
5
- export declare function handleAcknowledgeSpecDriftCommand(directory: string, _args: string[]): Promise<string>;
17
+ export declare function handleAcknowledgeSpecDriftCommand(directory: string, _args: string[], acknowledgedBy?: SpecDriftAcknowledgedBy): Promise<string>;
@@ -8,6 +8,14 @@ export type CommandContext = {
8
8
  args: string[];
9
9
  sessionID: string;
10
10
  agents: Record<string, AgentDefinition>;
11
+ /**
12
+ * Dispatch path identifier. Issue #890: forensic audit trail for
13
+ * commands that need to distinguish "user typed /swarm <cmd>" (chat)
14
+ * from "user ran bunx opencode-swarm run <cmd>" (cli). Handlers that
15
+ * don't care can ignore this field. Optional for backwards-compatibility
16
+ * with existing callers.
17
+ */
18
+ source?: 'cli' | 'chat';
11
19
  };
12
20
  export type CommandResult = Promise<string>;
13
21
  export type CommandCategory = 'core' | 'agent' | 'config' | 'diagnostics' | 'utility';
@@ -2,5 +2,14 @@ import type { ResolvedSwarmCommand, SwarmCommandPolicyResult } from './command-d
2
2
  export declare const SWARM_COMMAND_TOOL_COMMANDS: readonly ["agents", "config", "config doctor", "config-doctor", "doctor", "doctor tools", "status", "show-plan", "plan", "help", "history", "evidence", "evidence summary", "evidence-summary", "retrieve", "diagnose", "preflight", "benchmark", "knowledge", "sync-plan", "export", "list-agents"];
3
3
  export type SwarmCommandToolInputCommand = (typeof SWARM_COMMAND_TOOL_COMMANDS)[number];
4
4
  export declare const SWARM_COMMAND_TOOL_ALLOWLIST: Set<string>;
5
+ /**
6
+ * Issue #890: subcommands that must be invoked by a human user, not by the
7
+ * agent. The runtime Bash guardrail
8
+ * (`src/hooks/guardrails.ts` section 23) blocks the equivalent
9
+ * `bunx opencode-swarm run <cmd>` shell invocation; this set drives the
10
+ * chat-tool refusal message so the agent is told to surface to the user
11
+ * instead of being pointed at the CLI bypass it just attempted.
12
+ */
13
+ export declare const HUMAN_ONLY_SWARM_COMMANDS: Set<string>;
5
14
  export declare function classifySwarmCommandToolUse(resolved: ResolvedSwarmCommand): SwarmCommandPolicyResult;
6
15
  export declare function classifySwarmCommandChatFallbackUse(resolved: ResolvedSwarmCommand): SwarmCommandPolicyResult;
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.2",
36
+ version: "7.21.4",
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",
@@ -18564,7 +18564,7 @@ var init_manager = __esm(() => {
18564
18564
 
18565
18565
  // src/commands/acknowledge-spec-drift.ts
18566
18566
  import { promises as fsPromises4 } from "node:fs";
18567
- async function handleAcknowledgeSpecDriftCommand(directory, _args) {
18567
+ async function handleAcknowledgeSpecDriftCommand(directory, _args, acknowledgedBy = "unknown") {
18568
18568
  const specStalenessPath = validateSwarmPath(directory, "spec-staleness.json");
18569
18569
  let stalenessContent;
18570
18570
  try {
@@ -18605,7 +18605,7 @@ async function handleAcknowledgeSpecDriftCommand(directory, _args) {
18605
18605
  timestamp: new Date().toISOString(),
18606
18606
  phase,
18607
18607
  planTitle,
18608
- acknowledgedBy: "architect",
18608
+ acknowledgedBy,
18609
18609
  previousHash: stalenessData.specHash_plan,
18610
18610
  newHash: currentHash
18611
18611
  };
@@ -24285,7 +24285,7 @@ function getMostRecentAssistantText(messages) {
24285
24285
  function isTransientProviderFailureText(text) {
24286
24286
  if (!text.trim())
24287
24287
  return false;
24288
- const providerFailureMarker = /provider[_\s-]?unavailable|network\s+connection\s+lost/i.test(text);
24288
+ const providerFailureMarker = /provider[_\s-]?unavailable|network\s+connection\s+lost|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|broken.?pipe|dns(?:[\s_-]+(?:resolution)?)?[\s_-]+fail|name.?not.?resolved|EAI_AGAIN|connection\s+reset|connection\s+refused/i.test(text);
24289
24289
  if (!providerFailureMarker)
24290
24290
  return false;
24291
24291
  const status = extractStatusCode(text);
@@ -24962,6 +24962,45 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
24962
24962
  if (/^7z\b.*\s-sdel\b/i.test(seg) && /\.swarm(?:[\x5c/\s]|$)/i.test(seg)) {
24963
24963
  throw new Error(`BLOCKED: "7z" with delete-source flag targeting .swarm/ detected — archive with source deletion under .swarm/ is not allowed`);
24964
24964
  }
24965
+ {
24966
+ const HUMAN_ONLY_SWARM_SUBCOMMANDS = new Set([
24967
+ "acknowledge-spec-drift",
24968
+ "reset",
24969
+ "reset-session",
24970
+ "rollback",
24971
+ "checkpoint"
24972
+ ]);
24973
+ let probe = seg.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, "").replace(/^eval(?:\s+--)?\s+["']?/, "").replace(/["']\s*$/, "").replace(/^\$\(\s*/, "").replace(/^\(\s*/, "").replace(/\s*\)$/, "").replace(/^`/, "").replace(/`$/, "").trim();
24974
+ for (let i2 = 0;i2 < 4; i2++) {
24975
+ const before = probe;
24976
+ probe = probe.replace(/^env\s+(?:-i\b|--ignore-environment\b|-u\s+\S+|-[a-zA-Z]+\s+)*\s*/, "").replace(/^command\s+(?:-[pvV]\s+)*/, "").replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)+/, "").trim();
24977
+ if (probe === before)
24978
+ break;
24979
+ }
24980
+ const swarmCliBypassMatch = probe.match(/^\\?(?:bunx|npx|pnpx|npm(?:\s+(?:exec|x)(?:\s+--)?)?|pnpm(?:\s+(?:dlx|exec))?|yarn(?:\s+(?:dlx|exec))?|bun(?:\s+x)?|node|deno\s+run|tsx|ts-node)\b[^|;&]*?\bopencode-swarm\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
24981
+ if (swarmCliBypassMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmCliBypassMatch[1])) {
24982
+ throw new Error(`BLOCKED: "${swarmCliBypassMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmCliBypassMatch[1]}\` themselves.`);
24983
+ }
24984
+ const swarmBareBinMatch = probe.match(/^\\?opencode-swarm\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
24985
+ if (swarmBareBinMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmBareBinMatch[1])) {
24986
+ throw new Error(`BLOCKED: "${swarmBareBinMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmBareBinMatch[1]}\` themselves.`);
24987
+ }
24988
+ const swarmCliPathMatch = probe.match(/\bcli[/\\]+index\.[mc]?(?:js|ts)\b[^|;&]*?\brun\s+([A-Za-z0-9_-]+)/i);
24989
+ if (swarmCliPathMatch && HUMAN_ONLY_SWARM_SUBCOMMANDS.has(swarmCliPathMatch[1])) {
24990
+ throw new Error(`BLOCKED: "${swarmCliPathMatch[1]}" is a human-only swarm command and may not be invoked from shell by an agent. ` + `Present the situation to the user and ask them to run \`/swarm ${swarmCliPathMatch[1]}\` themselves.`);
24991
+ }
24992
+ }
24993
+ {
24994
+ const normForPathCheck = seg.replace(/\\/g, "/").replace(/\/(?:\.\/+)+/g, "/").replace(/\/{2,}/g, "/");
24995
+ if (/\.swarm\/spec-staleness\.json\b/i.test(normForPathCheck)) {
24996
+ const trimmed = seg.trim();
24997
+ const looksReadOnly = /^(?:cat|less|more|head|tail|file|stat|ls|dir|Get-Content|gc|Get-Item|gi|type)\b/i.test(trimmed);
24998
+ const hasWriteRedirect = />{1,2}\s*[^\s>]/.test(trimmed);
24999
+ if (!looksReadOnly || hasWriteRedirect) {
25000
+ throw new Error("BLOCKED: shell command targeting .swarm/spec-staleness.json detected. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
25001
+ }
25002
+ }
25003
+ }
24965
25004
  }
24966
25005
  }
24967
25006
  async function checkGateLimits(params) {
@@ -25149,11 +25188,37 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
25149
25188
  }
25150
25189
  }
25151
25190
  }
25191
+ function extractAllPatchPayloads(args2) {
25192
+ const toolArgs = args2;
25193
+ if (!toolArgs)
25194
+ return [];
25195
+ const out2 = [];
25196
+ for (const key of ["patch", "input", "diff"]) {
25197
+ const v = toolArgs[key];
25198
+ if (typeof v === "string" && v.length > 0)
25199
+ out2.push(v);
25200
+ }
25201
+ const cmd = toolArgs.cmd;
25202
+ if (Array.isArray(cmd)) {
25203
+ for (const entry of cmd) {
25204
+ if (typeof entry === "string" && entry.length > 0)
25205
+ out2.push(entry);
25206
+ }
25207
+ }
25208
+ return out2;
25209
+ }
25210
+ function patchPayloadHasHumanOnlyInvocation(args2) {
25211
+ const payloads = extractAllPatchPayloads(args2);
25212
+ if (payloads.length === 0)
25213
+ return false;
25214
+ const re = /\bopencode-swarm\b[\s\S]*?\brun\s+(?:acknowledge-spec-drift|reset|reset-session|rollback|checkpoint)\b/i;
25215
+ return payloads.some((p) => re.test(p));
25216
+ }
25152
25217
  function extractPatchTargetPaths(tool, args2) {
25153
25218
  if (tool !== "apply_patch" && tool !== "patch")
25154
25219
  return [];
25155
25220
  const toolArgs = args2;
25156
- const patchText = toolArgs?.input ?? toolArgs?.patch ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
25221
+ const patchText = toolArgs?.input ?? toolArgs?.patch ?? toolArgs?.diff ?? (Array.isArray(toolArgs?.cmd) ? toolArgs.cmd[1] : undefined);
25157
25222
  if (typeof patchText !== "string")
25158
25223
  return [];
25159
25224
  if (patchText.length > 1e6) {
@@ -25201,6 +25266,11 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
25201
25266
  function handlePlanAndScopeProtection(sessionID, tool, args2) {
25202
25267
  const toolArgs = args2;
25203
25268
  const targetPath = toolArgs?.filePath ?? toolArgs?.path ?? toolArgs?.file ?? toolArgs?.target;
25269
+ if (tool === "apply_patch" || tool === "patch") {
25270
+ if (patchPayloadHasHumanOnlyInvocation(args2)) {
25271
+ throw new Error("BLOCKED: apply_patch would introduce a script invoking a human-only swarm CLI subcommand. " + "Present the situation to the user and ask them to run the command themselves.");
25272
+ }
25273
+ }
25204
25274
  if (typeof targetPath === "string" && targetPath.length > 0) {
25205
25275
  const resolvedTarget = path10.resolve(effectiveDirectory, targetPath).toLowerCase();
25206
25276
  const planMdPath = path10.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
@@ -25208,15 +25278,27 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
25208
25278
  if (resolvedTarget === planMdPath || resolvedTarget === planJsonPath) {
25209
25279
  throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
25210
25280
  }
25281
+ const specStalenessPath = path10.resolve(effectiveDirectory, ".swarm", "spec-staleness.json").toLowerCase();
25282
+ if (resolvedTarget === specStalenessPath) {
25283
+ throw new Error("SPEC_DRIFT_VIOLATION: Direct writes to .swarm/spec-staleness.json are blocked. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
25284
+ }
25285
+ const content = toolArgs?.content ?? toolArgs?.text ?? toolArgs?.new_string ?? toolArgs?.newText;
25286
+ if (typeof content === "string" && /\bopencode-swarm\b[\s\S]*?\brun\s+(?:acknowledge-spec-drift|reset|reset-session|rollback|checkpoint)\b/i.test(content)) {
25287
+ throw new Error("BLOCKED: write/edit tool would create a script invoking a human-only swarm CLI subcommand. " + "Present the situation to the user and ask them to run the command themselves.");
25288
+ }
25211
25289
  }
25212
25290
  if (!targetPath && (tool === "apply_patch" || tool === "patch")) {
25213
25291
  for (const p of extractPatchTargetPaths(tool, args2)) {
25214
25292
  const resolvedP = path10.resolve(effectiveDirectory, p);
25215
25293
  const planMdPath = path10.resolve(effectiveDirectory, ".swarm", "plan.md").toLowerCase();
25216
25294
  const planJsonPath = path10.resolve(effectiveDirectory, ".swarm", "plan.json").toLowerCase();
25295
+ const specStalenessPath = path10.resolve(effectiveDirectory, ".swarm", "spec-staleness.json").toLowerCase();
25217
25296
  if (resolvedP.toLowerCase() === planMdPath || resolvedP.toLowerCase() === planJsonPath) {
25218
25297
  throw new Error("PLAN STATE VIOLATION: Direct writes to .swarm/plan.md and .swarm/plan.json are blocked. " + "plan.md is auto-regenerated from plan.json by PlanSyncWorker. " + "Use save_plan for ALL structural plan changes (adding/removing tasks, updating descriptions, dependencies, or phase names). " + "Use update_task_status() for task status only. " + "Use phase_complete() for phase transitions only.");
25219
25298
  }
25299
+ if (resolvedP.toLowerCase() === specStalenessPath) {
25300
+ throw new Error("SPEC_DRIFT_VIOLATION: Direct writes to .swarm/spec-staleness.json are blocked. " + "This file is system-managed and gates plan-mutating tools while spec drift is unresolved. " + "Present the drift to the user and ask them to run /swarm clarify or /swarm acknowledge-spec-drift.");
25301
+ }
25220
25302
  if (isOutsideSwarmDir(p, effectiveDirectory) && (isSourceCodePath(p) || hasTraversalSegments(p))) {
25221
25303
  const session = swarmState.agentSessions.get(sessionID);
25222
25304
  if (session) {
@@ -25762,6 +25844,35 @@ ${textPart2.text}`;
25762
25844
  }
25763
25845
  session.pendingAdvisoryMessages = [];
25764
25846
  } else if (!isArchitectSession && session && (session.pendingAdvisoryMessages?.length ?? 0) > 0) {
25847
+ const allAdvisories = session.pendingAdvisoryMessages ?? [];
25848
+ const TRANSIENT_PREFIXES = [
25849
+ "TRANSIENT ERROR:",
25850
+ "MODEL FALLBACK:",
25851
+ "DEGRADED:"
25852
+ ];
25853
+ const transientAdvisories = allAdvisories.filter((m) => TRANSIENT_PREFIXES.some((p) => m.startsWith(p)));
25854
+ if (transientAdvisories.length > 0) {
25855
+ let targetMsg = systemMessages[0];
25856
+ if (!targetMsg) {
25857
+ const newMsg = {
25858
+ info: { role: "system" },
25859
+ parts: [{ type: "text", text: "" }]
25860
+ };
25861
+ messages.unshift(newMsg);
25862
+ targetMsg = newMsg;
25863
+ }
25864
+ const textPart2 = (targetMsg.parts ?? []).find((part) => part.type === "text" && typeof part.text === "string");
25865
+ if (textPart2) {
25866
+ const joined = transientAdvisories.join(`
25867
+ ---
25868
+ `);
25869
+ textPart2.text = `[ADVISORIES]
25870
+ ${joined}
25871
+ [/ADVISORIES]
25872
+
25873
+ ${textPart2.text}`;
25874
+ }
25875
+ }
25765
25876
  session.pendingAdvisoryMessages = [];
25766
25877
  }
25767
25878
  if (isArchitectSession && session?.prmHardStopPending) {
@@ -26237,7 +26348,7 @@ var init_guardrails = __esm(() => {
26237
26348
  ]);
26238
26349
  storedInputArgs = new Map;
26239
26350
  TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 529]);
26240
- TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|provider[_\s-]?unavailable|server.?error|network.?connection.?lost|connection.?(refused|reset|timeout|lost)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable/i;
26351
+ TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|provider[_\s-]?unavailable|server.?error|network.?connection.?lost|connection.?(refused|reset|timeout|lost)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EPIPE|ENOTFOUND|broken.?pipe|dns(?:[\s_-]+(?:resolution)?)?[\s_-]+fail|name.?not.?resolved|EAI_AGAIN/i;
26241
26352
  DEGRADED_ERROR_PATTERN = /context.?length|token.?(limit|budget)|input.?too.?long|content.?filter|exceeds?.?(maximum.?)?tokens|maximum.?context|context.?window|too.?many.?tokens|prompt.?too.?long|message.?too.?long|request.?too.?large|max.?tokens/i;
26242
26353
  CONTENT_FILTER_PATTERN = /content.?filter/i;
26243
26354
  toolCallsSinceLastWrite = new Map;
@@ -61019,7 +61130,8 @@ async function executeSwarmCommand(args2) {
61019
61130
  directory,
61020
61131
  args: resolved.remainingArgs,
61021
61132
  sessionID,
61022
- agents
61133
+ agents,
61134
+ source: "chat"
61023
61135
  });
61024
61136
  } catch (_err) {
61025
61137
  const cmdName = tokens[0] || "unknown";
@@ -61051,6 +61163,12 @@ function classifySwarmCommandToolUse(resolved) {
61051
61163
  const canonicalKey = canonicalCommandKey(resolved);
61052
61164
  const args2 = resolved.remainingArgs;
61053
61165
  if (!SWARM_COMMAND_TOOL_ALLOWLIST.has(canonicalKey)) {
61166
+ if (HUMAN_ONLY_SWARM_COMMANDS.has(canonicalKey)) {
61167
+ return {
61168
+ allowed: false,
61169
+ message: `/swarm ${canonicalKey} is a human-only command. ` + `Present the situation to the user and ask them to run \`/swarm ${canonicalKey}\` themselves ` + `(or \`bunx opencode-swarm run ${canonicalKey}\` from a terminal). ` + `You MUST NOT run it yourself via Bash, swarm_command, or any other tool — ` + `the runtime guardrail will block such attempts.`
61170
+ };
61171
+ }
61054
61172
  return {
61055
61173
  allowed: false,
61056
61174
  message: `/swarm ${canonicalKey} is not available through the chat tool yet.
@@ -61140,7 +61258,7 @@ function classifySwarmCommandChatFallbackUse(resolved) {
61140
61258
  }
61141
61259
  return { allowed: true };
61142
61260
  }
61143
- var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
61261
+ var SWARM_COMMAND_TOOL_COMMANDS, SWARM_COMMAND_TOOL_ALLOWLIST, HUMAN_ONLY_SWARM_COMMANDS, NO_ARGS, SUMMARY_ID_PATTERN, TASK_ID_PATTERN;
61144
61262
  var init_tool_policy = __esm(() => {
61145
61263
  init_command_dispatch();
61146
61264
  SWARM_COMMAND_TOOL_COMMANDS = [
@@ -61186,6 +61304,13 @@ var init_tool_policy = __esm(() => {
61186
61304
  "sync-plan",
61187
61305
  "export"
61188
61306
  ]);
61307
+ HUMAN_ONLY_SWARM_COMMANDS = new Set([
61308
+ "acknowledge-spec-drift",
61309
+ "reset",
61310
+ "reset-session",
61311
+ "rollback",
61312
+ "checkpoint"
61313
+ ]);
61189
61314
  NO_ARGS = new Set([
61190
61315
  "agents",
61191
61316
  "config",
@@ -61743,7 +61868,7 @@ var init_registry = __esm(() => {
61743
61868
  init_write_retro2();
61744
61869
  COMMAND_REGISTRY = {
61745
61870
  "acknowledge-spec-drift": {
61746
- handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args),
61871
+ handler: (ctx) => handleAcknowledgeSpecDriftCommand(ctx.directory, ctx.args, ctx.source === "cli" ? "cli" : ctx.source === "chat" ? "user" : "unknown"),
61747
61872
  description: "Acknowledge that the spec has drifted from the plan and suppress further warnings",
61748
61873
  args: "",
61749
61874
  category: "diagnostics"
@@ -62999,7 +63124,7 @@ Continue handling small touch-ups (typos, cross-references) inline.
62999
63124
  ## SLASH COMMANDS
63000
63125
  {{SLASH_COMMANDS}}
63001
63126
  Commands above are documented with args and behavioral details. Run commands via /swarm <command> [args].
63002
- Outside OpenCode, invoke any plugin command via: \`bunx opencode-swarm run <command> [args]\` (e.g. \`bunx opencode-swarm run knowledge migrate\`). Do not use \`bun -e\` or look for \`src/commands/\` — those paths are internal to the plugin source and do not exist in user project directories.
63127
+ Outside OpenCode, invoke any plugin command via: \`bunx opencode-swarm run <command> [args]\` (e.g. \`bunx opencode-swarm run knowledge migrate\`). Do not use \`bun -e\` or look for \`src/commands/\` — those paths are internal to the plugin source and do not exist in user project directories. EXCEPTION — human-only commands (including but not limited to \`acknowledge-spec-drift\`, \`reset\`, \`reset-session\`, \`rollback\`, \`checkpoint\`, and any command that releases a runtime safety gate or destroys plan state): you MUST present these to the user and ask them to run the command themselves. Never invoke a human-only command via Bash, swarm_command, or chat fallback. The runtime guardrail will block such attempts; if a Bash call returns \`BLOCKED\` with a "human-only" message, do not retry under a different shell form — present the situation to the user instead.
63003
63128
 
63004
63129
  SMEs advise only. Reviewer and critic review only. None of them write code.
63005
63130
 
@@ -63887,10 +64012,17 @@ If resuming a project with an existing approved plan, CRITIC-GATE is already sat
63887
64012
  - This rule is satisfied by the save_plan tool's own spec gate — it exists as a reminder that planning requires a spec.
63888
64013
 
63889
64014
  6k. SPEC-STALENESS GUARD:
63890
- - If _specStale or .swarm/spec-staleness.json exists, the Architect MUST read the file and either:
63891
- - Run /swarm clarify to update the spec and align it with the plan, OR
63892
- - Run /swarm acknowledge-spec-drift to acknowledge the drift and suppress further warnings
63893
- - Do NOT proceed with implementation until spec staleness is resolved.
64015
+ - If _specStale or .swarm/spec-staleness.json exists, the Architect MUST stop
64016
+ and SURFACE THE DRIFT TO THE USER. The user (not the Architect) then runs
64017
+ either:
64018
+ - /swarm clarify to update the spec and align it with the plan, OR
64019
+ - /swarm acknowledge-spec-drift to acknowledge the drift and suppress further warnings
64020
+ - The Architect MUST NOT run /swarm acknowledge-spec-drift itself — not via
64021
+ the swarm_command tool, not via the chat fallback, and NOT by shelling out
64022
+ to \`bunx opencode-swarm run acknowledge-spec-drift\` (or any equivalent
64023
+ \`npx\`/\`node\`/\`bun\` invocation). Any such self-invocation is a
64024
+ control-bypass and will be refused by the runtime guardrails.
64025
+ - Do NOT proceed with implementation until the user resolves the staleness.
63894
64026
  - When re-saving a plan in response to spec drift, save_plan REQUIRES that ANY task
63895
64027
  present in the prior plan but absent from the new args.phases be enumerated
63896
64028
  in removed_task_ids with a removal_reason. save_plan will reject the call
@@ -63901,8 +64033,8 @@ If resuming a project with an existing approved plan, CRITIC-GATE is already sat
63901
64033
  - While .swarm/spec-staleness.json exists, the runtime STRUCTURALLY BLOCKS the
63902
64034
  following tools (SPEC_DRIFT_BLOCKED_TOOLS): save_plan, update_task_status,
63903
64035
  phase_complete, lean_turbo_run_phase, lean_turbo_acquire_locks. If a call
63904
- returns SPEC_DRIFT_BLOCK, do NOT retry; instead present the drift to the
63905
- user and run /swarm clarify or /swarm acknowledge-spec-drift first.
64036
+ returns SPEC_DRIFT_BLOCK, do NOT retry; surface the drift to the user and
64037
+ WAIT for them to run /swarm clarify or /swarm acknowledge-spec-drift.
63906
64038
 
63907
64039
  ### MODE: EXECUTE
63908
64040
  For each task (respecting dependencies):
@@ -65499,9 +65631,10 @@ WORKFLOW:
65499
65631
  - TODO comments in code (those go through the task system, not code comments)
65500
65632
 
65501
65633
  ## RELEASE NOTES
65502
- When writing release notes (docs/releases/v{VERSION}.md):
65503
- - Determine next version from .release-please-manifest.json + commit type (feat minor, fix patch)
65504
- - Follow the established format in existing release notes files
65634
+ When writing release notes (docs/releases/pending/<slug>.md):
65635
+ - Do NOT determine the next version. Do NOT create docs/releases/vX.Y.Z.md. release-please owns the version; the release workflow aggregates pending fragments.
65636
+ - Pick a short, kebab-case slug describing your change (e.g. spec-drift-self-ack-guardrail.md). Pick one unlikely to collide with concurrent PRs.
65637
+ - Follow the established format in existing release notes files (descriptive topic heading, not a version prefix).
65505
65638
  - Include: overview, breaking changes (if any), new features, bug fixes, internal improvements
65506
65639
  - Do NOT manually edit package.json version, CHANGELOG.md, or .release-please-manifest.json — release-please owns these
65507
65640
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.21.2",
3
+ "version": "7.21.4",
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",