opencode-swarm 7.29.2 → 7.29.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.29.2",
37
+ version: "7.29.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",
@@ -505,9 +505,7 @@ function bunSpawn(cmd, options) {
505
505
  return proc.exitCode;
506
506
  },
507
507
  kill(signal) {
508
- try {
509
- killChild(signal);
510
- } catch {}
508
+ killChild(signal);
511
509
  }
512
510
  };
513
511
  }
@@ -19859,15 +19857,36 @@ function validateProjectRoot(directory) {
19859
19857
  throw new Error(`Cannot verify project root for "${directory}" \u2014 directory may not exist or is inaccessible`);
19860
19858
  }
19861
19859
  let current = resolved;
19860
+ let depth = 0;
19862
19861
  while (true) {
19862
+ if (depth >= MAX_DEPTH)
19863
+ break;
19864
+ depth++;
19863
19865
  const parent = path7.dirname(current);
19864
19866
  if (parent === current)
19865
19867
  break;
19866
19868
  const parentSwarm = path7.join(parent, ".swarm");
19867
19869
  try {
19868
19870
  if (statSync4(parentSwarm).isDirectory()) {
19869
- warn(`[evidence] Rejecting write to subdirectory "${resolved}" \u2014 parent "${parent}" already contains .swarm/`);
19870
- throw new Error(`Cannot write evidence in "${resolved}" \u2014 parent directory "${parent}" already contains a .swarm/ folder. Evidence must be written to the project root.`);
19871
+ let hasProjectIndicator = false;
19872
+ for (const indicator of PROJECT_INDICATORS) {
19873
+ try {
19874
+ const indicatorStat = statSync4(path7.join(parent, indicator));
19875
+ if (indicatorStat.isFile() || indicatorStat.isDirectory()) {
19876
+ hasProjectIndicator = true;
19877
+ break;
19878
+ }
19879
+ } catch (error49) {
19880
+ if (error49 instanceof Error && "code" in error49 && error49.code === "ENOENT") {} else {
19881
+ hasProjectIndicator = true;
19882
+ break;
19883
+ }
19884
+ }
19885
+ }
19886
+ if (hasProjectIndicator) {
19887
+ warn(`[evidence] Rejecting write to subdirectory "${resolved}" \u2014 parent "${parent}" already contains .swarm/`);
19888
+ throw new Error(`Cannot write evidence in "${resolved}" \u2014 parent directory "${parent}" already contains a .swarm/ folder. Evidence must be written to the project root.`);
19889
+ }
19871
19890
  }
19872
19891
  } catch (error49) {
19873
19892
  if (error49 instanceof Error && error49.message.startsWith("Cannot write evidence")) {
@@ -20110,7 +20129,7 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
20110
20129
  }
20111
20130
  return archived;
20112
20131
  }
20113
- var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP, _internals5;
20132
+ var VALID_EVIDENCE_TYPES, sanitizeTaskId2, MAX_DEPTH = 20, PROJECT_INDICATORS, LEGACY_TASK_COMPLEXITY_MAP, _internals5;
20114
20133
  var init_manager2 = __esm(() => {
20115
20134
  init_zod();
20116
20135
  init_evidence_schema();
@@ -20135,6 +20154,19 @@ var init_manager2 = __esm(() => {
20135
20154
  "secretscan"
20136
20155
  ];
20137
20156
  sanitizeTaskId2 = sanitizeTaskId;
20157
+ PROJECT_INDICATORS = [
20158
+ "package.json",
20159
+ ".git",
20160
+ ".opencode",
20161
+ "Cargo.toml",
20162
+ "go.mod",
20163
+ "pyproject.toml",
20164
+ "Gemfile",
20165
+ "composer.json",
20166
+ "pom.xml",
20167
+ "build.gradle",
20168
+ "CMakeLists.txt"
20169
+ ];
20138
20170
  LEGACY_TASK_COMPLEXITY_MAP = {
20139
20171
  low: "simple",
20140
20172
  medium: "moderate",
@@ -40695,10 +40727,10 @@ function detectStraySwarmDirs(projectRoot) {
40695
40727
  "__pycache__",
40696
40728
  ".tox"
40697
40729
  ]);
40698
- const MAX_DEPTH = 10;
40730
+ const MAX_DEPTH2 = 10;
40699
40731
  const MAX_CONTENTS_ENTRIES = 20;
40700
40732
  function walk(dir, depth) {
40701
- if (depth > MAX_DEPTH)
40733
+ if (depth > MAX_DEPTH2)
40702
40734
  return;
40703
40735
  let entries;
40704
40736
  try {
@@ -46282,7 +46314,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
46282
46314
  const coverage = opts.coverage ?? false;
46283
46315
  switch (framework) {
46284
46316
  case "bun": {
46285
- const args = ["bun", "--smol", "test"];
46317
+ const args = ["bun", "test"];
46286
46318
  if (coverage)
46287
46319
  args.push("--coverage");
46288
46320
  if (scope !== "all" && files.length > 0)
@@ -48715,7 +48747,7 @@ function getTargetedExecutionUnsupportedReason(framework) {
48715
48747
  function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48716
48748
  switch (framework) {
48717
48749
  case "bun": {
48718
- const args = ["bun", "--smol", "test"];
48750
+ const args = ["bun", "test"];
48719
48751
  if (coverage)
48720
48752
  args.push("--coverage");
48721
48753
  if (scope !== "all" && files.length > 0) {
@@ -49252,24 +49284,17 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
49252
49284
  const proc = bunSpawn(command, {
49253
49285
  stdout: "pipe",
49254
49286
  stderr: "pipe",
49255
- stdin: "ignore",
49256
- cwd,
49257
- killProcessTree: true
49258
- });
49259
- let timeoutHandle;
49260
- const timeoutPromise = new Promise((resolve14) => {
49261
- timeoutHandle = setTimeout(() => {
49262
- proc.kill();
49263
- resolve14(-1);
49264
- }, timeout_ms);
49287
+ cwd
49265
49288
  });
49289
+ const timeoutPromise = new Promise((resolve14) => setTimeout(() => {
49290
+ proc.kill();
49291
+ resolve14(-1);
49292
+ }, timeout_ms));
49266
49293
  const [exitCode, stdoutResult, stderrResult] = await Promise.all([
49267
49294
  Promise.race([proc.exited, timeoutPromise]),
49268
49295
  readBoundedStream(proc.stdout, MAX_OUTPUT_BYTES3),
49269
49296
  readBoundedStream(proc.stderr, MAX_OUTPUT_BYTES3)
49270
49297
  ]);
49271
- if (timeoutHandle !== undefined)
49272
- clearTimeout(timeoutHandle);
49273
49298
  const duration_ms = Date.now() - startTime;
49274
49299
  let output = stdoutResult.text;
49275
49300
  if (stderrResult.text) {
@@ -49572,6 +49597,7 @@ var init_test_runner = __esm(() => {
49572
49597
  files: exports_external.array(exports_external.string()).optional().describe('Specific files to test. For "convention", pass source files or direct test files. For "graph" and "impact", pass source files only.'),
49573
49598
  coverage: exports_external.boolean().optional().describe("Enable coverage reporting if supported"),
49574
49599
  timeout_ms: exports_external.number().optional().describe("Timeout in milliseconds (default 60000, max 300000)"),
49600
+ allow_full_suite: exports_external.boolean().optional().describe('Explicit opt-in for scope "all". Required because full-suite output can destabilize SSE streaming.'),
49575
49601
  working_directory: exports_external.string().optional().describe("Explicit project root directory. When provided, tests run relative to this path instead of the plugin context directory. Use this when CWD differs from the actual project root.")
49576
49602
  },
49577
49603
  async execute(args, directory) {
@@ -49645,8 +49671,7 @@ var init_test_runner = __esm(() => {
49645
49671
  }
49646
49672
  const scope = args.scope || "all";
49647
49673
  if (scope === "all") {
49648
- const fullSuiteAllowed = process.env.SWARM_ALLOW_FULL_SUITE === "1" || process.env.SWARM_ALLOW_FULL_SUITE === "true";
49649
- if (!fullSuiteAllowed) {
49674
+ if (!process.env.SWARM_ALLOW_FULL_SUITE) {
49650
49675
  const errorResult = {
49651
49676
  success: false,
49652
49677
  framework: "none",
@@ -38,11 +38,17 @@ export declare function isQualityBudgetEvidence(evidence: Evidence): evidence is
38
38
  export declare function isSecretscanEvidence(evidence: Evidence): evidence is SecretscanEvidence;
39
39
  import { sanitizeTaskId as _sanitizeTaskId } from '../validation/task-id';
40
40
  export declare const sanitizeTaskId: typeof _sanitizeTaskId;
41
+ /** Maximum depth to walk up the directory tree before stopping (fail-open). */
42
+ export declare const MAX_DEPTH = 20;
43
+ /** File/directory names that indicate a real project root. */
44
+ export declare const PROJECT_INDICATORS: readonly ["package.json", ".git", ".opencode", "Cargo.toml", "go.mod", "pyproject.toml", "Gemfile", "composer.json", "pom.xml", "build.gradle", "CMakeLists.txt"];
41
45
  /**
42
46
  * Defense-in-depth: verify that `directory` is the project root and not a subdirectory
43
47
  * of a project that already has a .swarm/ at its root.
44
- * Walks up the directory tree to filesystem root looking for a parent .swarm/ directory.
45
- * @throws Error if a parent directory contains .swarm/
48
+ * Walks up the directory tree (bounded by MAX_DEPTH) looking for a parent .swarm/ directory.
49
+ * When .swarm/ is found, checks for at least one PROJECT_INDICATORS entry to distinguish
50
+ * real projects from stray artifacts (e.g. `C:\.swarm`).
51
+ * @throws Error if a parent directory contains both .swarm/ and a project indicator
46
52
  */
47
53
  export declare function validateProjectRoot(directory: string): void;
48
54
  /**
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ var package_default;
48
48
  var init_package = __esm(() => {
49
49
  package_default = {
50
50
  name: "opencode-swarm",
51
- version: "7.29.2",
51
+ version: "7.29.4",
52
52
  description: "Architect-centric agentic swarm plugin for OpenCode - hub-and-spoke orchestration with SME consultation, code generation, and QA review",
53
53
  main: "dist/index.js",
54
54
  types: "dist/index.d.ts",
@@ -16715,9 +16715,7 @@ function bunSpawn(cmd, options) {
16715
16715
  return proc.exitCode;
16716
16716
  },
16717
16717
  kill(signal) {
16718
- try {
16719
- killChild(signal);
16720
- } catch {}
16718
+ killChild(signal);
16721
16719
  }
16722
16720
  };
16723
16721
  }
@@ -16969,6 +16967,13 @@ var init_telemetry = __esm(() => {
16969
16967
  gatePassed(sessionId, gate, taskId) {
16970
16968
  _internals4.emit("gate_passed", { sessionId, gate, taskId });
16971
16969
  },
16970
+ gateParseError(taskId, error49) {
16971
+ _internals4.emit("gate_parse_error", {
16972
+ taskId,
16973
+ errorName: error49.name,
16974
+ errorMessage: error49.message.slice(0, 200)
16975
+ });
16976
+ },
16972
16977
  gateFailed(sessionId, gate, taskId, reason) {
16973
16978
  _internals4.emit("gate_failed", { sessionId, gate, taskId, reason });
16974
16979
  },
@@ -20687,15 +20692,36 @@ function validateProjectRoot(directory) {
20687
20692
  throw new Error(`Cannot verify project root for "${directory}" — directory may not exist or is inaccessible`);
20688
20693
  }
20689
20694
  let current = resolved;
20695
+ let depth = 0;
20690
20696
  while (true) {
20697
+ if (depth >= MAX_DEPTH)
20698
+ break;
20699
+ depth++;
20691
20700
  const parent = path8.dirname(current);
20692
20701
  if (parent === current)
20693
20702
  break;
20694
20703
  const parentSwarm = path8.join(parent, ".swarm");
20695
20704
  try {
20696
20705
  if (statSync5(parentSwarm).isDirectory()) {
20697
- warn(`[evidence] Rejecting write to subdirectory "${resolved}" — parent "${parent}" already contains .swarm/`);
20698
- throw new Error(`Cannot write evidence in "${resolved}" — parent directory "${parent}" already contains a .swarm/ folder. Evidence must be written to the project root.`);
20706
+ let hasProjectIndicator = false;
20707
+ for (const indicator of PROJECT_INDICATORS) {
20708
+ try {
20709
+ const indicatorStat = statSync5(path8.join(parent, indicator));
20710
+ if (indicatorStat.isFile() || indicatorStat.isDirectory()) {
20711
+ hasProjectIndicator = true;
20712
+ break;
20713
+ }
20714
+ } catch (error49) {
20715
+ if (error49 instanceof Error && "code" in error49 && error49.code === "ENOENT") {} else {
20716
+ hasProjectIndicator = true;
20717
+ break;
20718
+ }
20719
+ }
20720
+ }
20721
+ if (hasProjectIndicator) {
20722
+ warn(`[evidence] Rejecting write to subdirectory "${resolved}" — parent "${parent}" already contains .swarm/`);
20723
+ throw new Error(`Cannot write evidence in "${resolved}" — parent directory "${parent}" already contains a .swarm/ folder. Evidence must be written to the project root.`);
20724
+ }
20699
20725
  }
20700
20726
  } catch (error49) {
20701
20727
  if (error49 instanceof Error && error49.message.startsWith("Cannot write evidence")) {
@@ -20938,7 +20964,7 @@ async function archiveEvidence(directory, maxAgeDays, maxBundles) {
20938
20964
  }
20939
20965
  return archived;
20940
20966
  }
20941
- var VALID_EVIDENCE_TYPES, sanitizeTaskId2, LEGACY_TASK_COMPLEXITY_MAP, _internals8;
20967
+ var VALID_EVIDENCE_TYPES, sanitizeTaskId2, MAX_DEPTH = 20, PROJECT_INDICATORS, LEGACY_TASK_COMPLEXITY_MAP, _internals8;
20942
20968
  var init_manager2 = __esm(() => {
20943
20969
  init_zod();
20944
20970
  init_evidence_schema();
@@ -20963,6 +20989,19 @@ var init_manager2 = __esm(() => {
20963
20989
  "secretscan"
20964
20990
  ];
20965
20991
  sanitizeTaskId2 = sanitizeTaskId;
20992
+ PROJECT_INDICATORS = [
20993
+ "package.json",
20994
+ ".git",
20995
+ ".opencode",
20996
+ "Cargo.toml",
20997
+ "go.mod",
20998
+ "pyproject.toml",
20999
+ "Gemfile",
21000
+ "composer.json",
21001
+ "pom.xml",
21002
+ "build.gradle",
21003
+ "CMakeLists.txt"
21004
+ ];
20966
21005
  LEGACY_TASK_COMPLEXITY_MAP = {
20967
21006
  low: "simple",
20968
21007
  medium: "moderate",
@@ -38741,12 +38780,15 @@ function getEvidencePath(directory, taskId) {
38741
38780
  assertValidTaskId(taskId);
38742
38781
  return path12.join(getEvidenceDir(directory), `${taskId}.json`);
38743
38782
  }
38744
- function readExisting(evidencePath) {
38783
+ function readExisting(evidencePath, taskId) {
38745
38784
  try {
38746
38785
  const raw = readFileSync5(evidencePath, "utf-8");
38747
38786
  return TaskEvidenceSchema.parse(JSON.parse(raw));
38748
- } catch {
38749
- return null;
38787
+ } catch (error49) {
38788
+ if (error49.code === "ENOENT")
38789
+ return null;
38790
+ telemetry.gateParseError(taskId, error49);
38791
+ throw error49;
38750
38792
  }
38751
38793
  }
38752
38794
  async function atomicWrite2(targetPath, content) {
@@ -38767,7 +38809,13 @@ async function recordGateEvidence(directory, taskId, gate, sessionId, turbo) {
38767
38809
  const lockRelPath = path12.join("evidence", `${taskId}.json`);
38768
38810
  await withEvidenceLock(directory, lockRelPath, gate, taskId, async () => {
38769
38811
  const evidencePath = getEvidencePath(directory, taskId);
38770
- const existing = readExisting(evidencePath);
38812
+ let existing = null;
38813
+ try {
38814
+ existing = readExisting(evidencePath, taskId);
38815
+ } catch (error49) {
38816
+ telemetry.gateParseError(taskId, error49);
38817
+ throw error49;
38818
+ }
38771
38819
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, gate) : deriveRequiredGates(gate);
38772
38820
  const updated = {
38773
38821
  taskId,
@@ -38793,7 +38841,13 @@ async function recordAgentDispatch(directory, taskId, agentType, turbo) {
38793
38841
  const lockRelPath = path12.join("evidence", `${taskId}.json`);
38794
38842
  await withEvidenceLock(directory, lockRelPath, agentType, taskId, async () => {
38795
38843
  const evidencePath = getEvidencePath(directory, taskId);
38796
- const existing = readExisting(evidencePath);
38844
+ let existing = null;
38845
+ try {
38846
+ existing = readExisting(evidencePath, taskId);
38847
+ } catch (error49) {
38848
+ telemetry.gateParseError(taskId, error49);
38849
+ throw error49;
38850
+ }
38797
38851
  const requiredGates = existing ? expandRequiredGates(existing.required_gates, agentType) : deriveRequiredGates(agentType);
38798
38852
  const updated = {
38799
38853
  taskId,
@@ -38807,7 +38861,7 @@ async function recordAgentDispatch(directory, taskId, agentType, turbo) {
38807
38861
  async function readTaskEvidence(directory, taskId) {
38808
38862
  try {
38809
38863
  assertValidTaskId(taskId);
38810
- return readExisting(getEvidencePath(directory, taskId));
38864
+ return readExisting(getEvidencePath(directory, taskId), taskId);
38811
38865
  } catch {
38812
38866
  return null;
38813
38867
  }
@@ -39003,46 +39057,7 @@ async function buildParallelExecutionGuidance(directory, sessionID, session) {
39003
39057
  if (eligible.length === 0) {
39004
39058
  return `[PARALLEL EXECUTION PROFILE] parallelization_enabled=true max_concurrent_tasks=${maxConcurrent}; no dependency-ready pending tasks are available for a new coder slot. Continue the current task/gate.`;
39005
39059
  }
39006
- return `[PARALLEL EXECUTION PROFILE] parallelization_enabled=true max_concurrent_tasks=${maxConcurrent}; ${occupied.size} slot(s) occupied. Eligible now: ${eligible.join(", ")}. [NEXT] dispatch up to ${availableSlots} eligible coder task(s) before waiting; for each dispatched task, call update_task_status(in_progress), call declare_scope, then send the coder Task. Preserve ONE atomic task per coder Task call.`;
39007
- }
39008
- async function buildPlanContinuationGuidance(directory) {
39009
- if (!directory)
39010
- return null;
39011
- const plan = await loadPlanJsonOnly(directory);
39012
- const currentTaskId = getPlanContinuationTaskId(plan);
39013
- if (!currentTaskId)
39014
- return null;
39015
- const sanitizedTaskId = sanitizeGuidanceValue(currentTaskId, 32);
39016
- return `[NEXT] Continue plan task ${sanitizedTaskId}: if it is not already in progress, call update_task_status with task_id="${sanitizedTaskId}" and status="in_progress"; ` + `then call declare_scope for the task files and dispatch coder Task call(s) according to the execution profile. ` + `Preserve ONE atomic task per coder Task call; when parallel execution is enabled, use available coder slots instead of forcing a single coder.`;
39017
- }
39018
- function sanitizeGuidanceValue(value, maxLength) {
39019
- return value.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\[ \]/g, "()").replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, maxLength);
39020
- }
39021
- function getPlanContinuationTaskId(plan) {
39022
- if (!plan)
39023
- return;
39024
- const currentPhase = plan.current_phase ?? 1;
39025
- const phase = plan.phases.find((p) => p.id === currentPhase);
39026
- if (!phase)
39027
- return;
39028
- const sortedTasks = [...phase.tasks].sort((a, b) => comparePlanTaskIds(a.id, b.id));
39029
- const inProgress = sortedTasks.find((task) => task.status === "in_progress");
39030
- if (inProgress)
39031
- return inProgress.id;
39032
- const incomplete = sortedTasks.find((task) => task.status !== "completed" && task.status !== "closed");
39033
- return incomplete?.id;
39034
- }
39035
- function comparePlanTaskIds(a, b) {
39036
- const partsA = a.split(".").map((part) => Number.parseInt(part, 10));
39037
- const partsB = b.split(".").map((part) => Number.parseInt(part, 10));
39038
- const maxLength = Math.max(partsA.length, partsB.length);
39039
- for (let i2 = 0;i2 < maxLength; i2++) {
39040
- const numA = partsA[i2] ?? 0;
39041
- const numB = partsB[i2] ?? 0;
39042
- if (numA !== numB)
39043
- return numA - numB;
39044
- }
39045
- return 0;
39060
+ return `[PARALLEL EXECUTION PROFILE] parallelization_enabled=true max_concurrent_tasks=${maxConcurrent}; ${occupied.size} slot(s) occupied. Eligible now: ${eligible.join(", ")}. [NEXT] dispatch up to ${availableSlots} eligible coder task(s) before waiting; preserve ONE task per coder call and call declare_scope for each task.`;
39046
39061
  }
39047
39062
  function isParallelGuidancePhaseComplete(phase) {
39048
39063
  return phase.status === "complete" || phase.status === "completed" || phase.status === "closed";
@@ -39620,7 +39635,6 @@ ${trimComment}${after}`;
39620
39635
  const deliberationSession = ensureAgentSession(deliberationSessionID);
39621
39636
  const lastGate = deliberationSession.lastGateOutcome;
39622
39637
  const parallelGuidance = await buildParallelExecutionGuidance(directory, deliberationSessionID, deliberationSession);
39623
- const planContinuationGuidance = parallelGuidance === null ? await buildPlanContinuationGuidance(directory) : null;
39624
39638
  const taskAwaitingCompletion = await findTaskAwaitingCompletion(directory, deliberationSession);
39625
39639
  let guidance;
39626
39640
  if (taskAwaitingCompletion) {
@@ -39628,12 +39642,12 @@ ${trimComment}${after}`;
39628
39642
  [NEXT] Print the task completion checklist, then call update_task_status with task_id="${taskAwaitingCompletion}" and status="completed" before declare_scope or starting another task.`;
39629
39643
  } else if (lastGate?.taskId) {
39630
39644
  const gateResult = lastGate.passed ? "PASSED" : "FAILED";
39631
- const sanitizedGate = sanitizeGuidanceValue(lastGate.gate, 64);
39632
- const sanitizedTaskId = sanitizeGuidanceValue(lastGate.taskId, 32);
39645
+ const sanitizedGate = lastGate.gate.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\[ \]/g, "()").replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 64);
39646
+ const sanitizedTaskId = lastGate.taskId.replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\[/g, "(").replace(/\]/g, ")").replace(/[\r\n]/g, " ").slice(0, 32);
39633
39647
  guidance = `[Last gate: ${sanitizedGate} ${gateResult} for task ${sanitizedTaskId}]
39634
39648
  ${parallelGuidance ?? "[NEXT] Execute the next gate for the current task."}`;
39635
39649
  } else {
39636
- guidance = parallelGuidance ?? planContinuationGuidance ?? "[NEXT] Begin the first plan task and run gates sequentially.";
39650
+ guidance = parallelGuidance ?? "[NEXT] Begin the first plan task and run gates sequentially.";
39637
39651
  }
39638
39652
  const systemMsgIdx = messages.findIndex((m) => m && m.info?.role === "system");
39639
39653
  const insertIdx = systemMsgIdx >= 0 ? systemMsgIdx + 1 : 0;
@@ -61639,10 +61653,10 @@ function detectStraySwarmDirs(projectRoot) {
61639
61653
  "__pycache__",
61640
61654
  ".tox"
61641
61655
  ]);
61642
- const MAX_DEPTH = 10;
61656
+ const MAX_DEPTH2 = 10;
61643
61657
  const MAX_CONTENTS_ENTRIES = 20;
61644
61658
  function walk(dir, depth) {
61645
- if (depth > MAX_DEPTH)
61659
+ if (depth > MAX_DEPTH2)
61646
61660
  return;
61647
61661
  let entries;
61648
61662
  try {
@@ -67415,7 +67429,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
67415
67429
  const coverage = opts.coverage ?? false;
67416
67430
  switch (framework) {
67417
67431
  case "bun": {
67418
- const args2 = ["bun", "--smol", "test"];
67432
+ const args2 = ["bun", "test"];
67419
67433
  if (coverage)
67420
67434
  args2.push("--coverage");
67421
67435
  if (scope !== "all" && files.length > 0)
@@ -69848,7 +69862,7 @@ function getTargetedExecutionUnsupportedReason(framework) {
69848
69862
  function buildTestCommand2(framework, scope, files, coverage, baseDir) {
69849
69863
  switch (framework) {
69850
69864
  case "bun": {
69851
- const args2 = ["bun", "--smol", "test"];
69865
+ const args2 = ["bun", "test"];
69852
69866
  if (coverage)
69853
69867
  args2.push("--coverage");
69854
69868
  if (scope !== "all" && files.length > 0) {
@@ -70385,24 +70399,17 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
70385
70399
  const proc = bunSpawn(command, {
70386
70400
  stdout: "pipe",
70387
70401
  stderr: "pipe",
70388
- stdin: "ignore",
70389
- cwd,
70390
- killProcessTree: true
70391
- });
70392
- let timeoutHandle;
70393
- const timeoutPromise = new Promise((resolve16) => {
70394
- timeoutHandle = setTimeout(() => {
70395
- proc.kill();
70396
- resolve16(-1);
70397
- }, timeout_ms);
70402
+ cwd
70398
70403
  });
70404
+ const timeoutPromise = new Promise((resolve16) => setTimeout(() => {
70405
+ proc.kill();
70406
+ resolve16(-1);
70407
+ }, timeout_ms));
70399
70408
  const [exitCode, stdoutResult, stderrResult] = await Promise.all([
70400
70409
  Promise.race([proc.exited, timeoutPromise]),
70401
70410
  readBoundedStream(proc.stdout, MAX_OUTPUT_BYTES3),
70402
70411
  readBoundedStream(proc.stderr, MAX_OUTPUT_BYTES3)
70403
70412
  ]);
70404
- if (timeoutHandle !== undefined)
70405
- clearTimeout(timeoutHandle);
70406
70413
  const duration_ms = Date.now() - startTime;
70407
70414
  let output = stdoutResult.text;
70408
70415
  if (stderrResult.text) {
@@ -70705,6 +70712,7 @@ var init_test_runner = __esm(() => {
70705
70712
  files: exports_external.array(exports_external.string()).optional().describe('Specific files to test. For "convention", pass source files or direct test files. For "graph" and "impact", pass source files only.'),
70706
70713
  coverage: exports_external.boolean().optional().describe("Enable coverage reporting if supported"),
70707
70714
  timeout_ms: exports_external.number().optional().describe("Timeout in milliseconds (default 60000, max 300000)"),
70715
+ allow_full_suite: exports_external.boolean().optional().describe('Explicit opt-in for scope "all". Required because full-suite output can destabilize SSE streaming.'),
70708
70716
  working_directory: exports_external.string().optional().describe("Explicit project root directory. When provided, tests run relative to this path instead of the plugin context directory. Use this when CWD differs from the actual project root.")
70709
70717
  },
70710
70718
  async execute(args2, directory) {
@@ -70778,8 +70786,7 @@ var init_test_runner = __esm(() => {
70778
70786
  }
70779
70787
  const scope = args2.scope || "all";
70780
70788
  if (scope === "all") {
70781
- const fullSuiteAllowed = process.env.SWARM_ALLOW_FULL_SUITE === "1" || process.env.SWARM_ALLOW_FULL_SUITE === "true";
70782
- if (!fullSuiteAllowed) {
70789
+ if (!process.env.SWARM_ALLOW_FULL_SUITE) {
70783
70790
  const errorResult = {
70784
70791
  success: false,
70785
70792
  framework: "none",
@@ -75971,8 +75978,6 @@ SECURITY_KEYWORDS: password, secret, token, credential, auth, login, encryption,
75971
75978
  {{AGENT_PREFIX}}test_engineer - Test generation AND execution (writes tests, runs them, reports PASS/FAIL)
75972
75979
  {{AGENT_PREFIX}}critic - Plan review gate (reviews plan BEFORE implementation)
75973
75980
  {{AGENT_PREFIX}}critic_sounding_board - Pre-escalation pushback (honest engineer review before user contact)
75974
- {{AGENT_PREFIX}}skill_improver - Low-frequency skill / knowledge / prompt improvement adviser
75975
- {{AGENT_PREFIX}}spec_writer - .swarm/spec.md authoring via spec_write
75976
75981
  {{AGENT_PREFIX}}docs - Documentation updates (README, API docs, guides — NOT .swarm/ files)
75977
75982
  {{AGENT_PREFIX}}designer - UI/UX design specs (scaffold generation for UI components — runs BEFORE coder on UI tasks)
75978
75983
 
@@ -76023,20 +76028,30 @@ For every applicable directive in the block:
76023
76028
 
76024
76029
  You may also call the \`knowledge_ack\` tool to record an outcome explicitly when chat-text markers would be ambiguous (e.g. inside structured tool args).
76025
76030
 
76026
- ## SKILL IMPROVER
76031
+ ## SKILL IMPROVER (low-frequency, expensive-model adviser)
76027
76032
 
76028
- \`{{AGENT_PREFIX}}skill_improver\` / \`skill_improve\`: rare, quota-bounded,
76029
- disabled by default, proposal-only. Use for repeated rejections,
76030
- \`KNOWLEDGE_IGNORED\`, stale skills, or spec drift.
76033
+ The \`skill_improver\` agent and the \`skill_improve\` tool exist for rare, deep
76034
+ review of accumulated knowledge / skills / spec / architect prompt. They are
76035
+ quota-bounded (default 10 calls/day) and disabled by default. Suggest running
76036
+ \`skill_improve\` only after one of:
76037
+ - repeated reviewer rejections in a row,
76038
+ - many \`KNOWLEDGE_IGNORED\` outcomes for the same cluster,
76039
+ - stale skills (no updates while their target area changed),
76040
+ - a fresh spec mismatch with shipped behaviour.
76031
76041
 
76032
76042
  When \`skill_improver.require_user_approval\` is true (default), ASK the user
76033
76043
  before running. Default outputs are proposals only — they never modify source.
76034
76044
 
76035
76045
  ## SPEC WRITER
76036
76046
 
76037
- For substantial spec authoring/revision, prefer \`{{AGENT_PREFIX}}spec_writer\`;
76038
- it writes via \`spec_write\`. Use for major specs or non-trivial
76039
- decomposition. Handle small touch-ups.
76047
+ For substantial spec authoring or revision, prefer delegating to the
76048
+ \`spec_writer\` agent (independent model from architect). It writes only via
76049
+ the safe \`spec_write\` tool. Use it when:
76050
+ - the user requests a new spec or major spec revision,
76051
+ - requirements decomposition is non-trivial,
76052
+ - you would otherwise inline-author \`.swarm/spec.md\` yourself.
76053
+
76054
+ Continue handling small touch-ups (typos, cross-references) inline.
76040
76055
 
76041
76056
  ### ANTI-RATIONALIZATION
76042
76057
  - ✗ "The coder already knows these conventions" → Skills contain project-specific rules the model cannot know from training. Always pass.
@@ -76217,12 +76232,11 @@ MODE: BRAINSTORM runs seven phases in strict order. Do not skip phases. Do not c
76217
76232
  - Exit with a design outline the user can skim in under two minutes.
76218
76233
 
76219
76234
  **Phase 5: SPEC WRITE + SELF-REVIEW (architect + reviewer).**
76220
- - Delegate substantial spec drafting to \`{{AGENT_PREFIX}}spec_writer\` with the chosen design, dialogue notes, SME context, and SPEC CONTENT RULES. The spec writer must persist \`.swarm/spec.md\` through \`spec_write\`.
76221
- - The spec must follow the same SPEC CONTENT RULES that MODE: SPECIFY uses: WHAT/WHY only, no tech stack, no implementation details, FR-### / SC-### numbering, Given/When/Then scenarios, \`[NEEDS CLARIFICATION]\` markers (max 3).
76235
+ - Generate \`.swarm/spec.md\` following the same SPEC CONTENT RULES that MODE: SPECIFY uses: WHAT/WHY only, no tech stack, no implementation details, FR-### / SC-### numbering, Given/When/Then scenarios, \`[NEEDS CLARIFICATION]\` markers (max 3).
76222
76236
  - Cross-reference design sections by name where relevant context helps (but keep HOW out of the spec).
76223
76237
  - Delegate to \`{{AGENT_PREFIX}}reviewer\` for an independent review of the draft spec. Reviewer must flag: requirements that encode HOW, untestable requirements, missing edge cases, silent assumptions.
76224
76238
  - Apply reviewer feedback. If reviewer rejects, iterate once and re-review. After two rounds, surface remaining disagreements to the user.
76225
- - Read back and lint the final spec after \`{{AGENT_PREFIX}}spec_writer\` writes it.
76239
+ - Write the final spec to \`.swarm/spec.md\`.
76226
76240
  - Exit when reviewer signs off (or user explicitly accepts remaining disagreements).
76227
76241
 
76228
76242
  **Phase 6: QA GATE SELECTION (architect, dialogue only).**
@@ -76294,7 +76308,7 @@ Activates when: user asks to "specify", "define requirements", "write a spec", o
76294
76308
  1b. Run CODEBASE REALITY CHECK for any codebase references mentioned by the user or implied by the feature. Skip if work is purely greenfield (no existing codebase to check). Report discrepancies before proceeding to explorer.
76295
76309
  2. Delegate to \`{{AGENT_PREFIX}}explorer\` to scan the codebase for relevant context (existing patterns, related code, affected areas).
76296
76310
  3. Delegate to \`{{AGENT_PREFIX}}sme\` for domain research on the feature area to surface known constraints, best practices, and integration concerns.
76297
- 4. Delegate substantial spec drafting to \`{{AGENT_PREFIX}}spec_writer\`. Include the user requirements, explorer findings, SME constraints, and these required contents:
76311
+ 4. Generate \`.swarm/spec.md\` capturing:
76298
76312
  - First line must be: \`# Specification: <feature-name>\`
76299
76313
  - Feature description: WHAT users need and WHY — never HOW to implement
76300
76314
  - User scenarios with acceptance criteria (Given/When/Then format)
@@ -76303,7 +76317,7 @@ Activates when: user asks to "specify", "define requirements", "write a spec", o
76303
76317
  - Key entities if data is involved (no schema or field definitions — entity names only)
76304
76318
  - Edge cases and known failure modes
76305
76319
  - \`[NEEDS CLARIFICATION]\` markers (max 3) for items where uncertainty could change scope, security, or core behavior; prefer informed defaults over asking
76306
- 5. Require \`{{AGENT_PREFIX}}spec_writer\` to write the spec via \`spec_write\`, then read back and lint \`.swarm/spec.md\`.
76320
+ 5. Write the spec to \`.swarm/spec.md\`.
76307
76321
  5b. **QA GATE SELECTION (dialogue only).**
76308
76322
  {{QA_GATE_DIALOGUE_SPECIFY}}
76309
76323
 
@@ -76458,7 +76472,6 @@ If .swarm/plan.md exists:
76458
76472
  - Update context.md Swarm field to "{{SWARM_ID}}"
76459
76473
  - Inform user: "Resuming project from [other] swarm. Cleared stale context. Ready to continue."
76460
76474
  - Resume at current task
76461
- Resume execution rule: after identifying the current task, do not restart broad discovery. Move into EXECUTE: call \`update_task_status\` if the task is not already in progress, call \`declare_scope\` for the task files, then dispatch coder Task call(s) according to \`execution_profile\`. Preserve ONE atomic task per coder Task call; parallel profiles may dispatch up to the available coder slots.
76462
76475
  If .swarm/plan.md does not exist → New project, proceed to MODE: CLARIFY
76463
76476
  If new project: Run \`complexity_hotspots\` tool (90 days) to generate a risk map. Note modules with recommendation "security_review" or "full_gates" in context.md for stricter QA gates during Phase 5. Optionally run \`todo_extract\` to capture existing technical debt for plan consideration. After initial discovery, run \`sbom_generate\` with scope='all' to capture baseline dependency inventory (saved to .swarm/evidence/sbom/).
76464
76477
 
@@ -79507,24 +79520,8 @@ function createSwarmAgents(swarmId, swarmConfig, isDefault, pluginConfig, projec
79507
79520
  const swarmIdentity = isDefault ? "default" : swarmId;
79508
79521
  const agentPrefix = prefix;
79509
79522
  architect.config.prompt = architect.config.prompt?.replace(/\{\{SWARM_ID\}\}/g, swarmIdentity).replace(/\{\{AGENT_PREFIX\}\}/g, agentPrefix).replace(/\{\{QA_RETRY_LIMIT\}\}/g, String(qaRetryLimit)).replace(/\{\{PROJECT_LANGUAGE\}\}/g, projectContext.PROJECT_LANGUAGE).replace(/\{\{PROJECT_FRAMEWORK\}\}/g, projectContext.PROJECT_FRAMEWORK).replace(/\{\{BUILD_CMD\}\}/g, projectContext.BUILD_CMD).replace(/\{\{TEST_CMD\}\}/g, projectContext.TEST_CMD).replace(/\{\{LINT_CMD\}\}/g, projectContext.LINT_CMD).replace(/\{\{ENTRY_POINTS\}\}/g, projectContext.ENTRY_POINTS).replace(/\{\{CODER_CONSTRAINTS\}\}/g, projectContext.CODER_CONSTRAINTS).replace(/\{\{TEST_CONSTRAINTS\}\}/g, projectContext.TEST_CONSTRAINTS).replace(/\{\{REVIEWER_CHECKLIST\}\}/g, projectContext.REVIEWER_CHECKLIST).replace(/\{\{PROJECT_CONTEXT_SECONDARY_LANGUAGES\}\}/g, projectContext.PROJECT_CONTEXT_SECONDARY_LANGUAGES);
79510
- const skillImproverEnabled = !isAgentDisabled("skill_improver", swarmAgents, swarmPrefix);
79511
- const specWriterEnabled = !isAgentDisabled("spec_writer", swarmAgents, swarmPrefix);
79512
- if (!skillImproverEnabled) {
79513
- architect.config.prompt = architect.config.prompt?.replace(`, ${agentPrefix}skill_improver`, "").replace(`
79514
- ${agentPrefix}skill_improver - Low-frequency skill / knowledge / prompt improvement adviser`, "").replace(/\n## SKILL IMPROVER[\s\S]*?(?=\n\n## SPEC WRITER)/, "");
79515
- }
79516
- if (!specWriterEnabled) {
79517
- const escapedAgentPrefix = agentPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
79518
- architect.config.prompt = architect.config.prompt?.replace(`, ${agentPrefix}spec_writer`, "").replace(`
79519
- ${agentPrefix}spec_writer - .swarm/spec.md authoring via spec_write`, "").replace(/\n## SPEC WRITER[\s\S]*?(?=\n\n### ANTI-RATIONALIZATION)/, "").replace(new RegExp(`- Delegate substantial spec drafting to \`${escapedAgentPrefix}spec_writer\`[^\\n]*\\n`, "g"), "- spec_writer is disabled. Ask the user to enable the spec_writer agent before creating or revising `.swarm/spec.md`.\n").replace(new RegExp(`4\\. Delegate substantial spec drafting to \`${escapedAgentPrefix}spec_writer\`[^\\n]*`), "4. spec_writer is disabled. Ask the user to enable the spec_writer agent before creating or revising `.swarm/spec.md`.").replace(new RegExp(`5\\. Require \`${escapedAgentPrefix}spec_writer\` to write the spec via \`spec_write\`, then read back and lint \`\\.swarm/spec\\.md\`\\.`), "5. Do not continue SPECIFY until spec_writer is available.").replace(new RegExp(`- Read back and lint the final spec after \`${escapedAgentPrefix}spec_writer\` writes it\\.`), "- Do not continue BRAINSTORM spec writing until spec_writer is available.");
79520
- }
79521
79523
  if (!isDefault) {
79522
79524
  architect.description = `[${swarmName}] ${architect.description}`;
79523
- const optionalAgentLines = [
79524
- skillImproverEnabled ? `- @${swarmId}_skill_improver (not @skill_improver)` : undefined,
79525
- specWriterEnabled ? `- @${swarmId}_spec_writer (not @spec_writer)` : undefined
79526
- ].filter((line) => Boolean(line)).join(`
79527
- `);
79528
79525
  const swarmHeader = `## ⚠️ YOU ARE THE ${swarmName.toUpperCase()} SWARM ARCHITECT
79529
79526
 
79530
79527
  Your swarm ID is "${swarmId}". ALL your agents have the "${swarmId}_" prefix:
@@ -79532,8 +79529,8 @@ Your swarm ID is "${swarmId}". ALL your agents have the "${swarmId}_" prefix:
79532
79529
  - @${swarmId}_coder (not @coder)
79533
79530
  - @${swarmId}_sme (not @sme)
79534
79531
  - @${swarmId}_reviewer (not @reviewer)
79535
- ${optionalAgentLines ? `${optionalAgentLines}
79536
- ` : ""}- etc.
79532
+ - @${swarmId}_spec_writer (not @spec_writer)
79533
+ - etc.
79537
79534
 
79538
79535
  CRITICAL: Agents without the "${swarmId}_" prefix DO NOT EXIST or belong to a DIFFERENT swarm.
79539
79536
  If you call @coder instead of @${swarmId}_coder, the call will FAIL or go to the wrong swarm.
@@ -1,4 +1,4 @@
1
- export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'phase_changed' | 'budget_updated' | 'model_fallback' | 'hard_limit_hit' | 'revision_limit_hit' | 'loop_detected' | 'scope_violation' | 'qa_skip_violation' | 'heartbeat' | 'turbo_mode_changed' | 'auto_oversight_escalation' | 'environment_detected' | 'evidence_lock_acquired' | 'evidence_lock_contended' | 'evidence_lock_stale_recovered' | 'plan_ledger_cas_retry' | 'plan_md_write_failed' | 'prm_pattern_detected' | 'prm_course_correction_injected' | 'prm_escalation_triggered' | 'prm_hard_stop';
1
+ export type TelemetryEvent = 'session_started' | 'session_ended' | 'agent_activated' | 'delegation_begin' | 'delegation_end' | 'task_state_changed' | 'gate_passed' | 'gate_failed' | 'gate_parse_error' | 'phase_changed' | 'budget_updated' | 'model_fallback' | 'hard_limit_hit' | 'revision_limit_hit' | 'loop_detected' | 'scope_violation' | 'qa_skip_violation' | 'heartbeat' | 'turbo_mode_changed' | 'auto_oversight_escalation' | 'environment_detected' | 'evidence_lock_acquired' | 'evidence_lock_contended' | 'evidence_lock_stale_recovered' | 'plan_ledger_cas_retry' | 'plan_md_write_failed' | 'prm_pattern_detected' | 'prm_course_correction_injected' | 'prm_escalation_triggered' | 'prm_hard_stop';
2
2
  export type TelemetryListener = (event: TelemetryEvent, data: Record<string, unknown>) => void;
3
3
  /** @internal - For testing only */
4
4
  export declare function resetTelemetryForTesting(): void;
@@ -39,6 +39,7 @@ export declare const telemetry: {
39
39
  delegationEnd(sessionId: string, agentName: string, taskId: string, result: string): void;
40
40
  taskStateChanged(sessionId: string, taskId: string, newState: string, oldState?: string): void;
41
41
  gatePassed(sessionId: string, gate: string, taskId: string): void;
42
+ gateParseError(taskId: string, error: Error): void;
42
43
  gateFailed(sessionId: string, gate: string, taskId: string, reason: string): void;
43
44
  phaseChanged(sessionId: string, oldPhase: number, newPhase: number): void;
44
45
  budgetUpdated(sessionId: string, budgetPct: number, agentName: string): void;
@@ -23,6 +23,7 @@ export interface TestRunnerArgs {
23
23
  files?: string[];
24
24
  coverage?: boolean;
25
25
  timeout_ms?: number;
26
+ allow_full_suite?: boolean;
26
27
  }
27
28
  export type RegressionOutcome = 'pass' | 'skip' | 'regression' | 'scope_exceeded' | 'error';
28
29
  export interface TestTotals {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.29.2",
3
+ "version": "7.29.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",