opencode-swarm 7.24.0 → 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.0",
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",
@@ -35267,6 +35267,20 @@ function normalizeEntry(raw) {
35267
35267
  ro.failed_after_shown_count = typeof ro.failed_after_count === "number" ? ro.failed_after_count : 0;
35268
35268
  }
35269
35269
  }
35270
+ try {
35271
+ if (typeof obj.encounter_score !== "number" || Number.isNaN(obj.encounter_score)) {
35272
+ obj.encounter_score = 0;
35273
+ }
35274
+ } catch {
35275
+ try {
35276
+ Object.defineProperty(obj, "encounter_score", {
35277
+ value: 0,
35278
+ writable: true,
35279
+ configurable: true,
35280
+ enumerable: true
35281
+ });
35282
+ } catch {}
35283
+ }
35270
35284
  const arrayFields = [
35271
35285
  "triggers",
35272
35286
  "required_actions",
@@ -35826,12 +35840,26 @@ import path12 from "path";
35826
35840
  function isAlreadyInHive(entry, hiveEntries, threshold) {
35827
35841
  return findNearDuplicate(entry.lesson, hiveEntries, threshold) !== undefined;
35828
35842
  }
35829
- function countDistinctPhases(confirmedBy) {
35843
+ function isHiveEligible(entry, autoPromoteDays) {
35830
35844
  const phaseNumbers = new Set;
35831
- for (const record3 of confirmedBy) {
35832
- phaseNumbers.add(record3.phase_number);
35845
+ for (const record3 of entry.confirmed_by ?? []) {
35846
+ if (record3 && typeof record3.phase_number === "number") {
35847
+ phaseNumbers.add(record3.phase_number);
35848
+ }
35849
+ }
35850
+ if (entry.hive_eligible === true && phaseNumbers.size >= 3) {
35851
+ return true;
35852
+ }
35853
+ if ((entry.tags ?? []).includes("hive-fast-track")) {
35854
+ return true;
35855
+ }
35856
+ const createdMs = Date.parse(entry.created_at);
35857
+ const ageMs = Number.isFinite(createdMs) ? Date.now() - createdMs : 0;
35858
+ const ageThresholdMs = autoPromoteDays * 86400000;
35859
+ if (ageMs >= ageThresholdMs) {
35860
+ return true;
35833
35861
  }
35834
- return phaseNumbers.size;
35862
+ return false;
35835
35863
  }
35836
35864
  function countDistinctProjects(confirmedBy) {
35837
35865
  const projectNames = new Set;
@@ -35849,12 +35877,6 @@ function calculateEncounterScore(currentScore, isSameProject, config3) {
35849
35877
  const newScore = currentScore + increment;
35850
35878
  return Math.min(Math.max(newScore, config3.min_encounter_score), config3.max_encounter_score);
35851
35879
  }
35852
- function getEntryAgeMs(createdAt) {
35853
- const createdTime = new Date(createdAt).getTime();
35854
- if (Number.isNaN(createdTime))
35855
- return 0;
35856
- return Date.now() - createdTime;
35857
- }
35858
35880
  async function checkHivePromotions(swarmEntries, config3) {
35859
35881
  let newPromotions = 0;
35860
35882
  let encountersIncremented = 0;
@@ -35873,19 +35895,7 @@ async function checkHivePromotions(swarmEntries, config3) {
35873
35895
  if (isAlreadyInHive(swarmEntry, hiveEntries, config3.dedup_threshold)) {
35874
35896
  continue;
35875
35897
  }
35876
- let shouldPromote = false;
35877
- if (swarmEntry.hive_eligible === true && countDistinctPhases(swarmEntry.confirmed_by) >= 3) {
35878
- shouldPromote = true;
35879
- }
35880
- if (swarmEntry.tags.includes("hive-fast-track")) {
35881
- shouldPromote = true;
35882
- }
35883
- const ageMs = getEntryAgeMs(swarmEntry.created_at);
35884
- const ageThresholdMs = config3.auto_promote_days * 86400000;
35885
- if (ageMs >= ageThresholdMs) {
35886
- shouldPromote = true;
35887
- }
35888
- if (!shouldPromote) {
35898
+ if (!isHiveEligible(swarmEntry, config3.auto_promote_days)) {
35889
35899
  continue;
35890
35900
  }
35891
35901
  const validationResult = validateLesson(swarmEntry.lesson, hiveEntries.map((e) => e.lesson), {
@@ -37803,12 +37813,14 @@ async function handleCloseCommand(directory, args, options = {}) {
37803
37813
  if (planExists) {
37804
37814
  planAlreadyDone = phases.length > 0 && phases.every((p) => p.status === "complete" || p.status === "completed" || p.status === "blocked" || p.status === "closed");
37805
37815
  }
37806
- const config3 = KnowledgeConfigSchema.parse({});
37816
+ const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
37817
+ const config3 = KnowledgeConfigSchema.parse(loadedConfig.knowledge ?? {});
37807
37818
  const projectName = planData.title ?? "Unknown Project";
37808
37819
  const closedPhases = [];
37809
37820
  const closedTasks = [];
37810
37821
  const warnings = [];
37811
37822
  let hivePromoted = 0;
37823
+ let hiveSkipped = 0;
37812
37824
  if (!planAlreadyDone) {
37813
37825
  for (const phase of inProgressPhases) {
37814
37826
  closedPhases.push(phase.id);
@@ -37931,23 +37943,35 @@ async function handleCloseCommand(directory, args, options = {}) {
37931
37943
  await fs7.unlink(lessonsFilePath).catch(() => {});
37932
37944
  }
37933
37945
  if (curationSucceeded) {
37934
- try {
37935
- const knowledgePath = resolveSwarmKnowledgePath(directory);
37936
- const entries = await readKnowledge(knowledgePath);
37937
- if (entries.length > 0) {
37938
- for (const entry of entries) {
37939
- try {
37940
- await promoteToHive(directory, entry.lesson, entry.category);
37941
- hivePromoted++;
37942
- } catch (promotionErr) {
37943
- const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
37944
- warnings.push(`Hive promotion skipped for lesson: ${msg}`);
37946
+ if (config3.hive_enabled === false) {} else {
37947
+ try {
37948
+ const knowledgePath = resolveSwarmKnowledgePath(directory);
37949
+ const entries = await readKnowledge(knowledgePath);
37950
+ const autoPromoteDays = config3.auto_promote_days;
37951
+ if (entries.length > 0) {
37952
+ for (const entry of entries) {
37953
+ if (!isHiveEligible(entry, autoPromoteDays)) {
37954
+ hiveSkipped++;
37955
+ continue;
37956
+ }
37957
+ try {
37958
+ const result = await promoteToHive(directory, entry.lesson, entry.category);
37959
+ if (!result.includes("already exists")) {
37960
+ hivePromoted++;
37961
+ }
37962
+ } catch (promotionErr) {
37963
+ const msg = promotionErr instanceof Error ? promotionErr.message : String(promotionErr);
37964
+ warnings.push(`Hive promotion skipped for lesson: ${msg}`);
37965
+ }
37966
+ }
37967
+ if (hiveSkipped > 0) {
37968
+ warnings.push(`${hiveSkipped} swarm knowledge entr${hiveSkipped === 1 ? "y" : "ies"} not eligible for hive promotion`);
37945
37969
  }
37946
37970
  }
37971
+ } catch (hiveErr) {
37972
+ const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
37973
+ warnings.push(`Hive promotion failed: ${msg}`);
37947
37974
  }
37948
- } catch (hiveErr) {
37949
- const msg = hiveErr instanceof Error ? hiveErr.message : String(hiveErr);
37950
- warnings.push(`Hive promotion failed: ${msg}`);
37951
37975
  }
37952
37976
  }
37953
37977
  const fallbackKnowledgeCreated = curationResult?.stored ?? 0;
@@ -37964,8 +37988,8 @@ async function handleCloseCommand(directory, args, options = {}) {
37964
37988
  let skillReviewSummary = "";
37965
37989
  if (runSkillReview) {
37966
37990
  try {
37967
- const { config: loadedConfig } = loadPluginConfigWithMeta(directory);
37968
- const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig.skill_improver ?? {});
37991
+ const { config: loadedConfig2 } = loadPluginConfigWithMeta(directory);
37992
+ const skillImproverConfig = SkillImproverConfigSchema.parse(loadedConfig2.skill_improver ?? {});
37969
37993
  const skillReviewResult = await runAbortableSkillReview({
37970
37994
  directory,
37971
37995
  config: skillImproverConfig,
@@ -38330,7 +38354,6 @@ var init_close = __esm(() => {
38330
38354
  "handoff-prompt.md",
38331
38355
  "handoff-consumed.md",
38332
38356
  "escalation-report.md",
38333
- "knowledge.jsonl",
38334
38357
  "knowledge-rejected.jsonl",
38335
38358
  "repo-graph.json",
38336
38359
  "doc-manifest.json",
@@ -45976,7 +45999,14 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
45976
45999
  return args;
45977
46000
  }
45978
46001
  case "vitest": {
45979
- 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
+ ];
45980
46010
  if (coverage)
45981
46011
  args.push("--coverage");
45982
46012
  if (scope !== "all" && files.length > 0)
@@ -45984,7 +46014,7 @@ function defaultBuildTestCommand(profile, framework, files, dir = ".", opts = {}
45984
46014
  return args;
45985
46015
  }
45986
46016
  case "jest": {
45987
- const args = ["npx", "jest"];
46017
+ const args = ["npx", "jest", "--json"];
45988
46018
  if (coverage)
45989
46019
  args.push("--coverage");
45990
46020
  if (scope !== "all" && files.length > 0)
@@ -47219,6 +47249,42 @@ function sanitizeChangedFiles(changedFiles) {
47219
47249
  const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
47220
47250
  return validFiles.slice(0, MAX_CHANGED_FILES);
47221
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
+ }
47222
47288
  function appendTestRun(record3, workingDir) {
47223
47289
  if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
47224
47290
  throw new TypeError("testFile must be a non-empty string");
@@ -47256,16 +47322,16 @@ function appendTestRun(record3, workingDir) {
47256
47322
  }
47257
47323
  const existingRecords = readAllRecords(historyPath);
47258
47324
  existingRecords.push(sanitizedRecord);
47259
- const recordsByFile = new Map;
47325
+ const recordsByTest = new Map;
47260
47326
  for (const rec of existingRecords) {
47261
- const normalizedFile = rec.testFile.toLowerCase();
47262
- if (!recordsByFile.has(normalizedFile)) {
47263
- recordsByFile.set(normalizedFile, []);
47327
+ const normalizedKey = `${rec.testFile.toLowerCase()}|${rec.testName.toLowerCase()}`;
47328
+ if (!recordsByTest.has(normalizedKey)) {
47329
+ recordsByTest.set(normalizedKey, []);
47264
47330
  }
47265
- recordsByFile.get(normalizedFile).push(rec);
47331
+ recordsByTest.get(normalizedKey).push(rec);
47266
47332
  }
47267
47333
  const prunedRecords = [];
47268
- for (const [, records] of recordsByFile) {
47334
+ for (const [, records] of recordsByTest) {
47269
47335
  records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
47270
47336
  const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
47271
47337
  prunedRecords.push(...toKeep);
@@ -47305,8 +47371,9 @@ function readAllRecords(historyPath) {
47305
47371
  }
47306
47372
  try {
47307
47373
  const parsed = JSON.parse(trimmed);
47308
- if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
47309
- records.push(parsed);
47374
+ const record3 = parseStoredRecord(parsed);
47375
+ if (record3) {
47376
+ records.push(record3);
47310
47377
  }
47311
47378
  } catch {}
47312
47379
  }
@@ -48345,7 +48412,14 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48345
48412
  return args;
48346
48413
  }
48347
48414
  case "vitest": {
48348
- 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
+ ];
48349
48423
  if (coverage)
48350
48424
  args.push("--coverage");
48351
48425
  if (scope !== "all" && files.length > 0) {
@@ -48354,7 +48428,7 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48354
48428
  return args;
48355
48429
  }
48356
48430
  case "jest": {
48357
- const args = ["npx", "jest"];
48431
+ const args = ["npx", "jest", "--json"];
48358
48432
  if (coverage)
48359
48433
  args.push("--coverage");
48360
48434
  if (scope !== "all" && files.length > 0) {
@@ -48450,6 +48524,122 @@ function buildTestCommand2(framework, scope, files, coverage, baseDir) {
48450
48524
  return null;
48451
48525
  }
48452
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
+ }
48453
48643
  function parseTestOutput2(framework, output) {
48454
48644
  const totals = {
48455
48645
  passed: 0,
@@ -48737,7 +48927,16 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48737
48927
  };
48738
48928
  }
48739
48929
  const startTime = Date.now();
48930
+ const vitestJsonOutputPath = framework === "vitest" ? path39.join(cwd, ".swarm", "cache", "test-runner-vitest.json") : undefined;
48740
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
+ }
48741
48940
  const proc = bunSpawn(command, {
48742
48941
  stdout: "pipe",
48743
48942
  stderr: "pipe",
@@ -48758,13 +48957,37 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48758
48957
  output += (output ? `
48759
48958
  ` : "") + stderrResult.text;
48760
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
+ }
48761
48971
  if (stdoutResult.truncated || stderrResult.truncated) {
48762
48972
  output += `
48763
48973
  ... (output truncated at stream read limit)`;
48764
48974
  }
48765
48975
  const useDispatchParse = process.env.SWARM_LANG_BACKEND !== "legacy";
48766
48976
  const parsed = useDispatchParse ? await parseTestOutputViaDispatch(framework, output, cwd) ?? parseTestOutput2(framework, output) : parseTestOutput2(framework, output);
48767
- 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
+ }
48768
48991
  const isTimeout = exitCode === -1;
48769
48992
  const testPassed = exitCode === 0 && totals.failed === 0;
48770
48993
  if (testPassed) {
@@ -48777,7 +49000,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48777
49000
  duration_ms,
48778
49001
  totals,
48779
49002
  rawOutput: output,
48780
- outcome: "pass"
49003
+ outcome: "pass",
49004
+ testCases: parsedTestCases
48781
49005
  };
48782
49006
  if (coveragePercent !== undefined) {
48783
49007
  result.coveragePercent = coveragePercent;
@@ -48799,7 +49023,8 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48799
49023
  rawOutput: output,
48800
49024
  error: isTimeout ? `Tests timed out after ${timeout_ms}ms` : `Tests failed with ${totals.failed} failures`,
48801
49025
  message: isTimeout ? `${framework} tests timed out after ${timeout_ms}ms` : `${framework} tests failed (${totals.failed}/${totals.total} failed)`,
48802
- outcome: isTimeout ? "error" : "regression"
49026
+ outcome: isTimeout ? "error" : "regression",
49027
+ testCases: parsedTestCases
48803
49028
  };
48804
49029
  if (coveragePercent !== undefined) {
48805
49030
  result.coveragePercent = coveragePercent;
@@ -48820,25 +49045,78 @@ async function runTests(framework, scope, files, coverage, timeout_ms, cwd) {
48820
49045
  };
48821
49046
  }
48822
49047
  }
48823
- 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) {
48824
49066
  if (!result.totals || result.totals.total === 0)
48825
49067
  return;
48826
49068
  const now = new Date().toISOString();
48827
49069
  const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
48828
- 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);
49074
+ try {
49075
+ appendTestRun({
49076
+ timestamp: now,
49077
+ taskId: "auto",
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) {
48829
49096
  try {
48830
49097
  appendTestRun({
48831
49098
  timestamp: now,
48832
49099
  taskId: "auto",
48833
- testFile: testFile.replace(/\\/g, "/"),
48834
- testName: "(aggregate)",
48835
- result: result.success ? "pass" : "fail",
49100
+ testFile,
49101
+ testName: AGGREGATE_TEST_NAME,
49102
+ result: aggregateResult,
48836
49103
  durationMs: result.duration_ms || 0,
48837
49104
  changedFiles
48838
49105
  }, workingDir);
48839
49106
  } catch {}
48840
49107
  }
48841
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
+ }
48842
49120
  function analyzeFailures(workingDir) {
48843
49121
  const report = {
48844
49122
  flakyTests: [],
@@ -48846,7 +49124,7 @@ function analyzeFailures(workingDir) {
48846
49124
  quarantinedFailures: []
48847
49125
  };
48848
49126
  try {
48849
- const history = getAllHistory(workingDir);
49127
+ const history = selectHistoryForAnalysis(getAllHistory(workingDir));
48850
49128
  if (history.length === 0)
48851
49129
  return report;
48852
49130
  report.flakyTests = detectFlakyTests(history);
@@ -48867,7 +49145,7 @@ function analyzeFailures(workingDir) {
48867
49145
  } catch {}
48868
49146
  return report;
48869
49147
  }
48870
- 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;
48871
49149
  var init_test_runner = __esm(() => {
48872
49150
  init_zod();
48873
49151
  init_discovery();
@@ -49313,7 +49591,7 @@ var init_test_runner = __esm(() => {
49313
49591
  return JSON.stringify(errorResult, null, 2);
49314
49592
  }
49315
49593
  const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
49316
- recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
49594
+ recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined, result.testCases);
49317
49595
  let historyReport;
49318
49596
  if (!result.success && result.totals && result.totals.failed > 0) {
49319
49597
  historyReport = analyzeFailures(workingDir);
@@ -8,6 +8,18 @@ export interface HivePromotionSummary {
8
8
  advancements: number;
9
9
  total_hive_entries: number;
10
10
  }
11
+ /**
12
+ * Check whether a swarm knowledge entry is eligible for hive promotion.
13
+ * Three routes to eligibility:
14
+ * Route 1: hive_eligible flag + 3+ distinct phases
15
+ * Route 2: 'hive-fast-track' tag
16
+ * Route 3: age exceeds auto_promote_days threshold
17
+ *
18
+ * @param entry - The swarm knowledge entry to check
19
+ * @param autoPromoteDays - Number of days before age-based promotion kicks in
20
+ * @returns true if the entry is eligible for hive promotion
21
+ */
22
+ export declare function isHiveEligible(entry: SwarmKnowledgeEntry, autoPromoteDays: number): boolean;
11
23
  /**
12
24
  * Main promotion logic: checks swarm entries and promotes eligible ones to hive.
13
25
  * Also updates existing hive entries with new project confirmations.