opencode-swarm 7.22.1 → 7.23.1

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.22.1",
37
+ version: "7.23.1",
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",
@@ -43447,7 +43447,7 @@ var init_handoff_service = __esm(() => {
43447
43447
  });
43448
43448
 
43449
43449
  // src/session/snapshot-writer.ts
43450
- import { mkdirSync as mkdirSync10, renameSync as renameSync6 } from "fs";
43450
+ import { closeSync as closeSync3, fsyncSync as fsyncSync2, mkdirSync as mkdirSync10, openSync as openSync3, renameSync as renameSync6 } from "fs";
43451
43451
  import * as path27 from "path";
43452
43452
  function serializeAgentSession(s) {
43453
43453
  const gateLog = {};
@@ -43464,6 +43464,12 @@ function serializeAgentSession(s) {
43464
43464
  const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
43465
43465
  const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
43466
43466
  const lastCompletedPhaseAgentsDispatched = Array.from(s.lastCompletedPhaseAgentsDispatched ?? new Set);
43467
+ const stageBCompletion = {};
43468
+ if (s.stageBCompletion) {
43469
+ for (const [taskId, agents] of s.stageBCompletion) {
43470
+ stageBCompletion[taskId] = Array.from(agents);
43471
+ }
43472
+ }
43467
43473
  const windows = {};
43468
43474
  const rawWindows = s.windows ?? {};
43469
43475
  for (const [key, win] of Object.entries(rawWindows)) {
@@ -43520,7 +43526,8 @@ function serializeAgentSession(s) {
43520
43526
  fullAutoInteractionCount: s.fullAutoInteractionCount ?? 0,
43521
43527
  fullAutoDeadlockCount: s.fullAutoDeadlockCount ?? 0,
43522
43528
  fullAutoLastQuestionHash: s.fullAutoLastQuestionHash ?? null,
43523
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
43529
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
43530
+ ...Object.keys(stageBCompletion).length > 0 && { stageBCompletion }
43524
43531
  };
43525
43532
  }
43526
43533
  async function writeSnapshot(directory, state) {
@@ -43542,6 +43549,14 @@ async function writeSnapshot(directory, state) {
43542
43549
  mkdirSync10(dir, { recursive: true });
43543
43550
  const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
43544
43551
  await bunWrite(tempPath, content);
43552
+ try {
43553
+ const fd = openSync3(tempPath, "r+");
43554
+ try {
43555
+ fsyncSync2(fd);
43556
+ } finally {
43557
+ closeSync3(fd);
43558
+ }
43559
+ } catch {}
43545
43560
  renameSync6(tempPath, resolvedPath);
43546
43561
  } catch (error93) {
43547
43562
  log("[snapshot-writer] write failed", {
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import * as fs from 'node:fs';
20
20
  import type { MessageWithParts } from './knowledge-types.js';
21
- import { computeSkillRelevanceScore } from './skill-scoring.js';
21
+ import { computeSkillRelevanceScore, formatSkillIndexWithContext } from './skill-scoring.js';
22
22
  import { appendSkillUsageEntry, readSkillUsageEntries, readSkillUsageEntriesTail } from './skill-usage-log.js';
23
23
  /** Agents that should receive skill context in delegations. */
24
24
  export declare const SKILL_CAPABLE_AGENTS: Set<string>;
@@ -37,6 +37,8 @@ export interface SkillGateInput {
37
37
  }
38
38
  export interface SkillPropagationConfig {
39
39
  enabled: boolean;
40
+ /** When true, blocks delegations missing SKILLS field instead of warning. */
41
+ enforce?: boolean;
40
42
  }
41
43
  export declare const _internals: {
42
44
  readdirSync: typeof fs.readdirSync;
@@ -44,6 +46,8 @@ export declare const _internals: {
44
46
  statSync: typeof fs.statSync;
45
47
  mkdirSync: typeof fs.mkdirSync;
46
48
  appendFileSync: typeof fs.appendFileSync;
49
+ readFileSync: typeof fs.readFileSync;
50
+ writeFileSync: typeof fs.writeFileSync;
47
51
  skillPropagationGateBefore: typeof skillPropagationGateBefore;
48
52
  skillPropagationTransformScan: typeof skillPropagationTransformScan;
49
53
  SKILL_CAPABLE_AGENTS: Set<string>;
@@ -57,6 +61,7 @@ export declare const _internals: {
57
61
  parseSkillPaths: typeof parseSkillPaths;
58
62
  extractTaskIdFromPrompt: typeof extractTaskIdFromPrompt;
59
63
  computeSkillRelevanceScore: typeof computeSkillRelevanceScore;
64
+ formatSkillIndexWithContext: typeof formatSkillIndexWithContext;
60
65
  };
61
66
  /**
62
67
  * Scans project for available skill SKILL.md files.
@@ -94,13 +99,22 @@ export declare function extractTaskIdFromPrompt(prompt: string): string;
94
99
  /**
95
100
  * Pre-tool gate. When the architect delegates via Task tool to a skill-capable
96
101
  * agent and the SKILLS field is missing or 'none' while skills exist in the
97
- * project, logs a warning event to events.jsonl. NEVER blocks execution.
102
+ * project, logs a warning event to events.jsonl and returns a warning string
103
+ * for visible injection into the architect prompt. When config.enforce is true,
104
+ * blocks the delegation entirely instead of merely warning.
98
105
  *
99
106
  * Also records skill delegation entries to `.swarm/skill-usage.jsonl` when
100
107
  * the architect delegates to a skill-capable agent with a non-empty, non-"none"
101
108
  * SKILLS field.
109
+ *
110
+ * @returns { blocked: false, reason: null } when no action needed.
111
+ * { blocked: false, reason: "warning message" } when warning only (enforce=false).
112
+ * { blocked: true, reason: "blocked: ..." } when blocking (enforce=true).
102
113
  */
103
- export declare function skillPropagationGateBefore(directory: string, input: SkillGateInput, config: SkillPropagationConfig): Promise<void>;
114
+ export declare function skillPropagationGateBefore(directory: string, input: SkillGateInput, config: SkillPropagationConfig): Promise<{
115
+ blocked: boolean;
116
+ reason: string | null;
117
+ }>;
104
118
  /**
105
119
  * Chat messages transform hook. Scans reviewer output for SKILL_COMPLIANCE
106
120
  * verdicts and records compliance outcomes to `.swarm/skill-usage.jsonl`.
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.22.1",
36
+ version: "7.23.1",
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",
@@ -52386,7 +52386,7 @@ var init_handoff_service = __esm(() => {
52386
52386
  });
52387
52387
 
52388
52388
  // src/session/snapshot-writer.ts
52389
- import { mkdirSync as mkdirSync13, renameSync as renameSync9 } from "node:fs";
52389
+ import { closeSync as closeSync3, fsyncSync as fsyncSync2, mkdirSync as mkdirSync13, openSync as openSync3, renameSync as renameSync9 } from "node:fs";
52390
52390
  import * as path33 from "node:path";
52391
52391
  function serializeAgentSession(s) {
52392
52392
  const gateLog = {};
@@ -52403,6 +52403,12 @@ function serializeAgentSession(s) {
52403
52403
  const catastrophicPhaseWarnings = Array.from(s.catastrophicPhaseWarnings ?? new Set);
52404
52404
  const phaseAgentsDispatched = Array.from(s.phaseAgentsDispatched ?? new Set);
52405
52405
  const lastCompletedPhaseAgentsDispatched = Array.from(s.lastCompletedPhaseAgentsDispatched ?? new Set);
52406
+ const stageBCompletion = {};
52407
+ if (s.stageBCompletion) {
52408
+ for (const [taskId, agents] of s.stageBCompletion) {
52409
+ stageBCompletion[taskId] = Array.from(agents);
52410
+ }
52411
+ }
52406
52412
  const windows = {};
52407
52413
  const rawWindows = s.windows ?? {};
52408
52414
  for (const [key, win] of Object.entries(rawWindows)) {
@@ -52459,7 +52465,8 @@ function serializeAgentSession(s) {
52459
52465
  fullAutoInteractionCount: s.fullAutoInteractionCount ?? 0,
52460
52466
  fullAutoDeadlockCount: s.fullAutoDeadlockCount ?? 0,
52461
52467
  fullAutoLastQuestionHash: s.fullAutoLastQuestionHash ?? null,
52462
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
52468
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
52469
+ ...Object.keys(stageBCompletion).length > 0 && { stageBCompletion }
52463
52470
  };
52464
52471
  }
52465
52472
  async function writeSnapshot(directory, state) {
@@ -52481,6 +52488,14 @@ async function writeSnapshot(directory, state) {
52481
52488
  mkdirSync13(dir, { recursive: true });
52482
52489
  const tempPath = `${resolvedPath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
52483
52490
  await bunWrite(tempPath, content);
52491
+ try {
52492
+ const fd = openSync3(tempPath, "r+");
52493
+ try {
52494
+ fsyncSync2(fd);
52495
+ } finally {
52496
+ closeSync3(fd);
52497
+ }
52498
+ } catch {}
52484
52499
  renameSync9(tempPath, resolvedPath);
52485
52500
  } catch (error93) {
52486
52501
  log("[snapshot-writer] write failed", {
@@ -83382,6 +83397,8 @@ var _internals42 = {
83382
83397
  statSync: fs62.statSync.bind(fs62),
83383
83398
  mkdirSync: fs62.mkdirSync.bind(fs62),
83384
83399
  appendFileSync: fs62.appendFileSync.bind(fs62),
83400
+ readFileSync: fs62.readFileSync.bind(fs62),
83401
+ writeFileSync: fs62.writeFileSync.bind(fs62),
83385
83402
  skillPropagationGateBefore: null,
83386
83403
  skillPropagationTransformScan: null,
83387
83404
  SKILL_CAPABLE_AGENTS,
@@ -83394,7 +83411,8 @@ var _internals42 = {
83394
83411
  readSkillUsageEntriesTail,
83395
83412
  parseSkillPaths: null,
83396
83413
  extractTaskIdFromPrompt: null,
83397
- computeSkillRelevanceScore
83414
+ computeSkillRelevanceScore,
83415
+ formatSkillIndexWithContext
83398
83416
  };
83399
83417
  function discoverAvailableSkills(directory) {
83400
83418
  const results = [];
@@ -83494,22 +83512,22 @@ function extractTaskIdFromPrompt(prompt) {
83494
83512
  }
83495
83513
  async function skillPropagationGateBefore(directory, input, config3) {
83496
83514
  if (!config3.enabled)
83497
- return;
83515
+ return { blocked: false, reason: null };
83498
83516
  const toolName = typeof input.tool === "string" ? input.tool : "";
83499
83517
  if (toolName !== "task" && toolName !== "Task")
83500
- return;
83518
+ return { blocked: false, reason: null };
83501
83519
  const agentRaw = typeof input.agent === "string" ? input.agent : "";
83502
83520
  if (!agentRaw)
83503
- return;
83521
+ return { blocked: false, reason: null };
83504
83522
  const baseAgent = stripKnownSwarmPrefix(agentRaw);
83505
83523
  if (baseAgent !== "architect")
83506
- return;
83524
+ return { blocked: false, reason: null };
83507
83525
  const parsed = _internals42.parseDelegationArgs(input.args);
83508
83526
  if (!parsed)
83509
- return;
83527
+ return { blocked: false, reason: null };
83510
83528
  const targetBase = stripKnownSwarmPrefix(parsed.targetAgent);
83511
83529
  if (!_internals42.SKILL_CAPABLE_AGENTS.has(targetBase))
83512
- return;
83530
+ return { blocked: false, reason: null };
83513
83531
  const sessionID = typeof input.sessionID === "string" ? input.sessionID : "unknown";
83514
83532
  const availableSkills = _internals42.discoverAvailableSkills(directory);
83515
83533
  const skillsValue = parsed.skillsField.trim();
@@ -83545,16 +83563,19 @@ async function skillPropagationGateBefore(directory, input, config3) {
83545
83563
  }
83546
83564
  }
83547
83565
  }
83566
+ let scoringSkipped = false;
83567
+ let scored = [];
83548
83568
  if (skillsValue && skillsValue.toLowerCase() !== "none" && availableSkills.length > 0) {
83549
83569
  try {
83550
83570
  const sessionEntries = _internals42.readSkillUsageEntriesTail(directory, {
83551
83571
  sessionID
83552
83572
  });
83553
83573
  if (sessionEntries.length > _internals42.MAX_SCORING_SESSION_ENTRIES) {
83574
+ scoringSkipped = true;
83554
83575
  warn(`[skill-propagation-gate] skipping scoring — session has ${sessionEntries.length} entries (limit: ${_internals42.MAX_SCORING_SESSION_ENTRIES})`);
83555
83576
  } else {
83556
83577
  const prompt = typeof input.args?.prompt === "string" ? String(input.args.prompt) : "";
83557
- const scored = availableSkills.map((skillPath) => {
83578
+ scored = availableSkills.map((skillPath) => {
83558
83579
  const skillEntries = sessionEntries.filter((e) => e.skillPath === skillPath);
83559
83580
  const score = _internals42.computeSkillRelevanceScore(skillPath, prompt, skillEntries);
83560
83581
  return { skillPath, score, usageCount: skillEntries.length };
@@ -83569,11 +83590,77 @@ async function skillPropagationGateBefore(directory, input, config3) {
83569
83590
  warn(`[skill-propagation-gate] skill scoring failed (non-blocking): ${err2 instanceof Error ? err2.message : String(err2)}`);
83570
83591
  }
83571
83592
  }
83593
+ if (availableSkills.length > 0) {
83594
+ try {
83595
+ let skillsForIndex = availableSkills;
83596
+ if (scoringSkipped) {
83597
+ skillsForIndex = [...availableSkills].sort((a, b) => {
83598
+ const nameA = path89.basename(path89.dirname(a));
83599
+ const nameB = path89.basename(path89.dirname(b));
83600
+ return nameA.localeCompare(nameB);
83601
+ });
83602
+ } else if (typeof scored !== "undefined" && scored.length > 0) {
83603
+ skillsForIndex = scored.map((r) => r.skillPath);
83604
+ }
83605
+ const formattedIndex = _internals42.formatSkillIndexWithContext(skillsForIndex, directory);
83606
+ if (formattedIndex.length > 0) {
83607
+ const contextPath = path89.join(directory, ".swarm", "context.md");
83608
+ let existingContent = "";
83609
+ if (_internals42.existsSync(contextPath)) {
83610
+ existingContent = _internals42.readFileSync(contextPath, "utf-8");
83611
+ }
83612
+ const sectionHeader = "## Available Skills";
83613
+ const newSection = `${sectionHeader}
83614
+ ${formattedIndex}
83615
+ `;
83616
+ let updatedContent;
83617
+ if (existingContent.includes(sectionHeader)) {
83618
+ const sectionStart = existingContent.indexOf(sectionHeader);
83619
+ const sectionEnd = existingContent.indexOf(`
83620
+ ## `, sectionStart + sectionHeader.length);
83621
+ if (sectionEnd !== -1) {
83622
+ updatedContent = existingContent.slice(0, sectionStart) + newSection + existingContent.slice(sectionEnd + 1);
83623
+ } else {
83624
+ updatedContent = existingContent.slice(0, sectionStart) + newSection;
83625
+ }
83626
+ } else {
83627
+ if (existingContent.length > 0 && !existingContent.endsWith(`
83628
+ `)) {
83629
+ updatedContent = existingContent + `
83630
+ ` + newSection;
83631
+ } else {
83632
+ updatedContent = existingContent + newSection;
83633
+ }
83634
+ }
83635
+ const swarmDir = path89.dirname(contextPath);
83636
+ if (!_internals42.existsSync(swarmDir)) {
83637
+ _internals42.mkdirSync(swarmDir, { recursive: true });
83638
+ }
83639
+ _internals42.writeFileSync(contextPath, updatedContent, "utf-8");
83640
+ }
83641
+ } catch (err2) {
83642
+ warn(`[skill-propagation-gate] failed to write skill index to context.md: ${err2 instanceof Error ? err2.message : String(err2)}`);
83643
+ }
83644
+ }
83645
+ if (targetBase === "reviewer") {
83646
+ const prompt = typeof input.args?.prompt === "string" ? String(input.args.prompt) : "";
83647
+ const hasSkillsUsedByCoder = /SKILLS_USED_BY_CODER\s*:/i.test(prompt);
83648
+ const coderHadSkills = skillsValue.length > 0 && skillsValue.toLowerCase() !== "none";
83649
+ if (!hasSkillsUsedByCoder && coderHadSkills) {
83650
+ const message = `SKILLS_USED_BY_CODER warning: Delegating to reviewer without SKILLS_USED_BY_CODER field. ` + `Add SKILLS_USED_BY_CODER with the skills the coder received for this task.`;
83651
+ return { blocked: false, reason: message };
83652
+ }
83653
+ }
83572
83654
  if (availableSkills.length === 0)
83573
- return;
83655
+ return { blocked: false, reason: null };
83574
83656
  const skillsLower = skillsValue.toLowerCase();
83575
83657
  if (skillsValue && skillsLower !== "none")
83576
- return;
83658
+ return { blocked: false, reason: null };
83659
+ const skillNames = availableSkills.map((p) => {
83660
+ const parts2 = p.split("/");
83661
+ return parts2[parts2.length - 2] ?? p;
83662
+ });
83663
+ const warningMsg = `Skill propagation warning: Delegating to ${targetBase} without SKILLS field. ` + `Available skills: ${skillNames.join(", ")}`;
83577
83664
  try {
83578
83665
  _internals42.writeWarnEvent(directory, {
83579
83666
  type: "skill_propagation_warn",
@@ -83586,6 +83673,11 @@ async function skillPropagationGateBefore(directory, input, config3) {
83586
83673
  available_skills: availableSkills
83587
83674
  });
83588
83675
  } catch {}
83676
+ if (config3.enforce) {
83677
+ const blockedMsg = `Blocked by skill propagation gate: Delegating to ${targetBase} without SKILLS field. ` + `Available skills: ${skillNames.join(", ")}. ` + `Add a SKILLS: field or set enforce: false in config.`;
83678
+ return { blocked: true, reason: blockedMsg };
83679
+ }
83680
+ return { blocked: false, reason: warningMsg };
83589
83681
  }
83590
83682
  var COMPLIANCE_PATTERN = /SKILL_COMPLIANCE\s*:\s*(COMPLIANT|PARTIAL|VIOLATED)(?:\s*(?:—|-)\s*(.*))?\s*$/i;
83591
83683
  var CODER_SKILLS_PATTERN = /SKILLS_USED_BY_CODER\s*:\s*(.+)/i;
@@ -83599,10 +83691,13 @@ async function skillPropagationTransformScan(directory, output, sessionID) {
83599
83691
  let dedupKeys = new Set;
83600
83692
  let existingEntries = [];
83601
83693
  try {
83602
- existingEntries = _internals42.readSkillUsageEntries(directory, {
83694
+ existingEntries = _internals42.readSkillUsageEntriesTail(directory, {
83603
83695
  sessionID
83604
83696
  });
83605
- dedupKeys = new Set(existingEntries.map((e) => `${e.skillPath}|${e.agentName}|${e.taskID}`));
83697
+ dedupKeys = new Set(existingEntries.map((e, i2) => {
83698
+ const taskKey = e.taskID === "unknown" ? `unknown-${i2}` : e.taskID;
83699
+ return `${e.skillPath}|${e.agentName}|${taskKey}`;
83700
+ }));
83606
83701
  } catch (err2) {
83607
83702
  warn(`[skill-propagation-gate] dedup preload failed, continuing without dedup: ${err2 instanceof Error ? err2.message : String(err2)}`);
83608
83703
  }
@@ -83738,6 +83833,7 @@ _internals42.discoverAvailableSkills = discoverAvailableSkills;
83738
83833
  _internals42.parseDelegationArgs = parseDelegationArgs;
83739
83834
  _internals42.parseSkillPaths = parseSkillPaths;
83740
83835
  _internals42.extractTaskIdFromPrompt = extractTaskIdFromPrompt;
83836
+ _internals42.formatSkillIndexWithContext = formatSkillIndexWithContext;
83741
83837
 
83742
83838
  // src/hooks/slop-detector.ts
83743
83839
  import * as fs63 from "node:fs";
@@ -85115,6 +85211,12 @@ function deserializeAgentSession(s) {
85115
85211
  const catastrophicPhaseWarnings = new Set(s.catastrophicPhaseWarnings ?? []);
85116
85212
  const phaseAgentsDispatched = new Set(s.phaseAgentsDispatched ?? []);
85117
85213
  const lastCompletedPhaseAgentsDispatched = new Set(s.lastCompletedPhaseAgentsDispatched ?? []);
85214
+ const stageBCompletion = new Map;
85215
+ if (s.stageBCompletion) {
85216
+ for (const [taskId, agents] of Object.entries(s.stageBCompletion)) {
85217
+ stageBCompletion.set(taskId, new Set(agents));
85218
+ }
85219
+ }
85118
85220
  const windows = {};
85119
85221
  for (const [key, win] of Object.entries(s.windows ?? {})) {
85120
85222
  windows[key] = {
@@ -85169,7 +85271,8 @@ function deserializeAgentSession(s) {
85169
85271
  prmLastPatternDetected: null,
85170
85272
  prmTrajectoryStep: 0,
85171
85273
  prmHardStopPending: false,
85172
- sessionRehydratedAt: s.sessionRehydratedAt ?? 0
85274
+ sessionRehydratedAt: s.sessionRehydratedAt ?? 0,
85275
+ stageBCompletion
85173
85276
  };
85174
85277
  }
85175
85278
  async function readSnapshot(directory) {
@@ -86379,7 +86482,7 @@ function countCodeLines(content) {
86379
86482
  return lines.length;
86380
86483
  }
86381
86484
  function isTestFile(filePath) {
86382
- const basename13 = path97.basename(filePath);
86485
+ const basename14 = path97.basename(filePath);
86383
86486
  const _ext = path97.extname(filePath).toLowerCase();
86384
86487
  const testPatterns = [
86385
86488
  ".test.",
@@ -86395,7 +86498,7 @@ function isTestFile(filePath) {
86395
86498
  ".spec.jsx"
86396
86499
  ];
86397
86500
  for (const pattern of testPatterns) {
86398
- if (basename13.includes(pattern)) {
86501
+ if (basename14.includes(pattern)) {
86399
86502
  return true;
86400
86503
  }
86401
86504
  }
@@ -87009,8 +87112,8 @@ import {
87009
87112
  appendFileSync as appendFileSync11,
87010
87113
  existsSync as existsSync53,
87011
87114
  mkdirSync as mkdirSync24,
87012
- readFileSync as readFileSync43,
87013
- writeFileSync as writeFileSync16
87115
+ readFileSync as readFileSync44,
87116
+ writeFileSync as writeFileSync17
87014
87117
  } from "node:fs";
87015
87118
  import { join as join83 } from "node:path";
87016
87119
  var EVIDENCE_DIR2 = ".swarm/evidence";
@@ -87052,7 +87155,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87052
87155
  const existingRoot = Object.create(null);
87053
87156
  if (existsSync53(filePath)) {
87054
87157
  try {
87055
- const parsed = JSON.parse(readFileSync43(filePath, "utf-8"));
87158
+ const parsed = JSON.parse(readFileSync44(filePath, "utf-8"));
87056
87159
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
87057
87160
  safeAssignOwnProps(existingRoot, parsed);
87058
87161
  }
@@ -87080,7 +87183,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87080
87183
  updated.taskId = synthesis.taskId;
87081
87184
  if (!Array.isArray(updated.required_gates))
87082
87185
  updated.required_gates = [];
87083
- writeFileSync16(filePath, JSON.stringify(updated, null, 2));
87186
+ writeFileSync17(filePath, JSON.stringify(updated, null, 2));
87084
87187
  try {
87085
87188
  const councilDir = join83(workingDir, ".swarm", "council");
87086
87189
  mkdirSync24(councilDir, { recursive: true });
@@ -87412,7 +87515,7 @@ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFi
87412
87515
  }
87413
87516
 
87414
87517
  // src/council/criteria-store.ts
87415
- import { existsSync as existsSync54, mkdirSync as mkdirSync25, readFileSync as readFileSync44, writeFileSync as writeFileSync17 } from "node:fs";
87518
+ import { existsSync as existsSync54, mkdirSync as mkdirSync25, readFileSync as readFileSync45, writeFileSync as writeFileSync18 } from "node:fs";
87416
87519
  import { join as join84 } from "node:path";
87417
87520
  var COUNCIL_DIR = ".swarm/council";
87418
87521
  function writeCriteria(workingDir, taskId, criteria) {
@@ -87423,14 +87526,14 @@ function writeCriteria(workingDir, taskId, criteria) {
87423
87526
  criteria,
87424
87527
  declaredAt: new Date().toISOString()
87425
87528
  };
87426
- writeFileSync17(join84(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
87529
+ writeFileSync18(join84(dir, `${safeId(taskId)}.json`), JSON.stringify(payload, null, 2));
87427
87530
  }
87428
87531
  function readCriteria(workingDir, taskId) {
87429
87532
  const filePath = join84(workingDir, COUNCIL_DIR, `${safeId(taskId)}.json`);
87430
87533
  if (!existsSync54(filePath))
87431
87534
  return null;
87432
87535
  try {
87433
- const parsed = JSON.parse(readFileSync44(filePath, "utf-8"));
87536
+ const parsed = JSON.parse(readFileSync45(filePath, "utf-8"));
87434
87537
  if (parsed && typeof parsed === "object" && typeof parsed.taskId === "string" && Array.isArray(parsed.criteria)) {
87435
87538
  return parsed;
87436
87539
  }
@@ -99766,10 +99869,10 @@ init_loader();
99766
99869
  import {
99767
99870
  existsSync as existsSync72,
99768
99871
  mkdirSync as mkdirSync31,
99769
- readFileSync as readFileSync62,
99872
+ readFileSync as readFileSync63,
99770
99873
  renameSync as renameSync20,
99771
99874
  unlinkSync as unlinkSync15,
99772
- writeFileSync as writeFileSync24
99875
+ writeFileSync as writeFileSync25
99773
99876
  } from "node:fs";
99774
99877
  import path124 from "node:path";
99775
99878
  init_create_tool();
@@ -99903,7 +100006,7 @@ var submit_phase_council_verdicts = createSwarmTool({
99903
100006
  function getPhaseMutationGapFinding(phaseNumber, workingDir) {
99904
100007
  const mutationGatePath = path124.join(workingDir, ".swarm", "evidence", String(phaseNumber), "mutation-gate.json");
99905
100008
  try {
99906
- const raw = readFileSync62(mutationGatePath, "utf-8");
100009
+ const raw = readFileSync63(mutationGatePath, "utf-8");
99907
100010
  const parsed = JSON.parse(raw);
99908
100011
  const gateEntry = (parsed.entries ?? []).find((entry) => entry?.type === "mutation-gate");
99909
100012
  if (!gateEntry) {
@@ -99998,7 +100101,7 @@ function writePhaseCouncilEvidence(workingDir, synthesis) {
99998
100101
  };
99999
100102
  const tempFile = `${evidenceFile}.tmp-${Date.now()}`;
100000
100103
  try {
100001
- writeFileSync24(tempFile, JSON.stringify(evidenceBundle, null, 2), "utf-8");
100104
+ writeFileSync25(tempFile, JSON.stringify(evidenceBundle, null, 2), "utf-8");
100002
100105
  renameSync20(tempFile, evidenceFile);
100003
100106
  } finally {
100004
100107
  if (existsSync72(tempFile)) {
@@ -102209,7 +102312,7 @@ import * as path131 from "node:path";
102209
102312
 
102210
102313
  // src/mutation/engine.ts
102211
102314
  import { spawnSync as spawnSync3 } from "node:child_process";
102212
- import { unlinkSync as unlinkSync16, writeFileSync as writeFileSync25 } from "node:fs";
102315
+ import { unlinkSync as unlinkSync16, writeFileSync as writeFileSync26 } from "node:fs";
102213
102316
  import * as path130 from "node:path";
102214
102317
 
102215
102318
  // src/mutation/equivalence.ts
@@ -102352,7 +102455,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102352
102455
  const safeId2 = patch.id.replace(/[^a-zA-Z0-9_-]/g, "_");
102353
102456
  patchFile = path130.join(workingDir, `.mutation_patch_${safeId2}.diff`);
102354
102457
  try {
102355
- writeFileSync25(patchFile, patch.patch);
102458
+ writeFileSync26(patchFile, patch.patch);
102356
102459
  } catch (writeErr) {
102357
102460
  error93 = `Failed to write patch file: ${writeErr}`;
102358
102461
  outcome = "error";
@@ -103865,7 +103968,27 @@ function checkReviewerGate(taskId, workingDirectory, stageBParallelEnabled = fal
103865
103968
  }
103866
103969
  }
103867
103970
  const currentStateStr = stateEntries.length > 0 ? stateEntries.join(", ") : "no active sessions";
103868
- const finalReason = evidenceIncompleteReason ?? `Task ${taskId} has not passed QA gates. Current state by session: [${currentStateStr}]. Missing required state: tests_run or complete in at least one valid session. Do not write directly to plan files — use update_task_status after running the reviewer and test_engineer agents.`;
103971
+ const chainEntries = [];
103972
+ for (const [sessionId, chain] of swarmState.delegationChains) {
103973
+ const session = swarmState.agentSessions.get(sessionId);
103974
+ if (session && (session.currentTaskId === taskId || session.lastCoderDelegationTaskId === taskId)) {
103975
+ const targets = chain.map((d) => stripKnownSwarmPrefix(d.to));
103976
+ chainEntries.push(`${sessionId}: [${targets.join(", ")}]`);
103977
+ }
103978
+ }
103979
+ const chainSummary = chainEntries.length > 0 ? chainEntries.join("; ") : "no chains for this task";
103980
+ const rehydratedSessionCount = [
103981
+ ...swarmState.agentSessions.values()
103982
+ ].filter((s) => s.sessionRehydratedAt > 0).length;
103983
+ const finalReason = [
103984
+ `Task ${taskId} has not passed QA gates.`,
103985
+ ` Session states: [${currentStateStr}].`,
103986
+ ` Delegation chains: [${chainSummary}].`,
103987
+ ` Evidence: [${evidenceIncompleteReason ?? "no evidence file found"}].`,
103988
+ ` Rehydrated sessions: ${rehydratedSessionCount}.`,
103989
+ ` Missing required state: tests_run or complete.`
103990
+ ].join(`
103991
+ `);
103869
103992
  telemetry.gateFailed("", "qa_gate", taskId, evidenceIncompleteReason ? `Missing gates: evidence incomplete` : `Missing state: tests_run or complete`);
103870
103993
  return {
103871
103994
  blocked: true,
@@ -103887,7 +104010,7 @@ async function checkReviewerGateWithScope(taskId, workingDirectory, sessionID) {
103887
104010
  ${scopeWarning}` : scopeWarning
103888
104011
  };
103889
104012
  }
103890
- function recoverTaskStateFromDelegations(taskId) {
104013
+ function recoverTaskStateFromDelegations(taskId, directory) {
103891
104014
  let hasReviewer = false;
103892
104015
  let hasTestEngineer = false;
103893
104016
  for (const [sessionId, chain] of swarmState.delegationChains) {
@@ -103902,8 +104025,24 @@ function recoverTaskStateFromDelegations(taskId) {
103902
104025
  }
103903
104026
  }
103904
104027
  }
104028
+ if ((!hasReviewer || !hasTestEngineer) && directory) {
104029
+ try {
104030
+ const evidence = readTaskEvidenceRaw(directory, taskId);
104031
+ if (evidence && evidence.gates && Array.isArray(evidence.required_gates)) {
104032
+ if (evidence.gates["reviewer"] != null)
104033
+ hasReviewer = true;
104034
+ if (evidence.gates["test_engineer"] != null)
104035
+ hasTestEngineer = true;
104036
+ }
104037
+ } catch {}
104038
+ }
103905
104039
  if (!hasReviewer && !hasTestEngineer)
103906
104040
  return;
104041
+ if (swarmState.agentSessions.size === 0) {
104042
+ try {
104043
+ startAgentSession("recovery-session", "architect");
104044
+ } catch {}
104045
+ }
103907
104046
  for (const [, session] of swarmState.agentSessions) {
103908
104047
  if (!(session.taskWorkflowStates instanceof Map))
103909
104048
  continue;
@@ -104047,7 +104186,7 @@ async function executeUpdateTaskStatus(args2, fallbackDir, ctx) {
104047
104186
  } catch {}
104048
104187
  }
104049
104188
  if (args2.status === "completed") {
104050
- recoverTaskStateFromDelegations(args2.task_id);
104189
+ recoverTaskStateFromDelegations(args2.task_id, directory);
104051
104190
  let phaseRequiresReviewer = true;
104052
104191
  try {
104053
104192
  const planPath = path137.join(directory, ".swarm", "plan.json");
@@ -105762,12 +105901,20 @@ async function initializeOpenCodeSwarm(ctx) {
105762
105901
  agent: input.agent,
105763
105902
  sessionID: input.sessionID
105764
105903
  }, KnowledgeApplicationConfigSchema.parse(config3.knowledge_application ?? {}));
105765
- await skillPropagationGateBefore(ctx.directory, {
105904
+ const skillResult = await skillPropagationGateBefore(ctx.directory, {
105766
105905
  tool: input.tool,
105767
105906
  agent: input.agent,
105768
105907
  sessionID: input.sessionID,
105769
105908
  args: input.args
105770
105909
  }, { enabled: true });
105910
+ if (skillResult.blocked) {
105911
+ throw new Error(skillResult.reason ?? "Blocked by skill propagation gate");
105912
+ }
105913
+ if (skillResult.reason) {
105914
+ const skillSession = ensureAgentSession(input.sessionID, swarmState.activeAgent.get(input.sessionID) ?? ORCHESTRATOR_NAME);
105915
+ skillSession.pendingAdvisoryMessages ??= [];
105916
+ skillSession.pendingAdvisoryMessages.push(skillResult.reason);
105917
+ }
105771
105918
  if (swarmState.lastBudgetPct >= 50) {
105772
105919
  const pressureSession = ensureAgentSession(input.sessionID, swarmState.activeAgent.get(input.sessionID) ?? ORCHESTRATOR_NAME);
105773
105920
  if (!pressureSession.contextPressureWarningSent) {
@@ -59,6 +59,8 @@ export interface SerializedAgentSession {
59
59
  fullAutoLastQuestionHash?: string | null;
60
60
  /** Timestamp when session was rehydrated from snapshot (0 if never rehydrated) */
61
61
  sessionRehydratedAt?: number;
62
+ /** Stage B completion tracking: per-task set of completed Stage B agents. Optional for backward compat with old snapshots. */
63
+ stageBCompletion?: Record<string, string[]>;
62
64
  }
63
65
  /**
64
66
  * Minimal interface for serialized InvocationWindow
@@ -71,9 +71,14 @@ export declare function checkReviewerGateWithScope(taskId: string, workingDirect
71
71
  * missing), this function advances the task state so that checkReviewerGate can
72
72
  * make an accurate decision without attributing unrelated delegation activity.
73
73
  *
74
+ * Falls back to reading durable evidence files when delegation chains are empty
75
+ * (e.g., after a crash or session restart without snapshot). This ensures
76
+ * recovery works even when no in-memory delegation history exists.
77
+ *
74
78
  * @param taskId - The task ID to recover state for
79
+ * @param directory - Optional project directory for evidence file fallback
75
80
  */
76
- export declare function recoverTaskStateFromDelegations(taskId: string): void;
81
+ export declare function recoverTaskStateFromDelegations(taskId: string, directory?: string): void;
77
82
  /**
78
83
  * Result of the council-gate check used when transitioning to 'completed'.
79
84
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.22.1",
3
+ "version": "7.23.1",
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",