opencode-swarm 7.19.3 → 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.3",
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.3",
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",
@@ -55407,18 +55407,36 @@ async function buildImpactMap(cwd) {
55407
55407
  await _internals28.saveImpactMap(cwd, impactMap);
55408
55408
  return impactMap;
55409
55409
  }
55410
- async function loadImpactMap(cwd) {
55410
+ async function loadImpactMap(cwd, options) {
55411
55411
  const cachePath = path41.join(cwd, ".swarm", "cache", "impact-map.json");
55412
55412
  if (fs24.existsSync(cachePath)) {
55413
55413
  try {
55414
55414
  const content = fs24.readFileSync(cachePath, "utf-8");
55415
55415
  const data = JSON.parse(content);
55416
- const map3 = data.map;
55417
- const generatedAt = new Date(data.generatedAt).getTime();
55418
- if (!_internals28.isCacheStale(map3, generatedAt)) {
55419
- 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
+ }
55420
55428
  }
55421
- } 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 {};
55422
55440
  }
55423
55441
  return _internals28.buildImpactMap(cwd);
55424
55442
  }
@@ -55435,7 +55453,7 @@ async function saveImpactMap(cwd, impactMap) {
55435
55453
  };
55436
55454
  fs24.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
55437
55455
  }
55438
- async function analyzeImpact(changedFiles, cwd) {
55456
+ async function analyzeImpact(changedFiles, cwd, budget) {
55439
55457
  if (!Array.isArray(changedFiles)) {
55440
55458
  const emptyMap = {};
55441
55459
  return {
@@ -55449,24 +55467,49 @@ async function analyzeImpact(changedFiles, cwd) {
55449
55467
  const impactMap = await _internals28.loadImpactMap(cwd);
55450
55468
  const impactedTestsSet = new Set;
55451
55469
  const untestedFiles = [];
55470
+ let visitedCount = 0;
55471
+ let budgetExceeded = false;
55452
55472
  for (const changedFile of validFiles) {
55473
+ if (budget !== undefined && visitedCount >= budget) {
55474
+ budgetExceeded = true;
55475
+ break;
55476
+ }
55453
55477
  const normalizedChanged = normalizePath(path41.resolve(changedFile));
55454
55478
  const tests = impactMap[normalizedChanged];
55455
55479
  if (tests && tests.length > 0) {
55456
55480
  for (const test of tests) {
55481
+ if (budget !== undefined && visitedCount >= budget) {
55482
+ budgetExceeded = true;
55483
+ break;
55484
+ }
55457
55485
  impactedTestsSet.add(test);
55486
+ visitedCount++;
55458
55487
  }
55488
+ if (budgetExceeded)
55489
+ break;
55459
55490
  } else {
55460
55491
  let found = false;
55461
55492
  for (const [sourcePath, tests2] of Object.entries(impactMap)) {
55493
+ if (budget !== undefined && visitedCount >= budget) {
55494
+ budgetExceeded = true;
55495
+ break;
55496
+ }
55462
55497
  if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
55463
55498
  for (const test of tests2) {
55499
+ if (budget !== undefined && visitedCount >= budget) {
55500
+ budgetExceeded = true;
55501
+ break;
55502
+ }
55464
55503
  impactedTestsSet.add(test);
55504
+ visitedCount++;
55465
55505
  }
55506
+ if (budgetExceeded)
55507
+ break;
55466
55508
  found = true;
55467
- break;
55468
55509
  }
55469
55510
  }
55511
+ if (budgetExceeded)
55512
+ break;
55470
55513
  if (!found) {
55471
55514
  untestedFiles.push(changedFile);
55472
55515
  }
@@ -55484,7 +55527,8 @@ async function analyzeImpact(changedFiles, cwd) {
55484
55527
  impactedTests,
55485
55528
  unrelatedTests,
55486
55529
  untestedFiles,
55487
- impactMap
55530
+ impactMap,
55531
+ budgetExceeded
55488
55532
  };
55489
55533
  }
55490
55534
  var IMPORT_REGEX_ES, IMPORT_REGEX_REQUIRE, IMPORT_REGEX_REEXPORT, TS_EXTENSIONS, PYTHON_EXTENSIONS, GO_EXTENSIONS, EXTENSIONS_TO_TRY, goModuleCache, _internals28;
@@ -56346,6 +56390,25 @@ var init_dispatch = __esm(() => {
56346
56390
  // src/tools/test-runner.ts
56347
56391
  import * as fs29 from "node:fs";
56348
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
+ }
56349
56412
  function isAbsolutePath(str) {
56350
56413
  if (str.startsWith("/"))
56351
56414
  return true;
@@ -57690,6 +57753,18 @@ var init_test_runner = __esm(() => {
57690
57753
  };
57691
57754
  return JSON.stringify(errorResult, null, 2);
57692
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
+ }
57693
57768
  const graphTestFiles = await getTestFilesFromGraph(sourceFiles, workingDir);
57694
57769
  if (graphTestFiles.length > 0) {
57695
57770
  testFiles = graphTestFiles;
@@ -57728,8 +57803,31 @@ var init_test_runner = __esm(() => {
57728
57803
  };
57729
57804
  return JSON.stringify(errorResult, null, 2);
57730
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
+ }
57731
57818
  try {
57732
- 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
+ }
57733
57831
  if (impactResult.impactedTests.length > 0) {
57734
57832
  testFiles = impactResult.impactedTests.map((absPath) => {
57735
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.3",
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",