opencode-swarm 7.24.1 → 7.25.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.24.1",
37
+ version: "7.25.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",
@@ -45999,7 +45999,14 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
45999
45999
  return args;
46000
46000
  }
46001
46001
  case "vitest": {
46002
- const args = ["npx", "vitest", "run"];
46002
+ const args = [
46003
+ "npx",
46004
+ "vitest",
46005
+ "run",
46006
+ "--reporter=json",
46007
+ "--outputFile",
46008
+ ".swarm/cache/test-runner-vitest.json"
46009
+ ];
46003
46010
  if (coverage)
46004
46011
  args.push("--coverage");
46005
46012
  if (scope !== "all" && files.length > 0)
@@ -46007,7 +46014,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
46007
46014
  return args;
46008
46015
  }
46009
46016
  case "jest": {
46010
- const args = ["npx", "jest"];
46017
+ const args = ["npx", "jest", "--json"];
46011
46018
  if (coverage)
46012
46019
  args.push("--coverage");
46013
46020
  if (scope !== "all" && files.length > 0)
@@ -47242,6 +47249,42 @@ function sanitizeChangedFiles(changedFiles) {
47242
47249
  const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
47243
47250
  return validFiles.slice(0, MAX_CHANGED_FILES);
47244
47251
  }
47252
+ function isTestRunResult(value) {
47253
+ return value === "pass" || value === "fail" || value === "skip";
47254
+ }
47255
+ function parseStoredRecord(value) {
47256
+ if (typeof value !== "object" || value === null)
47257
+ return null;
47258
+ const record3 = value;
47259
+ if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
47260
+ return null;
47261
+ }
47262
+ if (typeof record3.testName !== "string" || record3.testName.length === 0) {
47263
+ return null;
47264
+ }
47265
+ if (typeof record3.taskId !== "string" || record3.taskId.length === 0) {
47266
+ return null;
47267
+ }
47268
+ if (!isTestRunResult(record3.result))
47269
+ return null;
47270
+ if (typeof record3.durationMs !== "number" || !Number.isFinite(record3.durationMs)) {
47271
+ return null;
47272
+ }
47273
+ if (typeof record3.timestamp !== "string" || Number.isNaN(Date.parse(record3.timestamp))) {
47274
+ return null;
47275
+ }
47276
+ return {
47277
+ timestamp: record3.timestamp,
47278
+ taskId: record3.taskId,
47279
+ testFile: record3.testFile,
47280
+ testName: record3.testName,
47281
+ result: record3.result,
47282
+ durationMs: Math.max(0, record3.durationMs),
47283
+ errorMessage: typeof record3.errorMessage === "string" ? sanitizeErrorMessage(record3.errorMessage) : undefined,
47284
+ stackPrefix: typeof record3.stackPrefix === "string" ? sanitizeStackPrefix(record3.stackPrefix) : undefined,
47285
+ changedFiles: sanitizeChangedFiles(Array.isArray(record3.changedFiles) ? record3.changedFiles : [])
47286
+ };
47287
+ }
47245
47288
  function appendTestRun(record3, workingDir) {
47246
47289
  if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
47247
47290
  throw new TypeError("testFile must be a non-empty string");
@@ -47279,16 +47322,16 @@ function appendTestRun(record3, workingDir) {
47279
47322
  }
47280
47323
  const existingRecords = readAllRecords(historyPath);
47281
47324
  existingRecords.push(sanitizedRecord);
47282
- const recordsByFile = new Map;
47325
+ const recordsByTest = new Map;
47283
47326
  for (const rec of existingRecords) {
47284
- const normalizedFile = rec.testFile.toLowerCase();
47285
- if (!recordsByFile.has(normalizedFile)) {
47286
- recordsByFile.set(normalizedFile, []);
47327
+ const normalizedKey = `${rec.testFile.toLowerCase()}|${rec.testName.toLowerCase()}`;
47328
+ if (!recordsByTest.has(normalizedKey)) {
47329
+ recordsByTest.set(normalizedKey, []);
47287
47330
  }
47288
- recordsByFile.get(normalizedFile).push(rec);
47331
+ recordsByTest.get(normalizedKey).push(rec);
47289
47332
  }
47290
47333
  const prunedRecords = [];
47291
- for (const [, records] of recordsByFile) {
47334
+ for (const [, records] of recordsByTest) {
47292
47335
  records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
47293
47336
  const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
47294
47337
  prunedRecords.push(...toKeep);
@@ -47328,8 +47371,9 @@ function readAllRecords(historyPath) {
47328
47371
  }
47329
47372
  try {
47330
47373
  const parsed = JSON.parse(trimmed);
47331
- if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
47332
- records.push(parsed);
47374
+ const record3 = parseStoredRecord(parsed);
47375
+ if (record3) {
47376
+ records.push(record3);
47333
47377
  }
47334
47378
  } catch {}
47335
47379
  }
@@ -48368,7 +48412,14 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48368
48412
  return args;
48369
48413
  }
48370
48414
  case "vitest": {
48371
- const args = ["npx", "vitest", "run"];
48415
+ const args = [
48416
+ "npx",
48417
+ "vitest",
48418
+ "run",
48419
+ "--reporter=json",
48420
+ "--outputFile",
48421
+ VITEST_JSON_OUTPUT_RELATIVE_PATH
48422
+ ];
48372
48423
  if (coverage)
48373
48424
  args.push("--coverage");
48374
48425
  if (scope !== "all" && files.length > 0) {
@@ -48377,7 +48428,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48377
48428
  return args;
48378
48429
  }
48379
48430
  case "jest": {
48380
- const args = ["npx", "jest"];
48431
+ const args = ["npx", "jest", "--json"];
48381
48432
  if (coverage)
48382
48433
  args.push("--coverage");
48383
48434
  if (scope !== "all" && files.length > 0) {
@@ -48473,6 +48524,122 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48473
48524
  return null;
48474
48525
  }
48475
48526
  }
48527
+ function mapFrameworkStatusToResult(status) {
48528
+ if (typeof status !== "string")
48529
+ return null;
48530
+ const normalized = status.toLowerCase();
48531
+ if (normalized === "pass" || normalized === "passed")
48532
+ return "pass";
48533
+ if (normalized === "fail" || normalized === "failed")
48534
+ return "fail";
48535
+ if (normalized === "skip" || normalized === "skipped" || normalized === "pending" || normalized === "todo") {
48536
+ return "skip";
48537
+ }
48538
+ return null;
48539
+ }
48540
+ function firstLine(value) {
48541
+ if (typeof value !== "string")
48542
+ return;
48543
+ const line = value.split(`
48544
+ `).find((part) => part.trim().length > 0)?.trim();
48545
+ return line && line.length > 0 ? line : undefined;
48546
+ }
48547
+ function parseJestLikeJsonTestResults(payload) {
48548
+ if (typeof payload !== "object" || payload === null)
48549
+ return [];
48550
+ const rawSuites = payload.testResults;
48551
+ if (!Array.isArray(rawSuites))
48552
+ return [];
48553
+ const parsed = [];
48554
+ for (const suite of rawSuites) {
48555
+ if (typeof suite !== "object" || suite === null)
48556
+ continue;
48557
+ const suiteObj = suite;
48558
+ const rawFile = typeof suiteObj.name === "string" ? suiteObj.name : typeof suiteObj.testFilePath === "string" ? suiteObj.testFilePath : undefined;
48559
+ if (!rawFile)
48560
+ continue;
48561
+ const testFile = rawFile.replace(/\\/g, "/");
48562
+ const assertionResults = suiteObj.assertionResults;
48563
+ if (!Array.isArray(assertionResults))
48564
+ continue;
48565
+ for (const assertion of assertionResults) {
48566
+ if (typeof assertion !== "object" || assertion === null)
48567
+ continue;
48568
+ const assertionObj = assertion;
48569
+ const result = mapFrameworkStatusToResult(assertionObj.status);
48570
+ const testName = typeof assertionObj.fullName === "string" ? assertionObj.fullName : typeof assertionObj.title === "string" ? assertionObj.title : undefined;
48571
+ if (!result || !testName || testName.length === 0)
48572
+ continue;
48573
+ const failureMessages = Array.isArray(assertionObj.failureMessages) ? assertionObj.failureMessages : [];
48574
+ const firstFailure = failureMessages.find((entry) => typeof entry === "string" && entry.length > 0);
48575
+ const durationMs = typeof assertionObj.duration === "number" && Number.isFinite(assertionObj.duration) ? Math.max(assertionObj.duration, 0) : 0;
48576
+ parsed.push({
48577
+ testFile,
48578
+ testName,
48579
+ result,
48580
+ durationMs,
48581
+ errorMessage: firstLine(firstFailure),
48582
+ stackPrefix: firstLine(firstFailure)
48583
+ });
48584
+ }
48585
+ }
48586
+ return parsed;
48587
+ }
48588
+ function parseBunJsonLines(output) {
48589
+ const parsed = [];
48590
+ for (const line of output.split(`
48591
+ `)) {
48592
+ const trimmed = line.trim();
48593
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
48594
+ continue;
48595
+ try {
48596
+ const obj = JSON.parse(trimmed);
48597
+ const rawFile = typeof obj.file === "string" ? obj.file : typeof obj.testFile === "string" ? obj.testFile : typeof obj.path === "string" ? obj.path : undefined;
48598
+ const rawName = typeof obj.testName === "string" ? obj.testName : typeof obj.fullName === "string" ? obj.fullName : typeof obj.name === "string" ? obj.name : undefined;
48599
+ const result = mapFrameworkStatusToResult(typeof obj.status === "string" ? obj.status : obj.result);
48600
+ if (!rawFile || !rawName || !result)
48601
+ continue;
48602
+ const errorObj = typeof obj.error === "object" && obj.error !== null ? obj.error : undefined;
48603
+ const durationMs = typeof obj.durationMs === "number" && Number.isFinite(obj.durationMs) ? Math.max(obj.durationMs, 0) : typeof obj.duration === "number" && Number.isFinite(obj.duration) ? Math.max(obj.duration, 0) : 0;
48604
+ parsed.push({
48605
+ testFile: rawFile.replace(/\\/g, "/"),
48606
+ testName: rawName,
48607
+ result,
48608
+ durationMs,
48609
+ errorMessage: firstLine(errorObj?.message ?? obj.errorMessage),
48610
+ stackPrefix: firstLine(errorObj?.stack)
48611
+ });
48612
+ } catch {}
48613
+ }
48614
+ return parsed;
48615
+ }
48616
+ function parseFrameworkJsonTestResults(framework, output) {
48617
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
48618
+ if (jsonMatch) {
48619
+ try {
48620
+ const parsed = JSON.parse(jsonMatch[0]);
48621
+ const testResults = parseJestLikeJsonTestResults(parsed);
48622
+ if (testResults.length > 0)
48623
+ return testResults;
48624
+ } catch {}
48625
+ }
48626
+ for (const line of output.split(`
48627
+ `)) {
48628
+ const trimmed = line.trim();
48629
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
48630
+ continue;
48631
+ try {
48632
+ const parsed = JSON.parse(trimmed);
48633
+ const testResults = parseJestLikeJsonTestResults(parsed);
48634
+ if (testResults.length > 0)
48635
+ return testResults;
48636
+ } catch {}
48637
+ }
48638
+ if (framework === "bun") {
48639
+ return parseBunJsonLines(output);
48640
+ }
48641
+ return [];
48642
+ }
48476
48643
  function parseTestOutput2(framework, output) {
48477
48644
  const totals = {
48478
48645
  passed: 0,
@@ -48760,7 +48927,16 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48760
48927
  };
48761
48928
  }
48762
48929
  const startTime = Date.now();
48930
+ const vitestJsonOutputPath = framework === "vitest" ? path39.join(cwd, ".swarm", "cache", "test-runner-vitest.json") : undefined;
48763
48931
  try {
48932
+ if (vitestJsonOutputPath) {
48933
+ try {
48934
+ fs22.mkdirSync(path39.dirname(vitestJsonOutputPath), { recursive: true });
48935
+ if (fs22.existsSync(vitestJsonOutputPath)) {
48936
+ fs22.unlinkSync(vitestJsonOutputPath);
48937
+ }
48938
+ } catch {}
48939
+ }
48764
48940
  const proc = bunSpawn(command, {
48765
48941
  stdout: "pipe",
48766
48942
  stderr: "pipe",
@@ -48781,13 +48957,37 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48781
48957
  output += (output ? `
48782
48958
  ` : "") + stderrResult.text;
48783
48959
  }
48960
+ if (vitestJsonOutputPath) {
48961
+ try {
48962
+ if (fs22.existsSync(vitestJsonOutputPath)) {
48963
+ const vitestJsonOutput = fs22.readFileSync(vitestJsonOutputPath, "utf-8");
48964
+ if (vitestJsonOutput.trim().length > 0) {
48965
+ output += (output ? `
48966
+ ` : "") + vitestJsonOutput;
48967
+ }
48968
+ }
48969
+ } catch {}
48970
+ }
48784
48971
  if (stdoutResult.truncated || stderrResult.truncated) {
48785
48972
  output += `
48786
48973
  ... (output truncated at stream read limit)`;
48787
48974
  }
48788
48975
  const useDispatchParse = process.env.SWARM_LANG_BACKEND !== "legacy";
48789
48976
  const parsed = useDispatchParse ? await parseTestOutputViaDispatch(framework, output, cwd) ?? parseTestOutput2(framework, output) : parseTestOutput2(framework, output);
48790
- const { totals, coveragePercent } = parsed;
48977
+ const parsedTestCases = parseFrameworkJsonTestResults(framework, output);
48978
+ const totals = { ...parsed.totals };
48979
+ const { coveragePercent } = parsed;
48980
+ if (totals.total === 0 && parsedTestCases.length > 0) {
48981
+ for (const entry of parsedTestCases) {
48982
+ if (entry.result === "pass")
48983
+ totals.passed++;
48984
+ else if (entry.result === "fail")
48985
+ totals.failed++;
48986
+ else
48987
+ totals.skipped++;
48988
+ }
48989
+ totals.total = parsedTestCases.length;
48990
+ }
48791
48991
  const isTimeout = exitCode === -1;
48792
48992
  const testPassed = exitCode === 0 && totals.failed === 0;
48793
48993
  if (testPassed) {
@@ -48800,7 +49000,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48800
49000
  duration_ms,
48801
49001
  totals,
48802
49002
  rawOutput: output,
48803
- outcome: "pass"
49003
+ outcome: "pass",
49004
+ testCases: parsedTestCases
48804
49005
  };
48805
49006
  if (coveragePercent !== undefined) {
48806
49007
  result.coveragePercent = coveragePercent;
@@ -48822,7 +49023,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48822
49023
  rawOutput: output,
48823
49024
  error: isTimeout ? `Tests timed out after ${timeout_ms}ms` : `Tests failed with ${totals.failed} failures`,
48824
49025
  message: isTimeout ? `${framework} tests timed out after ${timeout_ms}ms` : `${framework} tests failed (${totals.failed}/${totals.total} failed)`,
48825
- outcome: isTimeout ? "error" : "regression"
49026
+ outcome: isTimeout ? "error" : "regression",
49027
+ testCases: parsedTestCases
48826
49028
  };
48827
49029
  if (coveragePercent !== undefined) {
48828
49030
  result.coveragePercent = coveragePercent;
@@ -48843,25 +49045,78 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48843
49045
  };
48844
49046
  }
48845
49047
  }
48846
- function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles) {
49048
+ function normalizeHistoryTestFile(testFile, workingDir) {
49049
+ const normalized = testFile.replace(/\\/g, "/");
49050
+ if (!path39.isAbsolute(testFile))
49051
+ return normalized;
49052
+ const relative8 = path39.relative(workingDir, testFile);
49053
+ if (relative8.startsWith("..") || path39.isAbsolute(relative8)) {
49054
+ return normalized;
49055
+ }
49056
+ return relative8.replace(/\\/g, "/");
49057
+ }
49058
+ function combineAggregateResult(current, next) {
49059
+ if (current === "fail" || next === "fail")
49060
+ return "fail";
49061
+ if (current === "pass" || next === "pass")
49062
+ return "pass";
49063
+ return "skip";
49064
+ }
49065
+ function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles, parsedTestCases) {
48847
49066
  if (!result.totals || result.totals.total === 0)
48848
49067
  return;
48849
49068
  const now = new Date().toISOString();
48850
49069
  const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
48851
- for (const testFile of testFiles) {
49070
+ const aggregateResultsByFile = new Map;
49071
+ const validParsedCases = parsedTestCases?.filter((parsedCase) => parsedCase.testFile.length > 0 && parsedCase.testName.length > 0) ?? [];
49072
+ for (const parsedCase of validParsedCases) {
49073
+ const normalizedTestFile = normalizeHistoryTestFile(parsedCase.testFile, workingDir);
48852
49074
  try {
48853
49075
  appendTestRun({
48854
49076
  timestamp: now,
48855
49077
  taskId: "auto",
48856
- testFile: testFile.replace(/\\/g, "/"),
48857
- testName: "(aggregate)",
48858
- result: result.success ? "pass" : "fail",
49078
+ testFile: normalizedTestFile,
49079
+ testName: parsedCase.testName,
49080
+ result: parsedCase.result,
49081
+ durationMs: parsedCase.durationMs,
49082
+ errorMessage: parsedCase.errorMessage,
49083
+ stackPrefix: parsedCase.stackPrefix,
49084
+ changedFiles
49085
+ }, workingDir);
49086
+ } catch {}
49087
+ aggregateResultsByFile.set(normalizedTestFile, combineAggregateResult(aggregateResultsByFile.get(normalizedTestFile), parsedCase.result));
49088
+ }
49089
+ if (aggregateResultsByFile.size === 0) {
49090
+ const aggregateResult = result.success ? "pass" : "fail";
49091
+ for (const testFile of testFiles) {
49092
+ aggregateResultsByFile.set(testFile.replace(/\\/g, "/"), aggregateResult);
49093
+ }
49094
+ }
49095
+ for (const [testFile, aggregateResult] of aggregateResultsByFile) {
49096
+ try {
49097
+ appendTestRun({
49098
+ timestamp: now,
49099
+ taskId: "auto",
49100
+ testFile,
49101
+ testName: AGGREGATE_TEST_NAME,
49102
+ result: aggregateResult,
48859
49103
  durationMs: result.duration_ms || 0,
48860
49104
  changedFiles
48861
49105
  }, workingDir);
48862
49106
  } catch {}
48863
49107
  }
48864
49108
  }
49109
+ function selectHistoryForAnalysis(history) {
49110
+ const filesWithIndividualRecords = new Set;
49111
+ for (const record3 of history) {
49112
+ if (record3.testName !== AGGREGATE_TEST_NAME) {
49113
+ filesWithIndividualRecords.add(record3.testFile.toLowerCase());
49114
+ }
49115
+ }
49116
+ if (filesWithIndividualRecords.size === 0)
49117
+ return history;
49118
+ return history.filter((record3) => record3.testName !== AGGREGATE_TEST_NAME || !filesWithIndividualRecords.has(record3.testFile.toLowerCase()));
49119
+ }
48865
49120
  function analyzeFailures(workingDir) {
48866
49121
  const report = {
48867
49122
  flakyTests: [],
@@ -48869,7 +49124,7 @@ function analyzeFailures(workingDir) {
48869
49124
  quarantinedFailures: []
48870
49125
  };
48871
49126
  try {
48872
- const history = getAllHistory(workingDir);
49127
+ const history = selectHistoryForAnalysis(getAllHistory(workingDir));
48873
49128
  if (history.length === 0)
48874
49129
  return report;
48875
49130
  report.flakyTests = detectFlakyTests(history);
@@ -48890,7 +49145,7 @@ function analyzeFailures(workingDir) {
48890
49145
  } catch {}
48891
49146
  return report;
48892
49147
  }
48893
- var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
49148
+ var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, AGGREGATE_TEST_NAME = "(aggregate)", VITEST_JSON_OUTPUT_RELATIVE_PATH = ".swarm/cache/test-runner-vitest.json", POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
48894
49149
  var init_test_runner = __esm(() => {
48895
49150
  init_zod();
48896
49151
  init_discovery();
@@ -49336,7 +49591,7 @@ var init_test_runner = __esm(() => {
49336
49591
  return JSON.stringify(errorResult, null, 2);
49337
49592
  }
49338
49593
  const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
49339
- recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
49594
+ recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined, result.testCases);
49340
49595
  let historyReport;
49341
49596
  if (!result.success && result.totals && result.totals.failed > 0) {
49342
49597
  historyReport = analyzeFailures(workingDir);
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.24.1",
36
+ version: "7.25.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",
@@ -54990,7 +54990,14 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
54990
54990
  return args2;
54991
54991
  }
54992
54992
  case "vitest": {
54993
- const args2 = ["npx", "vitest", "run"];
54993
+ const args2 = [
54994
+ "npx",
54995
+ "vitest",
54996
+ "run",
54997
+ "--reporter=json",
54998
+ "--outputFile",
54999
+ ".swarm/cache/test-runner-vitest.json"
55000
+ ];
54994
55001
  if (coverage)
54995
55002
  args2.push("--coverage");
54996
55003
  if (scope !== "all" && files.length > 0)
@@ -54998,7 +55005,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
54998
55005
  return args2;
54999
55006
  }
55000
55007
  case "jest": {
55001
- const args2 = ["npx", "jest"];
55008
+ const args2 = ["npx", "jest", "--json"];
55002
55009
  if (coverage)
55003
55010
  args2.push("--coverage");
55004
55011
  if (scope !== "all" && files.length > 0)
@@ -56233,6 +56240,42 @@ function sanitizeChangedFiles(changedFiles) {
56233
56240
  const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
56234
56241
  return validFiles.slice(0, MAX_CHANGED_FILES);
56235
56242
  }
56243
+ function isTestRunResult(value) {
56244
+ return value === "pass" || value === "fail" || value === "skip";
56245
+ }
56246
+ function parseStoredRecord(value) {
56247
+ if (typeof value !== "object" || value === null)
56248
+ return null;
56249
+ const record3 = value;
56250
+ if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
56251
+ return null;
56252
+ }
56253
+ if (typeof record3.testName !== "string" || record3.testName.length === 0) {
56254
+ return null;
56255
+ }
56256
+ if (typeof record3.taskId !== "string" || record3.taskId.length === 0) {
56257
+ return null;
56258
+ }
56259
+ if (!isTestRunResult(record3.result))
56260
+ return null;
56261
+ if (typeof record3.durationMs !== "number" || !Number.isFinite(record3.durationMs)) {
56262
+ return null;
56263
+ }
56264
+ if (typeof record3.timestamp !== "string" || Number.isNaN(Date.parse(record3.timestamp))) {
56265
+ return null;
56266
+ }
56267
+ return {
56268
+ timestamp: record3.timestamp,
56269
+ taskId: record3.taskId,
56270
+ testFile: record3.testFile,
56271
+ testName: record3.testName,
56272
+ result: record3.result,
56273
+ durationMs: Math.max(0, record3.durationMs),
56274
+ errorMessage: typeof record3.errorMessage === "string" ? sanitizeErrorMessage(record3.errorMessage) : undefined,
56275
+ stackPrefix: typeof record3.stackPrefix === "string" ? sanitizeStackPrefix(record3.stackPrefix) : undefined,
56276
+ changedFiles: sanitizeChangedFiles(Array.isArray(record3.changedFiles) ? record3.changedFiles : [])
56277
+ };
56278
+ }
56236
56279
  function appendTestRun(record3, workingDir) {
56237
56280
  if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
56238
56281
  throw new TypeError("testFile must be a non-empty string");
@@ -56270,16 +56313,16 @@ function appendTestRun(record3, workingDir) {
56270
56313
  }
56271
56314
  const existingRecords = readAllRecords(historyPath);
56272
56315
  existingRecords.push(sanitizedRecord);
56273
- const recordsByFile = new Map;
56316
+ const recordsByTest = new Map;
56274
56317
  for (const rec of existingRecords) {
56275
- const normalizedFile = rec.testFile.toLowerCase();
56276
- if (!recordsByFile.has(normalizedFile)) {
56277
- recordsByFile.set(normalizedFile, []);
56318
+ const normalizedKey = `${rec.testFile.toLowerCase()}|${rec.testName.toLowerCase()}`;
56319
+ if (!recordsByTest.has(normalizedKey)) {
56320
+ recordsByTest.set(normalizedKey, []);
56278
56321
  }
56279
- recordsByFile.get(normalizedFile).push(rec);
56322
+ recordsByTest.get(normalizedKey).push(rec);
56280
56323
  }
56281
56324
  const prunedRecords = [];
56282
- for (const [, records] of recordsByFile) {
56325
+ for (const [, records] of recordsByTest) {
56283
56326
  records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
56284
56327
  const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
56285
56328
  prunedRecords.push(...toKeep);
@@ -56319,8 +56362,9 @@ function readAllRecords(historyPath) {
56319
56362
  }
56320
56363
  try {
56321
56364
  const parsed = JSON.parse(trimmed);
56322
- if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
56323
- records.push(parsed);
56365
+ const record3 = parseStoredRecord(parsed);
56366
+ if (record3) {
56367
+ records.push(record3);
56324
56368
  }
56325
56369
  } catch {}
56326
56370
  }
@@ -57359,7 +57403,14 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
57359
57403
  return args2;
57360
57404
  }
57361
57405
  case "vitest": {
57362
- const args2 = ["npx", "vitest", "run"];
57406
+ const args2 = [
57407
+ "npx",
57408
+ "vitest",
57409
+ "run",
57410
+ "--reporter=json",
57411
+ "--outputFile",
57412
+ VITEST_JSON_OUTPUT_RELATIVE_PATH
57413
+ ];
57363
57414
  if (coverage)
57364
57415
  args2.push("--coverage");
57365
57416
  if (scope !== "all" && files.length > 0) {
@@ -57368,7 +57419,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
57368
57419
  return args2;
57369
57420
  }
57370
57421
  case "jest": {
57371
- const args2 = ["npx", "jest"];
57422
+ const args2 = ["npx", "jest", "--json"];
57372
57423
  if (coverage)
57373
57424
  args2.push("--coverage");
57374
57425
  if (scope !== "all" && files.length > 0) {
@@ -57464,6 +57515,122 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
57464
57515
  return null;
57465
57516
  }
57466
57517
  }
57518
+ function mapFrameworkStatusToResult(status) {
57519
+ if (typeof status !== "string")
57520
+ return null;
57521
+ const normalized = status.toLowerCase();
57522
+ if (normalized === "pass" || normalized === "passed")
57523
+ return "pass";
57524
+ if (normalized === "fail" || normalized === "failed")
57525
+ return "fail";
57526
+ if (normalized === "skip" || normalized === "skipped" || normalized === "pending" || normalized === "todo") {
57527
+ return "skip";
57528
+ }
57529
+ return null;
57530
+ }
57531
+ function firstLine(value) {
57532
+ if (typeof value !== "string")
57533
+ return;
57534
+ const line = value.split(`
57535
+ `).find((part) => part.trim().length > 0)?.trim();
57536
+ return line && line.length > 0 ? line : undefined;
57537
+ }
57538
+ function parseJestLikeJsonTestResults(payload) {
57539
+ if (typeof payload !== "object" || payload === null)
57540
+ return [];
57541
+ const rawSuites = payload.testResults;
57542
+ if (!Array.isArray(rawSuites))
57543
+ return [];
57544
+ const parsed = [];
57545
+ for (const suite of rawSuites) {
57546
+ if (typeof suite !== "object" || suite === null)
57547
+ continue;
57548
+ const suiteObj = suite;
57549
+ const rawFile = typeof suiteObj.name === "string" ? suiteObj.name : typeof suiteObj.testFilePath === "string" ? suiteObj.testFilePath : undefined;
57550
+ if (!rawFile)
57551
+ continue;
57552
+ const testFile = rawFile.replace(/\\/g, "/");
57553
+ const assertionResults = suiteObj.assertionResults;
57554
+ if (!Array.isArray(assertionResults))
57555
+ continue;
57556
+ for (const assertion of assertionResults) {
57557
+ if (typeof assertion !== "object" || assertion === null)
57558
+ continue;
57559
+ const assertionObj = assertion;
57560
+ const result = mapFrameworkStatusToResult(assertionObj.status);
57561
+ const testName = typeof assertionObj.fullName === "string" ? assertionObj.fullName : typeof assertionObj.title === "string" ? assertionObj.title : undefined;
57562
+ if (!result || !testName || testName.length === 0)
57563
+ continue;
57564
+ const failureMessages = Array.isArray(assertionObj.failureMessages) ? assertionObj.failureMessages : [];
57565
+ const firstFailure = failureMessages.find((entry) => typeof entry === "string" && entry.length > 0);
57566
+ const durationMs = typeof assertionObj.duration === "number" && Number.isFinite(assertionObj.duration) ? Math.max(assertionObj.duration, 0) : 0;
57567
+ parsed.push({
57568
+ testFile,
57569
+ testName,
57570
+ result,
57571
+ durationMs,
57572
+ errorMessage: firstLine(firstFailure),
57573
+ stackPrefix: firstLine(firstFailure)
57574
+ });
57575
+ }
57576
+ }
57577
+ return parsed;
57578
+ }
57579
+ function parseBunJsonLines(output) {
57580
+ const parsed = [];
57581
+ for (const line of output.split(`
57582
+ `)) {
57583
+ const trimmed = line.trim();
57584
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
57585
+ continue;
57586
+ try {
57587
+ const obj = JSON.parse(trimmed);
57588
+ const rawFile = typeof obj.file === "string" ? obj.file : typeof obj.testFile === "string" ? obj.testFile : typeof obj.path === "string" ? obj.path : undefined;
57589
+ const rawName = typeof obj.testName === "string" ? obj.testName : typeof obj.fullName === "string" ? obj.fullName : typeof obj.name === "string" ? obj.name : undefined;
57590
+ const result = mapFrameworkStatusToResult(typeof obj.status === "string" ? obj.status : obj.result);
57591
+ if (!rawFile || !rawName || !result)
57592
+ continue;
57593
+ const errorObj = typeof obj.error === "object" && obj.error !== null ? obj.error : undefined;
57594
+ const durationMs = typeof obj.durationMs === "number" && Number.isFinite(obj.durationMs) ? Math.max(obj.durationMs, 0) : typeof obj.duration === "number" && Number.isFinite(obj.duration) ? Math.max(obj.duration, 0) : 0;
57595
+ parsed.push({
57596
+ testFile: rawFile.replace(/\\/g, "/"),
57597
+ testName: rawName,
57598
+ result,
57599
+ durationMs,
57600
+ errorMessage: firstLine(errorObj?.message ?? obj.errorMessage),
57601
+ stackPrefix: firstLine(errorObj?.stack)
57602
+ });
57603
+ } catch {}
57604
+ }
57605
+ return parsed;
57606
+ }
57607
+ function parseFrameworkJsonTestResults(framework, output) {
57608
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
57609
+ if (jsonMatch) {
57610
+ try {
57611
+ const parsed = JSON.parse(jsonMatch[0]);
57612
+ const testResults = parseJestLikeJsonTestResults(parsed);
57613
+ if (testResults.length > 0)
57614
+ return testResults;
57615
+ } catch {}
57616
+ }
57617
+ for (const line of output.split(`
57618
+ `)) {
57619
+ const trimmed = line.trim();
57620
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
57621
+ continue;
57622
+ try {
57623
+ const parsed = JSON.parse(trimmed);
57624
+ const testResults = parseJestLikeJsonTestResults(parsed);
57625
+ if (testResults.length > 0)
57626
+ return testResults;
57627
+ } catch {}
57628
+ }
57629
+ if (framework === "bun") {
57630
+ return parseBunJsonLines(output);
57631
+ }
57632
+ return [];
57633
+ }
57467
57634
  function parseTestOutput2(framework, output) {
57468
57635
  const totals = {
57469
57636
  passed: 0,
@@ -57751,7 +57918,16 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
57751
57918
  };
57752
57919
  }
57753
57920
  const startTime = Date.now();
57921
+ const vitestJsonOutputPath = framework === "vitest" ? path46.join(cwd, ".swarm", "cache", "test-runner-vitest.json") : undefined;
57754
57922
  try {
57923
+ if (vitestJsonOutputPath) {
57924
+ try {
57925
+ fs29.mkdirSync(path46.dirname(vitestJsonOutputPath), { recursive: true });
57926
+ if (fs29.existsSync(vitestJsonOutputPath)) {
57927
+ fs29.unlinkSync(vitestJsonOutputPath);
57928
+ }
57929
+ } catch {}
57930
+ }
57755
57931
  const proc = bunSpawn(command, {
57756
57932
  stdout: "pipe",
57757
57933
  stderr: "pipe",
@@ -57772,13 +57948,37 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
57772
57948
  output += (output ? `
57773
57949
  ` : "") + stderrResult.text;
57774
57950
  }
57951
+ if (vitestJsonOutputPath) {
57952
+ try {
57953
+ if (fs29.existsSync(vitestJsonOutputPath)) {
57954
+ const vitestJsonOutput = fs29.readFileSync(vitestJsonOutputPath, "utf-8");
57955
+ if (vitestJsonOutput.trim().length > 0) {
57956
+ output += (output ? `
57957
+ ` : "") + vitestJsonOutput;
57958
+ }
57959
+ }
57960
+ } catch {}
57961
+ }
57775
57962
  if (stdoutResult.truncated || stderrResult.truncated) {
57776
57963
  output += `
57777
57964
  ... (output truncated at stream read limit)`;
57778
57965
  }
57779
57966
  const useDispatchParse = process.env.SWARM_LANG_BACKEND !== "legacy";
57780
57967
  const parsed = useDispatchParse ? await parseTestOutputViaDispatch(framework, output, cwd) ?? parseTestOutput2(framework, output) : parseTestOutput2(framework, output);
57781
- const { totals, coveragePercent } = parsed;
57968
+ const parsedTestCases = parseFrameworkJsonTestResults(framework, output);
57969
+ const totals = { ...parsed.totals };
57970
+ const { coveragePercent } = parsed;
57971
+ if (totals.total === 0 && parsedTestCases.length > 0) {
57972
+ for (const entry of parsedTestCases) {
57973
+ if (entry.result === "pass")
57974
+ totals.passed++;
57975
+ else if (entry.result === "fail")
57976
+ totals.failed++;
57977
+ else
57978
+ totals.skipped++;
57979
+ }
57980
+ totals.total = parsedTestCases.length;
57981
+ }
57782
57982
  const isTimeout = exitCode === -1;
57783
57983
  const testPassed = exitCode === 0 && totals.failed === 0;
57784
57984
  if (testPassed) {
@@ -57791,7 +57991,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
57791
57991
  duration_ms,
57792
57992
  totals,
57793
57993
  rawOutput: output,
57794
- outcome: "pass"
57994
+ outcome: "pass",
57995
+ testCases: parsedTestCases
57795
57996
  };
57796
57997
  if (coveragePercent !== undefined) {
57797
57998
  result.coveragePercent = coveragePercent;
@@ -57813,7 +58014,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
57813
58014
  rawOutput: output,
57814
58015
  error: isTimeout ? `Tests timed out after ${timeout_ms}ms` : `Tests failed with ${totals.failed} failures`,
57815
58016
  message: isTimeout ? `${framework} tests timed out after ${timeout_ms}ms` : `${framework} tests failed (${totals.failed}/${totals.total} failed)`,
57816
- outcome: isTimeout ? "error" : "regression"
58017
+ outcome: isTimeout ? "error" : "regression",
58018
+ testCases: parsedTestCases
57817
58019
  };
57818
58020
  if (coveragePercent !== undefined) {
57819
58021
  result.coveragePercent = coveragePercent;
@@ -57834,25 +58036,78 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
57834
58036
  };
57835
58037
  }
57836
58038
  }
57837
- function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles) {
58039
+ function normalizeHistoryTestFile(testFile, workingDir) {
58040
+ const normalized = testFile.replace(/\\/g, "/");
58041
+ if (!path46.isAbsolute(testFile))
58042
+ return normalized;
58043
+ const relative9 = path46.relative(workingDir, testFile);
58044
+ if (relative9.startsWith("..") || path46.isAbsolute(relative9)) {
58045
+ return normalized;
58046
+ }
58047
+ return relative9.replace(/\\/g, "/");
58048
+ }
58049
+ function combineAggregateResult(current, next) {
58050
+ if (current === "fail" || next === "fail")
58051
+ return "fail";
58052
+ if (current === "pass" || next === "pass")
58053
+ return "pass";
58054
+ return "skip";
58055
+ }
58056
+ function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles, parsedTestCases) {
57838
58057
  if (!result.totals || result.totals.total === 0)
57839
58058
  return;
57840
58059
  const now = new Date().toISOString();
57841
58060
  const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
57842
- for (const testFile of testFiles) {
58061
+ const aggregateResultsByFile = new Map;
58062
+ const validParsedCases = parsedTestCases?.filter((parsedCase) => parsedCase.testFile.length > 0 && parsedCase.testName.length > 0) ?? [];
58063
+ for (const parsedCase of validParsedCases) {
58064
+ const normalizedTestFile = normalizeHistoryTestFile(parsedCase.testFile, workingDir);
58065
+ try {
58066
+ appendTestRun({
58067
+ timestamp: now,
58068
+ taskId: "auto",
58069
+ testFile: normalizedTestFile,
58070
+ testName: parsedCase.testName,
58071
+ result: parsedCase.result,
58072
+ durationMs: parsedCase.durationMs,
58073
+ errorMessage: parsedCase.errorMessage,
58074
+ stackPrefix: parsedCase.stackPrefix,
58075
+ changedFiles
58076
+ }, workingDir);
58077
+ } catch {}
58078
+ aggregateResultsByFile.set(normalizedTestFile, combineAggregateResult(aggregateResultsByFile.get(normalizedTestFile), parsedCase.result));
58079
+ }
58080
+ if (aggregateResultsByFile.size === 0) {
58081
+ const aggregateResult = result.success ? "pass" : "fail";
58082
+ for (const testFile of testFiles) {
58083
+ aggregateResultsByFile.set(testFile.replace(/\\/g, "/"), aggregateResult);
58084
+ }
58085
+ }
58086
+ for (const [testFile, aggregateResult] of aggregateResultsByFile) {
57843
58087
  try {
57844
58088
  appendTestRun({
57845
58089
  timestamp: now,
57846
58090
  taskId: "auto",
57847
- testFile: testFile.replace(/\\/g, "/"),
57848
- testName: "(aggregate)",
57849
- result: result.success ? "pass" : "fail",
58091
+ testFile,
58092
+ testName: AGGREGATE_TEST_NAME,
58093
+ result: aggregateResult,
57850
58094
  durationMs: result.duration_ms || 0,
57851
58095
  changedFiles
57852
58096
  }, workingDir);
57853
58097
  } catch {}
57854
58098
  }
57855
58099
  }
58100
+ function selectHistoryForAnalysis(history) {
58101
+ const filesWithIndividualRecords = new Set;
58102
+ for (const record3 of history) {
58103
+ if (record3.testName !== AGGREGATE_TEST_NAME) {
58104
+ filesWithIndividualRecords.add(record3.testFile.toLowerCase());
58105
+ }
58106
+ }
58107
+ if (filesWithIndividualRecords.size === 0)
58108
+ return history;
58109
+ return history.filter((record3) => record3.testName !== AGGREGATE_TEST_NAME || !filesWithIndividualRecords.has(record3.testFile.toLowerCase()));
58110
+ }
57856
58111
  function analyzeFailures(workingDir) {
57857
58112
  const report = {
57858
58113
  flakyTests: [],
@@ -57860,7 +58115,7 @@ function analyzeFailures(workingDir) {
57860
58115
  quarantinedFailures: []
57861
58116
  };
57862
58117
  try {
57863
- const history = getAllHistory(workingDir);
58118
+ const history = selectHistoryForAnalysis(getAllHistory(workingDir));
57864
58119
  if (history.length === 0)
57865
58120
  return report;
57866
58121
  report.flakyTests = detectFlakyTests(history);
@@ -57881,7 +58136,7 @@ function analyzeFailures(workingDir) {
57881
58136
  } catch {}
57882
58137
  return report;
57883
58138
  }
57884
- var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
58139
+ var MAX_OUTPUT_BYTES3 = 512000, MAX_COMMAND_LENGTH2 = 500, DEFAULT_TIMEOUT_MS = 60000, MAX_TIMEOUT_MS = 300000, MAX_SAFE_TEST_FILES = 50, MAX_SAFE_SOURCE_FILES = 1, AGGREGATE_TEST_NAME = "(aggregate)", VITEST_JSON_OUTPUT_RELATIVE_PATH = ".swarm/cache/test-runner-vitest.json", POWERSHELL_METACHARACTERS, DISPATCH_FRAMEWORK_MAP, COMPOUND_TEST_EXTENSIONS, TEST_DIRECTORY_NAMES, SOURCE_EXTENSIONS, SKIP_DIRECTORIES, test_runner;
57885
58140
  var init_test_runner = __esm(() => {
57886
58141
  init_zod();
57887
58142
  init_discovery();
@@ -58327,7 +58582,7 @@ var init_test_runner = __esm(() => {
58327
58582
  return JSON.stringify(errorResult, null, 2);
58328
58583
  }
58329
58584
  const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
58330
- recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
58585
+ recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined, result.testCases);
58331
58586
  let historyReport;
58332
58587
  if (!result.success && result.totals && result.totals.failed > 0) {
58333
58588
  historyReport = analyzeFailures(workingDir);
@@ -60004,7 +60259,7 @@ var init_reset_session = __esm(() => {
60004
60259
  });
60005
60260
 
60006
60261
  // src/summaries/manager.ts
60007
- import { mkdirSync as mkdirSync14, readdirSync as readdirSync13, renameSync as renameSync11, rmSync as rmSync4, statSync as statSync12 } from "node:fs";
60262
+ import { mkdirSync as mkdirSync15, readdirSync as readdirSync13, renameSync as renameSync11, rmSync as rmSync4, statSync as statSync12 } from "node:fs";
60008
60263
  import * as path50 from "node:path";
60009
60264
  function sanitizeSummaryId(id) {
60010
60265
  if (!id || id.length === 0) {
@@ -60050,7 +60305,7 @@ async function storeSummary(directory, id, fullOutput, summaryText, maxStoredByt
60050
60305
  originalBytes: Buffer.byteLength(fullOutput, "utf8")
60051
60306
  };
60052
60307
  const entryJson = JSON.stringify(entry);
60053
- mkdirSync14(summaryDir, { recursive: true });
60308
+ mkdirSync15(summaryDir, { recursive: true });
60054
60309
  const tempPath = path50.join(summaryDir, `${sanitizedId}.json.tmp.${Date.now()}.${process.pid}`);
60055
60310
  try {
60056
60311
  await bunWrite(tempPath, entryJson);
@@ -67167,12 +67422,12 @@ __export(exports_evidence_summary_integration, {
67167
67422
  createEvidenceSummaryIntegration: () => createEvidenceSummaryIntegration,
67168
67423
  EvidenceSummaryIntegration: () => EvidenceSummaryIntegration
67169
67424
  });
67170
- import { existsSync as existsSync32, mkdirSync as mkdirSync16, writeFileSync as writeFileSync8 } from "node:fs";
67425
+ import { existsSync as existsSync32, mkdirSync as mkdirSync17, writeFileSync as writeFileSync8 } from "node:fs";
67171
67426
  import * as path57 from "node:path";
67172
67427
  function persistSummary(projectDir, artifact, filename) {
67173
67428
  const swarmPath = path57.join(projectDir, ".swarm");
67174
67429
  if (!existsSync32(swarmPath)) {
67175
- mkdirSync16(swarmPath, { recursive: true });
67430
+ mkdirSync17(swarmPath, { recursive: true });
67176
67431
  }
67177
67432
  const artifactPath = path57.join(swarmPath, filename);
67178
67433
  const content = JSON.stringify(artifact, null, 2);
@@ -72102,7 +72357,7 @@ var init_curator_drift = __esm(() => {
72102
72357
  var exports_project_context = {};
72103
72358
  __export(exports_project_context, {
72104
72359
  buildProjectContext: () => buildProjectContext,
72105
- _internals: () => _internals55,
72360
+ _internals: () => _internals56,
72106
72361
  LANG_BACKEND_DETECTION_TIMEOUT_MS: () => LANG_BACKEND_DETECTION_TIMEOUT_MS
72107
72362
  });
72108
72363
  import * as fs112 from "node:fs";
@@ -72186,7 +72441,7 @@ function selectLintCommand(backend, directory) {
72186
72441
  return null;
72187
72442
  }
72188
72443
  async function buildProjectContext(directory) {
72189
- const backend = await _internals55.pickBackend(directory);
72444
+ const backend = await _internals56.pickBackend(directory);
72190
72445
  if (!backend)
72191
72446
  return null;
72192
72447
  const ctx = emptyProjectContext();
@@ -72217,16 +72472,16 @@ async function buildProjectContext(directory) {
72217
72472
  if (backend.prompts.reviewerChecklist.length > 0) {
72218
72473
  ctx.REVIEWER_CHECKLIST = bulletList(backend.prompts.reviewerChecklist);
72219
72474
  }
72220
- const profiles = _internals55.pickedProfiles(directory);
72475
+ const profiles = _internals56.pickedProfiles(directory);
72221
72476
  if (profiles.length > 1) {
72222
72477
  ctx.PROJECT_CONTEXT_SECONDARY_LANGUAGES = profiles.slice(1).map((p) => p.id).join(", ");
72223
72478
  }
72224
72479
  return ctx;
72225
72480
  }
72226
- var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals55;
72481
+ var LANG_BACKEND_DETECTION_TIMEOUT_MS = 300, _internals56;
72227
72482
  var init_project_context = __esm(() => {
72228
72483
  init_dispatch();
72229
- _internals55 = {
72484
+ _internals56 = {
72230
72485
  pickBackend,
72231
72486
  pickedProfiles
72232
72487
  };
@@ -73119,7 +73374,7 @@ init_state();
73119
73374
  init_utils();
73120
73375
  init_bun_compat();
73121
73376
  init_utils2();
73122
- import { renameSync as renameSync13, unlinkSync as unlinkSync10 } from "node:fs";
73377
+ import { renameSync as renameSync13, unlinkSync as unlinkSync11 } from "node:fs";
73123
73378
  import * as nodePath2 from "node:path";
73124
73379
  function createAgentActivityHooks(config3, directory) {
73125
73380
  if (config3.hooks?.agent_activity === false) {
@@ -73197,7 +73452,7 @@ async function doFlush(directory) {
73197
73452
  renameSync13(tempPath, path62);
73198
73453
  } catch (writeError) {
73199
73454
  try {
73200
- unlinkSync10(tempPath);
73455
+ unlinkSync11(tempPath);
73201
73456
  } catch {}
73202
73457
  throw writeError;
73203
73458
  }
@@ -87173,7 +87428,7 @@ ${body2}`);
87173
87428
  import {
87174
87429
  appendFileSync as appendFileSync12,
87175
87430
  existsSync as existsSync53,
87176
- mkdirSync as mkdirSync24,
87431
+ mkdirSync as mkdirSync25,
87177
87432
  readFileSync as readFileSync44,
87178
87433
  writeFileSync as writeFileSync17
87179
87434
  } from "node:fs";
@@ -87212,7 +87467,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87212
87467
  throw new Error(`writeCouncilEvidence: invalid taskId "${synthesis.taskId}" — must match N.M or N.M.P format`);
87213
87468
  }
87214
87469
  const dir = join83(workingDir, EVIDENCE_DIR2);
87215
- mkdirSync24(dir, { recursive: true });
87470
+ mkdirSync25(dir, { recursive: true });
87216
87471
  const filePath = join83(dir, `${synthesis.taskId}.json`);
87217
87472
  const existingRoot = Object.create(null);
87218
87473
  if (existsSync53(filePath)) {
@@ -87248,7 +87503,7 @@ function writeCouncilEvidence(workingDir, synthesis) {
87248
87503
  writeFileSync17(filePath, JSON.stringify(updated, null, 2));
87249
87504
  try {
87250
87505
  const councilDir = join83(workingDir, ".swarm", "council");
87251
- mkdirSync24(councilDir, { recursive: true });
87506
+ mkdirSync25(councilDir, { recursive: true });
87252
87507
  const auditLine = JSON.stringify({
87253
87508
  round: synthesis.roundNumber,
87254
87509
  verdict: synthesis.overallVerdict,
@@ -87577,12 +87832,12 @@ function buildFinalCouncilFeedback(projectSummary, verdict, vetoedBy, requiredFi
87577
87832
  }
87578
87833
 
87579
87834
  // src/council/criteria-store.ts
87580
- import { existsSync as existsSync54, mkdirSync as mkdirSync25, readFileSync as readFileSync45, writeFileSync as writeFileSync18 } from "node:fs";
87835
+ import { existsSync as existsSync54, mkdirSync as mkdirSync26, readFileSync as readFileSync45, writeFileSync as writeFileSync18 } from "node:fs";
87581
87836
  import { join as join84 } from "node:path";
87582
87837
  var COUNCIL_DIR = ".swarm/council";
87583
87838
  function writeCriteria(workingDir, taskId, criteria) {
87584
87839
  const dir = join84(workingDir, COUNCIL_DIR);
87585
- mkdirSync25(dir, { recursive: true });
87840
+ mkdirSync26(dir, { recursive: true });
87586
87841
  const payload = {
87587
87842
  taskId,
87588
87843
  criteria,
@@ -89436,12 +89691,12 @@ function extractFilename(code, language, index) {
89436
89691
  `);
89437
89692
  const ext = EXT_MAP[language.toLowerCase()] ?? ".txt";
89438
89693
  if (lines.length > 0) {
89439
- const firstLine = lines[0].trim();
89440
- const filenameMatch = firstLine.match(/^[#/]+\s*filename[:\s]+(\S+\.\w+)/i);
89694
+ const firstLine2 = lines[0].trim();
89695
+ const filenameMatch = firstLine2.match(/^[#/]+\s*filename[:\s]+(\S+\.\w+)/i);
89441
89696
  if (filenameMatch) {
89442
89697
  return filenameMatch[1];
89443
89698
  }
89444
- const bareMatch = firstLine.match(/^[#/]+\s*(\w+\.\w+)\s*$/);
89699
+ const bareMatch = firstLine2.match(/^[#/]+\s*(\w+\.\w+)\s*$/);
89445
89700
  if (bareMatch) {
89446
89701
  return bareMatch[1];
89447
89702
  }
@@ -99930,10 +100185,10 @@ init_zod();
99930
100185
  init_loader();
99931
100186
  import {
99932
100187
  existsSync as existsSync72,
99933
- mkdirSync as mkdirSync31,
100188
+ mkdirSync as mkdirSync32,
99934
100189
  readFileSync as readFileSync63,
99935
100190
  renameSync as renameSync20,
99936
- unlinkSync as unlinkSync15,
100191
+ unlinkSync as unlinkSync16,
99937
100192
  writeFileSync as writeFileSync25
99938
100193
  } from "node:fs";
99939
100194
  import path124 from "node:path";
@@ -100129,7 +100384,7 @@ function getPhaseMutationGapFinding(phaseNumber, workingDir) {
100129
100384
  }
100130
100385
  function writePhaseCouncilEvidence(workingDir, synthesis) {
100131
100386
  const evidenceDir = path124.join(workingDir, ".swarm", "evidence", String(synthesis.phaseNumber));
100132
- mkdirSync31(evidenceDir, { recursive: true });
100387
+ mkdirSync32(evidenceDir, { recursive: true });
100133
100388
  const evidenceFile = path124.join(evidenceDir, "phase-council.json");
100134
100389
  const evidenceBundle = {
100135
100390
  entries: [
@@ -100167,7 +100422,7 @@ function writePhaseCouncilEvidence(workingDir, synthesis) {
100167
100422
  renameSync20(tempFile, evidenceFile);
100168
100423
  } finally {
100169
100424
  if (existsSync72(tempFile)) {
100170
- unlinkSync15(tempFile);
100425
+ unlinkSync16(tempFile);
100171
100426
  }
100172
100427
  }
100173
100428
  }
@@ -102374,7 +102629,7 @@ import * as path131 from "node:path";
102374
102629
 
102375
102630
  // src/mutation/engine.ts
102376
102631
  import { spawnSync as spawnSync3 } from "node:child_process";
102377
- import { unlinkSync as unlinkSync16, writeFileSync as writeFileSync26 } from "node:fs";
102632
+ import { unlinkSync as unlinkSync17, writeFileSync as writeFileSync26 } from "node:fs";
102378
102633
  import * as path130 from "node:path";
102379
102634
 
102380
102635
  // src/mutation/equivalence.ts
@@ -102506,6 +102761,12 @@ async function batchCheckEquivalence(patches, llmJudge) {
102506
102761
  var MUTATION_TIMEOUT_MS = 30000;
102507
102762
  var TOTAL_BUDGET_MS = 300000;
102508
102763
  var GIT_APPLY_TIMEOUT_MS = 5000;
102764
+ var _internals51 = {
102765
+ executeMutation,
102766
+ computeReport,
102767
+ executeMutationSuite,
102768
+ spawnSync: spawnSync3
102769
+ };
102509
102770
  async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102510
102771
  const startTime = Date.now();
102511
102772
  let outcome = "survived";
@@ -102532,7 +102793,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102532
102793
  };
102533
102794
  }
102534
102795
  try {
102535
- const applyResult = spawnSync3("git", ["apply", "--", patchFile], {
102796
+ const applyResult = _internals51.spawnSync("git", ["apply", "--", patchFile], {
102536
102797
  cwd: workingDir,
102537
102798
  timeout: GIT_APPLY_TIMEOUT_MS,
102538
102799
  stdio: "pipe"
@@ -102561,7 +102822,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102561
102822
  }
102562
102823
  let testPassed = false;
102563
102824
  try {
102564
- const spawnResult = spawnSync3(testCommand[0], testCommand.slice(1), {
102825
+ const spawnResult = _internals51.spawnSync(testCommand[0], testCommand.slice(1), {
102565
102826
  cwd: workingDir,
102566
102827
  timeout: MUTATION_TIMEOUT_MS,
102567
102828
  stdio: "pipe"
@@ -102594,7 +102855,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102594
102855
  } finally {
102595
102856
  if (patchFile) {
102596
102857
  try {
102597
- const revertResult = spawnSync3("git", ["apply", "-R", "--", patchFile], {
102858
+ const revertResult = _internals51.spawnSync("git", ["apply", "-R", "--", patchFile], {
102598
102859
  cwd: workingDir,
102599
102860
  timeout: GIT_APPLY_TIMEOUT_MS,
102600
102861
  stdio: "pipe"
@@ -102613,7 +102874,7 @@ async function executeMutation(patch, testCommand, _testFiles, workingDir) {
102613
102874
  revertError = new Error(`Failed to revert mutation ${patch.id}: ${revertErr}. Working tree may be dirty.`);
102614
102875
  }
102615
102876
  try {
102616
- unlinkSync16(patchFile);
102877
+ unlinkSync17(patchFile);
102617
102878
  } catch (_unlinkErr) {}
102618
102879
  }
102619
102880
  }
@@ -102787,7 +103048,7 @@ async function executeMutationSuite(patches, testCommand, testFiles, workingDir,
102787
103048
  }
102788
103049
 
102789
103050
  // src/mutation/gate.ts
102790
- var _internals51 = {
103051
+ var _internals52 = {
102791
103052
  evaluateMutationGate,
102792
103053
  buildTestImprovementPrompt,
102793
103054
  buildMessage
@@ -102808,8 +103069,8 @@ function evaluateMutationGate(report, passThreshold = PASS_THRESHOLD, warnThresh
102808
103069
  } else {
102809
103070
  verdict = "fail";
102810
103071
  }
102811
- const testImprovementPrompt = _internals51.buildTestImprovementPrompt(report, passThreshold, verdict);
102812
- const message = _internals51.buildMessage(verdict, adjustedKillRate, report.killed, report.totalMutants, report.equivalent, warnThreshold);
103072
+ const testImprovementPrompt = _internals52.buildTestImprovementPrompt(report, passThreshold, verdict);
103073
+ const message = _internals52.buildMessage(verdict, adjustedKillRate, report.killed, report.totalMutants, report.equivalent, warnThreshold);
102813
103074
  return {
102814
103075
  verdict,
102815
103076
  killRate: report.killRate,
@@ -103426,7 +103687,7 @@ import * as path135 from "node:path";
103426
103687
  init_bun_compat();
103427
103688
  import * as fs104 from "node:fs";
103428
103689
  import * as path134 from "node:path";
103429
- var _internals52 = { bunSpawn };
103690
+ var _internals53 = { bunSpawn };
103430
103691
  var _swarmGitExcludedChecked = false;
103431
103692
  function fileCoversSwarm(content) {
103432
103693
  for (const rawLine of content.split(`
@@ -103459,7 +103720,7 @@ async function ensureSwarmGitExcluded(directory, options = {}) {
103459
103720
  checkIgnoreExitCode
103460
103721
  ] = await Promise.all([
103461
103722
  (async () => {
103462
- const proc = _internals52.bunSpawn(["git", "-C", directory, "rev-parse", "--show-toplevel"], GIT_SPAWN_OPTIONS);
103723
+ const proc = _internals53.bunSpawn(["git", "-C", directory, "rev-parse", "--show-toplevel"], GIT_SPAWN_OPTIONS);
103463
103724
  try {
103464
103725
  return await Promise.all([proc.exited, proc.stdout.text()]);
103465
103726
  } finally {
@@ -103469,7 +103730,7 @@ async function ensureSwarmGitExcluded(directory, options = {}) {
103469
103730
  }
103470
103731
  })(),
103471
103732
  (async () => {
103472
- const proc = _internals52.bunSpawn(["git", "-C", directory, "rev-parse", "--git-path", "info/exclude"], GIT_SPAWN_OPTIONS);
103733
+ const proc = _internals53.bunSpawn(["git", "-C", directory, "rev-parse", "--git-path", "info/exclude"], GIT_SPAWN_OPTIONS);
103473
103734
  try {
103474
103735
  return await Promise.all([proc.exited, proc.stdout.text()]);
103475
103736
  } finally {
@@ -103479,7 +103740,7 @@ async function ensureSwarmGitExcluded(directory, options = {}) {
103479
103740
  }
103480
103741
  })(),
103481
103742
  (async () => {
103482
- const proc = _internals52.bunSpawn(["git", "-C", directory, "check-ignore", "-q", ".swarm/.gitkeep"], GIT_SPAWN_OPTIONS);
103743
+ const proc = _internals53.bunSpawn(["git", "-C", directory, "check-ignore", "-q", ".swarm/.gitkeep"], GIT_SPAWN_OPTIONS);
103483
103744
  try {
103484
103745
  return await proc.exited;
103485
103746
  } finally {
@@ -103518,7 +103779,7 @@ async function ensureSwarmGitExcluded(directory, options = {}) {
103518
103779
  }
103519
103780
  } catch {}
103520
103781
  }
103521
- const trackedProc = _internals52.bunSpawn(["git", "-C", directory, "ls-files", "--", ".swarm"], GIT_SPAWN_OPTIONS);
103782
+ const trackedProc = _internals53.bunSpawn(["git", "-C", directory, "ls-files", "--", ".swarm"], GIT_SPAWN_OPTIONS);
103522
103783
  let trackedExitCode;
103523
103784
  let trackedOutput;
103524
103785
  try {
@@ -103543,7 +103804,7 @@ async function ensureSwarmGitExcluded(directory, options = {}) {
103543
103804
  }
103544
103805
 
103545
103806
  // src/hooks/diff-scope.ts
103546
- var _internals53 = { bunSpawn };
103807
+ var _internals54 = { bunSpawn };
103547
103808
  function getDeclaredScope(taskId, directory) {
103548
103809
  try {
103549
103810
  const planPath = path135.join(directory, ".swarm", "plan.json");
@@ -103578,7 +103839,7 @@ var GIT_DIFF_SPAWN_OPTIONS = {
103578
103839
  };
103579
103840
  async function getChangedFiles(directory) {
103580
103841
  try {
103581
- const proc = _internals53.bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
103842
+ const proc = _internals54.bunSpawn(["git", "diff", "--name-only", "HEAD~1"], {
103582
103843
  cwd: directory,
103583
103844
  ...GIT_DIFF_SPAWN_OPTIONS
103584
103845
  });
@@ -103595,7 +103856,7 @@ async function getChangedFiles(directory) {
103595
103856
  return stdout.trim().split(`
103596
103857
  `).map((f) => f.trim()).filter((f) => f.length > 0);
103597
103858
  }
103598
- const proc2 = _internals53.bunSpawn(["git", "diff", "--name-only", "HEAD"], {
103859
+ const proc2 = _internals54.bunSpawn(["git", "diff", "--name-only", "HEAD"], {
103599
103860
  cwd: directory,
103600
103861
  ...GIT_DIFF_SPAWN_OPTIONS
103601
103862
  });
@@ -103653,7 +103914,7 @@ init_telemetry();
103653
103914
  init_file_locks();
103654
103915
  import * as fs106 from "node:fs";
103655
103916
  import * as path136 from "node:path";
103656
- var _internals54 = {
103917
+ var _internals55 = {
103657
103918
  listActiveLocks,
103658
103919
  verifyLeanTurboTaskCompletion
103659
103920
  };
@@ -103795,7 +104056,7 @@ function verifyLeanTurboTaskCompletion(directory, taskId, sessionID) {
103795
104056
  }
103796
104057
  };
103797
104058
  }
103798
- const activeLocks = _internals54.listActiveLocks(directory);
104059
+ const activeLocks = _internals55.listActiveLocks(directory);
103799
104060
  const laneLocks = activeLocks.filter((lock) => lock.laneId === lane.laneId);
103800
104061
  if (laneLocks.length > 0) {
103801
104062
  return {
@@ -1,3 +1,5 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ type SpawnSyncFn = typeof spawnSync;
1
3
  export type MutationOutcome = 'killed' | 'survived' | 'timeout' | 'error' | 'equivalent' | 'skipped';
2
4
  export interface MutationPatch {
3
5
  id: string;
@@ -46,7 +48,9 @@ export declare const _internals: {
46
48
  executeMutation: typeof executeMutation;
47
49
  computeReport: typeof computeReport;
48
50
  executeMutationSuite: typeof executeMutationSuite;
51
+ spawnSync: SpawnSyncFn;
49
52
  };
50
53
  export declare function executeMutation(patch: MutationPatch, testCommand: string[], _testFiles: string[], workingDir: string): Promise<MutationResult>;
51
54
  export declare function computeReport(results: MutationResult[], durationMs: number, budgetMs?: number): MutationReport;
52
55
  export declare function executeMutationSuite(patches: MutationPatch[], testCommand: string[], testFiles: string[], workingDir: string, budgetMs?: number, onProgress?: (completed: number, total: number, result: MutationResult) => void, sourceFiles?: Map<string, string>): Promise<MutationReport>;
56
+ export {};
@@ -32,6 +32,14 @@ export interface TestTotals {
32
32
  skipped: number;
33
33
  total: number;
34
34
  }
35
+ export interface ParsedTestCaseResult {
36
+ testFile: string;
37
+ testName: string;
38
+ result: 'pass' | 'fail' | 'skip';
39
+ durationMs: number;
40
+ errorMessage?: string;
41
+ stackPrefix?: string;
42
+ }
35
43
  export interface TestSuccessResult {
36
44
  success: true;
37
45
  framework: TestFramework;
@@ -42,6 +50,7 @@ export interface TestSuccessResult {
42
50
  totals: TestTotals;
43
51
  coveragePercent?: number;
44
52
  rawOutput?: string;
53
+ testCases?: ParsedTestCaseResult[];
45
54
  message?: string;
46
55
  outcome?: RegressionOutcome;
47
56
  }
@@ -56,6 +65,7 @@ export interface TestErrorResult {
56
65
  coveragePercent?: number;
57
66
  error: string;
58
67
  rawOutput?: string;
68
+ testCases?: ParsedTestCaseResult[];
59
69
  message?: string;
60
70
  outcome?: RegressionOutcome;
61
71
  attempted_scope?: 'graph';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm",
3
- "version": "7.24.1",
3
+ "version": "7.25.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",