opencode-swarm 7.19.2 → 7.20.0

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.19.2",
37
+ version: "7.20.0",
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",
@@ -46785,18 +46785,36 @@ async function buildImpactMap(cwd) {
46785
46785
  await _internals22.saveImpactMap(cwd, impactMap);
46786
46786
  return impactMap;
46787
46787
  }
46788
- async function loadImpactMap(cwd) {
46788
+ async function loadImpactMap(cwd, options) {
46789
46789
  const cachePath = path34.join(cwd, ".swarm", "cache", "impact-map.json");
46790
46790
  if (fs17.existsSync(cachePath)) {
46791
46791
  try {
46792
46792
  const content = fs17.readFileSync(cachePath, "utf-8");
46793
46793
  const data = JSON.parse(content);
46794
- const map3 = data.map;
46795
- const generatedAt = new Date(data.generatedAt).getTime();
46796
- if (!_internals22.isCacheStale(map3, generatedAt)) {
46797
- return map3;
46794
+ if (data.map !== null && typeof data.map === "object" && !Array.isArray(data.map)) {
46795
+ const map3 = data.map;
46796
+ const hasValidValues = Object.values(map3).every((v) => Array.isArray(v) && v.every((item) => typeof item === "string"));
46797
+ if (hasValidValues) {
46798
+ const generatedAt = new Date(data.generatedAt).getTime();
46799
+ if (!_internals22.isCacheStale(map3, generatedAt)) {
46800
+ return map3;
46801
+ }
46802
+ if (options?.skipRebuild) {
46803
+ return map3;
46804
+ }
46805
+ }
46798
46806
  }
46799
- } catch {}
46807
+ if (options?.skipRebuild) {
46808
+ return {};
46809
+ }
46810
+ } catch {
46811
+ if (options?.skipRebuild) {
46812
+ return {};
46813
+ }
46814
+ }
46815
+ }
46816
+ if (options?.skipRebuild) {
46817
+ return {};
46800
46818
  }
46801
46819
  return _internals22.buildImpactMap(cwd);
46802
46820
  }
@@ -46813,7 +46831,7 @@ async function saveImpactMap(cwd, impactMap) {
46813
46831
  };
46814
46832
  fs17.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
46815
46833
  }
46816
- async function analyzeImpact(changedFiles, cwd) {
46834
+ async function analyzeImpact(changedFiles, cwd, budget) {
46817
46835
  if (!Array.isArray(changedFiles)) {
46818
46836
  const emptyMap = {};
46819
46837
  return {
@@ -46827,24 +46845,49 @@ async function analyzeImpact(changedFiles, cwd) {
46827
46845
  const impactMap = await _internals22.loadImpactMap(cwd);
46828
46846
  const impactedTestsSet = new Set;
46829
46847
  const untestedFiles = [];
46848
+ let visitedCount = 0;
46849
+ let budgetExceeded = false;
46830
46850
  for (const changedFile of validFiles) {
46851
+ if (budget !== undefined && visitedCount >= budget) {
46852
+ budgetExceeded = true;
46853
+ break;
46854
+ }
46831
46855
  const normalizedChanged = normalizePath(path34.resolve(changedFile));
46832
46856
  const tests = impactMap[normalizedChanged];
46833
46857
  if (tests && tests.length > 0) {
46834
46858
  for (const test of tests) {
46859
+ if (budget !== undefined && visitedCount >= budget) {
46860
+ budgetExceeded = true;
46861
+ break;
46862
+ }
46835
46863
  impactedTestsSet.add(test);
46864
+ visitedCount++;
46836
46865
  }
46866
+ if (budgetExceeded)
46867
+ break;
46837
46868
  } else {
46838
46869
  let found = false;
46839
46870
  for (const [sourcePath, tests2] of Object.entries(impactMap)) {
46871
+ if (budget !== undefined && visitedCount >= budget) {
46872
+ budgetExceeded = true;
46873
+ break;
46874
+ }
46840
46875
  if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
46841
46876
  for (const test of tests2) {
46877
+ if (budget !== undefined && visitedCount >= budget) {
46878
+ budgetExceeded = true;
46879
+ break;
46880
+ }
46842
46881
  impactedTestsSet.add(test);
46882
+ visitedCount++;
46843
46883
  }
46884
+ if (budgetExceeded)
46885
+ break;
46844
46886
  found = true;
46845
- break;
46846
46887
  }
46847
46888
  }
46889
+ if (budgetExceeded)
46890
+ break;
46848
46891
  if (!found) {
46849
46892
  untestedFiles.push(changedFile);
46850
46893
  }
@@ -46862,7 +46905,8 @@ async function analyzeImpact(changedFiles, cwd) {
46862
46905
  impactedTests,
46863
46906
  unrelatedTests,
46864
46907
  untestedFiles,
46865
- impactMap
46908
+ impactMap,
46909
+ budgetExceeded
46866
46910
  };
46867
46911
  }
46868
46912
  var IMPORT_REGEX_ES, IMPORT_REGEX_REQUIRE, IMPORT_REGEX_REEXPORT, TS_EXTENSIONS, PYTHON_EXTENSIONS, GO_EXTENSIONS, EXTENSIONS_TO_TRY, goModuleCache, _internals22;
@@ -47724,6 +47768,25 @@ var init_dispatch = __esm(() => {
47724
47768
  // src/tools/test-runner.ts
47725
47769
  import * as fs22 from "fs";
47726
47770
  import * as path39 from "path";
47771
+ async function estimateFanOut(sourceFiles, cwd) {
47772
+ try {
47773
+ const impactMap = await loadImpactMap(cwd, { skipRebuild: true });
47774
+ const uniqueTestFiles = new Set;
47775
+ for (const sourceFile of sourceFiles) {
47776
+ const resolvedPath = path39.resolve(cwd, sourceFile);
47777
+ const normalizedPath = resolvedPath.replace(/\\/g, "/");
47778
+ const testFiles = impactMap[normalizedPath];
47779
+ if (testFiles) {
47780
+ for (const testFile of testFiles) {
47781
+ uniqueTestFiles.add(testFile);
47782
+ }
47783
+ }
47784
+ }
47785
+ return { estimatedCount: uniqueTestFiles.size };
47786
+ } catch {
47787
+ return { estimatedCount: 0 };
47788
+ }
47789
+ }
47727
47790
  function isAbsolutePath(str) {
47728
47791
  if (str.startsWith("/"))
47729
47792
  return true;
@@ -49068,6 +49131,18 @@ var init_test_runner = __esm(() => {
49068
49131
  };
49069
49132
  return JSON.stringify(errorResult, null, 2);
49070
49133
  }
49134
+ const estimate = await estimateFanOut(sourceFiles, workingDir);
49135
+ if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
49136
+ const errorResult = {
49137
+ success: false,
49138
+ framework,
49139
+ scope,
49140
+ error: "Estimated test file count exceeds safe maximum",
49141
+ message: `Scope "graph" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
49142
+ outcome: "scope_exceeded"
49143
+ };
49144
+ return JSON.stringify(errorResult, null, 2);
49145
+ }
49071
49146
  const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
49072
49147
  if (graphTestFiles.length > 0) {
49073
49148
  testFiles = graphTestFiles;
@@ -49106,8 +49181,31 @@ var init_test_runner = __esm(() => {
49106
49181
  };
49107
49182
  return JSON.stringify(errorResult, null, 2);
49108
49183
  }
49184
+ const estimate = await estimateFanOut(sourceFiles, workingDir);
49185
+ if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
49186
+ const errorResult = {
49187
+ success: false,
49188
+ framework,
49189
+ scope,
49190
+ error: "Estimated test file count exceeds safe maximum",
49191
+ message: `Scope "impact" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
49192
+ outcome: "scope_exceeded"
49193
+ };
49194
+ return JSON.stringify(errorResult, null, 2);
49195
+ }
49109
49196
  try {
49110
- const impactResult = await analyzeImpact(sourceFiles, workingDir);
49197
+ const impactResult = await analyzeImpact(sourceFiles, workingDir, MAX_SAFE_TEST_FILES);
49198
+ if (impactResult.budgetExceeded) {
49199
+ const errorResult = {
49200
+ success: false,
49201
+ framework,
49202
+ scope,
49203
+ error: "Budget exceeded during impact analysis",
49204
+ message: `Impact analysis exceeded safe budget of ${MAX_SAFE_TEST_FILES} test files.`,
49205
+ outcome: "scope_exceeded"
49206
+ };
49207
+ return JSON.stringify(errorResult, null, 2);
49208
+ }
49111
49209
  if (impactResult.impactedTests.length > 0) {
49112
49210
  testFiles = impactResult.impactedTests.map((absPath) => {
49113
49211
  const relativePath = path39.relative(workingDir, absPath);
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.19.2",
36
+ version: "7.20.0",
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",
@@ -24183,6 +24183,91 @@ function extractStatusCode(errorMsg) {
24183
24183
  }
24184
24184
  return null;
24185
24185
  }
24186
+ function isPlainObject2(value) {
24187
+ return typeof value === "object" && value !== null && (value.constructor === Object || Object.getPrototypeOf(value) === null);
24188
+ }
24189
+ function readSignalField(source, key) {
24190
+ try {
24191
+ return source[key];
24192
+ } catch {
24193
+ return;
24194
+ }
24195
+ }
24196
+ function pushSignalValue(parts2, value) {
24197
+ if (typeof value === "string") {
24198
+ parts2.push(value);
24199
+ return;
24200
+ }
24201
+ if (typeof value === "number" || typeof value === "boolean") {
24202
+ parts2.push(String(value));
24203
+ }
24204
+ }
24205
+ function appendSelectedFields(parts2, source, keys) {
24206
+ for (const key of keys) {
24207
+ pushSignalValue(parts2, readSignalField(source, key));
24208
+ }
24209
+ }
24210
+ function appendNestedErrorSignal(parts2, value) {
24211
+ if (typeof value === "string") {
24212
+ parts2.push(value);
24213
+ return;
24214
+ }
24215
+ if (value instanceof Error) {
24216
+ parts2.push(value.name, value.message);
24217
+ appendSelectedFields(parts2, value, [
24218
+ "code",
24219
+ "status",
24220
+ "statusCode"
24221
+ ]);
24222
+ return;
24223
+ }
24224
+ if (!isPlainObject2(value))
24225
+ return;
24226
+ appendSelectedFields(parts2, value, [
24227
+ "code",
24228
+ "status",
24229
+ "statusCode",
24230
+ "message",
24231
+ "error_type"
24232
+ ]);
24233
+ }
24234
+ function extractErrorSignal(errorContent) {
24235
+ if (typeof errorContent === "string")
24236
+ return errorContent;
24237
+ if (errorContent == null)
24238
+ return "";
24239
+ const parts2 = [];
24240
+ try {
24241
+ if (errorContent instanceof Error) {
24242
+ parts2.push(errorContent.name, errorContent.message);
24243
+ appendSelectedFields(parts2, errorContent, ["code", "status", "statusCode"]);
24244
+ return parts2.join(" ");
24245
+ }
24246
+ if (!isPlainObject2(errorContent))
24247
+ return "";
24248
+ appendSelectedFields(parts2, errorContent, [
24249
+ "code",
24250
+ "status",
24251
+ "statusCode",
24252
+ "message",
24253
+ "error_type"
24254
+ ]);
24255
+ appendNestedErrorSignal(parts2, readSignalField(errorContent, "error"));
24256
+ const metadata2 = readSignalField(errorContent, "metadata");
24257
+ if (isPlainObject2(metadata2)) {
24258
+ appendSelectedFields(parts2, metadata2, [
24259
+ "code",
24260
+ "status",
24261
+ "statusCode",
24262
+ "error_type"
24263
+ ]);
24264
+ }
24265
+ appendNestedErrorSignal(parts2, readSignalField(errorContent, "cause"));
24266
+ } catch {
24267
+ return parts2.join(" ");
24268
+ }
24269
+ return parts2.join(" ");
24270
+ }
24186
24271
  function getStoredInputArgs(callID) {
24187
24272
  return storedInputArgs.get(callID);
24188
24273
  }
@@ -25392,16 +25477,17 @@ function createGuardrailsHooks(directory, directoryOrConfig, config2, authorityC
25392
25477
  if (hasError) {
25393
25478
  const outputStr = typeof output.output === "string" ? output.output : "";
25394
25479
  const errorContent = output.error ?? outputStr;
25395
- const extractedStatus = typeof errorContent === "string" ? extractStatusCode(errorContent) : null;
25480
+ const errorSignal = extractErrorSignal(errorContent);
25481
+ const extractedStatus = extractStatusCode(errorSignal);
25396
25482
  const isTransientStatusCode = extractedStatus !== null && TRANSIENT_STATUS_CODES.has(extractedStatus);
25397
- const isTransientPatternMatch = typeof errorContent === "string" && TRANSIENT_MODEL_ERROR_PATTERN.test(errorContent);
25483
+ const isTransientPatternMatch = TRANSIENT_MODEL_ERROR_PATTERN.test(errorSignal);
25398
25484
  const isTransientMatch = isTransientStatusCode || isTransientPatternMatch;
25399
25485
  const isTransient = !!session && isTransientMatch && window2.transientRetryCount < cfg.max_transient_retries;
25400
- const isDegraded = !isTransient && typeof errorContent === "string" && DEGRADED_ERROR_PATTERN.test(errorContent);
25486
+ const isDegraded = !isTransient && DEGRADED_ERROR_PATTERN.test(errorSignal);
25401
25487
  if (isTransient) {
25402
25488
  window2.transientRetryCount++;
25403
25489
  } else if (isDegraded) {
25404
- const isContentFilter = typeof errorContent === "string" && CONTENT_FILTER_PATTERN.test(errorContent);
25490
+ const isContentFilter = CONTENT_FILTER_PATTERN.test(errorSignal);
25405
25491
  if (session && !session.modelFallbackExhausted) {
25406
25492
  session.model_fallback_index++;
25407
25493
  const baseAgentName = session.agentName ? session.agentName.replace(/^[^_]+[_]/, "") : "";
@@ -26058,7 +26144,7 @@ var init_guardrails = __esm(() => {
26058
26144
  ]);
26059
26145
  storedInputArgs = new Map;
26060
26146
  TRANSIENT_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504, 529]);
26061
- TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|server.?error|connection.?(refused|reset|timeout)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable/i;
26147
+ TRANSIENT_MODEL_ERROR_PATTERN = /rate.?limit|429|500|502|503|504|529|timeout|overloaded|model.?not.?found|temporarily.?unavailable|provider.?unavailable|server.?error|connection.?(refused|reset|timeout|lost)|bad.?gateway|gateway.?timeout|internal.?server.?error|service.?unavailable/i;
26062
26148
  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;
26063
26149
  CONTENT_FILTER_PATTERN = /content.?filter/i;
26064
26150
  toolCallsSinceLastWrite = new Map;
@@ -28257,7 +28343,7 @@ __export(exports_util2, {
28257
28343
  jsonStringifyReplacer: () => jsonStringifyReplacer2,
28258
28344
  joinValues: () => joinValues2,
28259
28345
  issue: () => issue2,
28260
- isPlainObject: () => isPlainObject2,
28346
+ isPlainObject: () => isPlainObject3,
28261
28347
  isObject: () => isObject2,
28262
28348
  hexToUint8Array: () => hexToUint8Array2,
28263
28349
  getSizableOrigin: () => getSizableOrigin2,
@@ -28425,7 +28511,7 @@ function esc2(str) {
28425
28511
  function isObject2(data) {
28426
28512
  return typeof data === "object" && data !== null && !Array.isArray(data);
28427
28513
  }
28428
- function isPlainObject2(o) {
28514
+ function isPlainObject3(o) {
28429
28515
  if (isObject2(o) === false)
28430
28516
  return false;
28431
28517
  const ctor = o.constructor;
@@ -28440,7 +28526,7 @@ function isPlainObject2(o) {
28440
28526
  return true;
28441
28527
  }
28442
28528
  function shallowClone2(o) {
28443
- if (isPlainObject2(o))
28529
+ if (isPlainObject3(o))
28444
28530
  return { ...o };
28445
28531
  if (Array.isArray(o))
28446
28532
  return [...o];
@@ -28566,7 +28652,7 @@ function omit2(schema, mask) {
28566
28652
  return clone2(schema, def);
28567
28653
  }
28568
28654
  function extend2(schema, shape) {
28569
- if (!isPlainObject2(shape)) {
28655
+ if (!isPlainObject3(shape)) {
28570
28656
  throw new Error("Invalid input to extend: expected a plain object");
28571
28657
  }
28572
28658
  const checks3 = schema._zod.def.checks;
@@ -28585,7 +28671,7 @@ function extend2(schema, shape) {
28585
28671
  return clone2(schema, def);
28586
28672
  }
28587
28673
  function safeExtend2(schema, shape) {
28588
- if (!isPlainObject2(shape)) {
28674
+ if (!isPlainObject3(shape)) {
28589
28675
  throw new Error("Invalid input to safeExtend: expected a plain object");
28590
28676
  }
28591
28677
  const def = {
@@ -29968,7 +30054,7 @@ function mergeValues2(a, b) {
29968
30054
  if (a instanceof Date && b instanceof Date && +a === +b) {
29969
30055
  return { valid: true, data: a };
29970
30056
  }
29971
- if (isPlainObject2(a) && isPlainObject2(b)) {
30057
+ if (isPlainObject3(a) && isPlainObject3(b)) {
29972
30058
  const bKeys = Object.keys(b);
29973
30059
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
29974
30060
  const newObj = { ...a, ...b };
@@ -31065,7 +31151,7 @@ var init_schemas3 = __esm(() => {
31065
31151
  $ZodType2.init(inst, def);
31066
31152
  inst._zod.parse = (payload, ctx) => {
31067
31153
  const input = payload.value;
31068
- if (!isPlainObject2(input)) {
31154
+ if (!isPlainObject3(input)) {
31069
31155
  payload.issues.push({
31070
31156
  expected: "record",
31071
31157
  code: "invalid_type",
@@ -55321,18 +55407,36 @@ async function buildImpactMap(cwd) {
55321
55407
  await _internals28.saveImpactMap(cwd, impactMap);
55322
55408
  return impactMap;
55323
55409
  }
55324
- async function loadImpactMap(cwd) {
55410
+ async function loadImpactMap(cwd, options) {
55325
55411
  const cachePath = path41.join(cwd, ".swarm", "cache", "impact-map.json");
55326
55412
  if (fs24.existsSync(cachePath)) {
55327
55413
  try {
55328
55414
  const content = fs24.readFileSync(cachePath, "utf-8");
55329
55415
  const data = JSON.parse(content);
55330
- const map3 = data.map;
55331
- const generatedAt = new Date(data.generatedAt).getTime();
55332
- if (!_internals28.isCacheStale(map3, generatedAt)) {
55333
- return map3;
55416
+ if (data.map !== null && typeof data.map === "object" && !Array.isArray(data.map)) {
55417
+ const map3 = data.map;
55418
+ const hasValidValues = Object.values(map3).every((v) => Array.isArray(v) && v.every((item) => typeof item === "string"));
55419
+ if (hasValidValues) {
55420
+ const generatedAt = new Date(data.generatedAt).getTime();
55421
+ if (!_internals28.isCacheStale(map3, generatedAt)) {
55422
+ return map3;
55423
+ }
55424
+ if (options?.skipRebuild) {
55425
+ return map3;
55426
+ }
55427
+ }
55334
55428
  }
55335
- } catch {}
55429
+ if (options?.skipRebuild) {
55430
+ return {};
55431
+ }
55432
+ } catch {
55433
+ if (options?.skipRebuild) {
55434
+ return {};
55435
+ }
55436
+ }
55437
+ }
55438
+ if (options?.skipRebuild) {
55439
+ return {};
55336
55440
  }
55337
55441
  return _internals28.buildImpactMap(cwd);
55338
55442
  }
@@ -55349,7 +55453,7 @@ async function saveImpactMap(cwd, impactMap) {
55349
55453
  };
55350
55454
  fs24.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
55351
55455
  }
55352
- async function analyzeImpact(changedFiles, cwd) {
55456
+ async function analyzeImpact(changedFiles, cwd, budget) {
55353
55457
  if (!Array.isArray(changedFiles)) {
55354
55458
  const emptyMap = {};
55355
55459
  return {
@@ -55363,24 +55467,49 @@ async function analyzeImpact(changedFiles, cwd) {
55363
55467
  const impactMap = await _internals28.loadImpactMap(cwd);
55364
55468
  const impactedTestsSet = new Set;
55365
55469
  const untestedFiles = [];
55470
+ let visitedCount = 0;
55471
+ let budgetExceeded = false;
55366
55472
  for (const changedFile of validFiles) {
55473
+ if (budget !== undefined && visitedCount >= budget) {
55474
+ budgetExceeded = true;
55475
+ break;
55476
+ }
55367
55477
  const normalizedChanged = normalizePath(path41.resolve(changedFile));
55368
55478
  const tests = impactMap[normalizedChanged];
55369
55479
  if (tests && tests.length > 0) {
55370
55480
  for (const test of tests) {
55481
+ if (budget !== undefined && visitedCount >= budget) {
55482
+ budgetExceeded = true;
55483
+ break;
55484
+ }
55371
55485
  impactedTestsSet.add(test);
55486
+ visitedCount++;
55372
55487
  }
55488
+ if (budgetExceeded)
55489
+ break;
55373
55490
  } else {
55374
55491
  let found = false;
55375
55492
  for (const [sourcePath, tests2] of Object.entries(impactMap)) {
55493
+ if (budget !== undefined && visitedCount >= budget) {
55494
+ budgetExceeded = true;
55495
+ break;
55496
+ }
55376
55497
  if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
55377
55498
  for (const test of tests2) {
55499
+ if (budget !== undefined && visitedCount >= budget) {
55500
+ budgetExceeded = true;
55501
+ break;
55502
+ }
55378
55503
  impactedTestsSet.add(test);
55504
+ visitedCount++;
55379
55505
  }
55506
+ if (budgetExceeded)
55507
+ break;
55380
55508
  found = true;
55381
- break;
55382
55509
  }
55383
55510
  }
55511
+ if (budgetExceeded)
55512
+ break;
55384
55513
  if (!found) {
55385
55514
  untestedFiles.push(changedFile);
55386
55515
  }
@@ -55398,7 +55527,8 @@ async function analyzeImpact(changedFiles, cwd) {
55398
55527
  impactedTests,
55399
55528
  unrelatedTests,
55400
55529
  untestedFiles,
55401
- impactMap
55530
+ impactMap,
55531
+ budgetExceeded
55402
55532
  };
55403
55533
  }
55404
55534
  var IMPORT_REGEX_ES, IMPORT_REGEX_REQUIRE, IMPORT_REGEX_REEXPORT, TS_EXTENSIONS, PYTHON_EXTENSIONS, GO_EXTENSIONS, EXTENSIONS_TO_TRY, goModuleCache, _internals28;
@@ -56260,6 +56390,25 @@ var init_dispatch = __esm(() => {
56260
56390
  // src/tools/test-runner.ts
56261
56391
  import * as fs29 from "node:fs";
56262
56392
  import * as path46 from "node:path";
56393
+ async function estimateFanOut(sourceFiles, cwd) {
56394
+ try {
56395
+ const impactMap = await loadImpactMap(cwd, { skipRebuild: true });
56396
+ const uniqueTestFiles = new Set;
56397
+ for (const sourceFile of sourceFiles) {
56398
+ const resolvedPath = path46.resolve(cwd, sourceFile);
56399
+ const normalizedPath = resolvedPath.replace(/\\/g, "/");
56400
+ const testFiles = impactMap[normalizedPath];
56401
+ if (testFiles) {
56402
+ for (const testFile of testFiles) {
56403
+ uniqueTestFiles.add(testFile);
56404
+ }
56405
+ }
56406
+ }
56407
+ return { estimatedCount: uniqueTestFiles.size };
56408
+ } catch {
56409
+ return { estimatedCount: 0 };
56410
+ }
56411
+ }
56263
56412
  function isAbsolutePath(str) {
56264
56413
  if (str.startsWith("/"))
56265
56414
  return true;
@@ -57604,6 +57753,18 @@ var init_test_runner = __esm(() => {
57604
57753
  };
57605
57754
  return JSON.stringify(errorResult, null, 2);
57606
57755
  }
57756
+ const estimate = await estimateFanOut(sourceFiles, workingDir);
57757
+ if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
57758
+ const errorResult = {
57759
+ success: false,
57760
+ framework,
57761
+ scope,
57762
+ error: "Estimated test file count exceeds safe maximum",
57763
+ message: `Scope "graph" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
57764
+ outcome: "scope_exceeded"
57765
+ };
57766
+ return JSON.stringify(errorResult, null, 2);
57767
+ }
57607
57768
  const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
57608
57769
  if (graphTestFiles.length > 0) {
57609
57770
  testFiles = graphTestFiles;
@@ -57642,8 +57803,31 @@ var init_test_runner = __esm(() => {
57642
57803
  };
57643
57804
  return JSON.stringify(errorResult, null, 2);
57644
57805
  }
57806
+ const estimate = await estimateFanOut(sourceFiles, workingDir);
57807
+ if (estimate.estimatedCount > MAX_SAFE_TEST_FILES) {
57808
+ const errorResult = {
57809
+ success: false,
57810
+ framework,
57811
+ scope,
57812
+ error: "Estimated test file count exceeds safe maximum",
57813
+ message: `Scope "impact" resolution would produce approximately ${estimate.estimatedCount} test files, which exceeds the safe limit of ${MAX_SAFE_TEST_FILES}. Break the source files into smaller batches and retry.`,
57814
+ outcome: "scope_exceeded"
57815
+ };
57816
+ return JSON.stringify(errorResult, null, 2);
57817
+ }
57645
57818
  try {
57646
- const impactResult = await analyzeImpact(sourceFiles, workingDir);
57819
+ const impactResult = await analyzeImpact(sourceFiles, workingDir, MAX_SAFE_TEST_FILES);
57820
+ if (impactResult.budgetExceeded) {
57821
+ const errorResult = {
57822
+ success: false,
57823
+ framework,
57824
+ scope,
57825
+ error: "Budget exceeded during impact analysis",
57826
+ message: `Impact analysis exceeded safe budget of ${MAX_SAFE_TEST_FILES} test files.`,
57827
+ outcome: "scope_exceeded"
57828
+ };
57829
+ return JSON.stringify(errorResult, null, 2);
57830
+ }
57647
57831
  if (impactResult.impactedTests.length > 0) {
57648
57832
  testFiles = impactResult.impactedTests.map((absPath) => {
57649
57833
  const relativePath = path46.relative(workingDir, absPath);
@@ -3,6 +3,7 @@ export interface TestImpactResult {
3
3
  unrelatedTests: string[];
4
4
  untestedFiles: string[];
5
5
  impactMap: Record<string, string[]>;
6
+ budgetExceeded?: boolean;
6
7
  }
7
8
  declare function normalizePath(p: string): string;
8
9
  declare function isCacheStale(impactMap: Record<string, string[]>, generatedAtMs: number): boolean;
@@ -30,7 +31,12 @@ export declare const _internals: {
30
31
  _clearGoModuleCache: typeof _clearGoModuleCache;
31
32
  };
32
33
  export declare function buildImpactMap(cwd: string): Promise<Record<string, string[]>>;
33
- export declare function loadImpactMap(cwd: string): Promise<Record<string, string[]>>;
34
+ export interface LoadImpactMapOptions {
35
+ /** If true and cache is stale, return the stale map instead of rebuilding.
36
+ * Use for estimation-only reads where slight staleness is acceptable. */
37
+ skipRebuild?: boolean;
38
+ }
39
+ export declare function loadImpactMap(cwd: string, options?: LoadImpactMapOptions): Promise<Record<string, string[]>>;
34
40
  declare function saveImpactMap(cwd: string, impactMap: Record<string, string[]>): Promise<void>;
35
- export declare function analyzeImpact(changedFiles: string[], cwd: string): Promise<TestImpactResult>;
41
+ export declare function analyzeImpact(changedFiles: string[], cwd: string, budget?: number): Promise<TestImpactResult>;
36
42
  export {};
@@ -5,6 +5,17 @@ export declare const DEFAULT_TIMEOUT_MS = 60000;
5
5
  export declare const MAX_TIMEOUT_MS = 300000;
6
6
  export declare const MAX_SAFE_TEST_FILES = 50;
7
7
  export declare const MAX_SAFE_SOURCE_FILES = 1;
8
+ /**
9
+ * Estimate the fan-out (number of unique test files) for given source files
10
+ * by reading the cached impact map without spawning a subprocess.
11
+ * This is a pre-resolution check to prevent session blocking.
12
+ *
13
+ * Completes in <100ms by design — reads only the cached JSON and performs
14
+ * in-memory Set collection.
15
+ */
16
+ export declare function estimateFanOut(sourceFiles: string[], cwd: string): Promise<{
17
+ estimatedCount: number;
18
+ }>;
8
19
  export declare const SUPPORTED_FRAMEWORKS: readonly ["bun", "vitest", "jest", "mocha", "pytest", "cargo", "pester", "go-test", "maven", "gradle", "dotnet-test", "ctest", "swift-test", "dart-test", "rspec", "minitest"];
9
20
  export type TestFramework = (typeof SUPPORTED_FRAMEWORKS)[number] | 'none';
10
21
  export interface TestRunnerArgs {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.19.2",
3
+ "version": "7.20.0",
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",