opencode-swarm 6.67.1 → 6.68.1

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.
Files changed (71) hide show
  1. package/dist/cli/index.js +1201 -167
  2. package/dist/commands/brainstorm.d.ts +13 -0
  3. package/dist/commands/brainstorm.test.d.ts +1 -0
  4. package/dist/commands/index.d.ts +2 -0
  5. package/dist/commands/qa-gates.d.ts +15 -0
  6. package/dist/commands/qa-gates.test.d.ts +1 -0
  7. package/dist/commands/registry.d.ts +12 -0
  8. package/dist/db/global-db.d.ts +22 -0
  9. package/dist/db/global-db.test.d.ts +7 -0
  10. package/dist/db/index.d.ts +13 -0
  11. package/dist/db/project-db.d.ts +40 -0
  12. package/dist/db/project-db.test.d.ts +4 -0
  13. package/dist/db/qa-gate-profile.d.ts +89 -0
  14. package/dist/db/qa-gate-profile.test.d.ts +4 -0
  15. package/dist/diff/__tests__/semantic-classifier.test.d.ts +1 -0
  16. package/dist/diff/__tests__/summary-generator.test.d.ts +1 -0
  17. package/dist/diff/semantic-classifier.d.ts +55 -0
  18. package/dist/diff/summary-generator.d.ts +33 -0
  19. package/dist/index.js +3070 -919
  20. package/dist/mutation/__tests__/engine.adversarial.test.d.ts +1 -0
  21. package/dist/mutation/__tests__/engine.test.d.ts +1 -0
  22. package/dist/mutation/__tests__/equivalence.adversarial.test.d.ts +1 -0
  23. package/dist/mutation/__tests__/equivalence.test.d.ts +1 -0
  24. package/dist/mutation/__tests__/gate.adversarial.test.d.ts +1 -0
  25. package/dist/mutation/__tests__/gate.test.d.ts +1 -0
  26. package/dist/mutation/engine.d.ts +47 -0
  27. package/dist/mutation/equivalence.d.ts +35 -0
  28. package/dist/mutation/gate.d.ts +28 -0
  29. package/dist/state.d.ts +7 -0
  30. package/dist/test-impact/__tests__/analyzer-import-fix.adversarial.test.d.ts +1 -0
  31. package/dist/test-impact/__tests__/analyzer-import-fix.test.d.ts +1 -0
  32. package/dist/test-impact/__tests__/analyzer.adversarial.test.d.ts +1 -0
  33. package/dist/test-impact/__tests__/analyzer.test.d.ts +1 -0
  34. package/dist/test-impact/__tests__/council-fixes.test.d.ts +1 -0
  35. package/dist/test-impact/__tests__/failure-classifier.adversarial.test.d.ts +1 -0
  36. package/dist/test-impact/__tests__/failure-classifier.test.d.ts +1 -0
  37. package/dist/test-impact/__tests__/flaky-detector.adversarial.test.d.ts +1 -0
  38. package/dist/test-impact/__tests__/flaky-detector.test.d.ts +1 -0
  39. package/dist/test-impact/__tests__/history-store.adversarial.test.d.ts +1 -0
  40. package/dist/test-impact/__tests__/history-store.test.d.ts +1 -0
  41. package/dist/test-impact/__tests__/test-impact.adversarial.test.d.ts +1 -0
  42. package/dist/test-impact/__tests__/test-impact.test.d.ts +1 -0
  43. package/dist/test-impact/analyzer.d.ts +9 -0
  44. package/dist/test-impact/failure-classifier.d.ts +26 -0
  45. package/dist/test-impact/flaky-detector.d.ts +14 -0
  46. package/dist/test-impact/history-store.d.ts +15 -0
  47. package/dist/tools/__tests__/barrel-exports.test.d.ts +1 -0
  48. package/dist/tools/__tests__/diff-ast-fallback.test.d.ts +1 -0
  49. package/dist/tools/__tests__/diff-markdown-summary.test.d.ts +1 -0
  50. package/dist/tools/__tests__/diff-semantic.test.d.ts +1 -0
  51. package/dist/tools/__tests__/diff-summary.adversarial.test.d.ts +1 -0
  52. package/dist/tools/__tests__/diff-summary.test.d.ts +1 -0
  53. package/dist/tools/__tests__/mutation-test.adversarial.test.d.ts +1 -0
  54. package/dist/tools/__tests__/mutation-test.sourcefiles.test.d.ts +1 -0
  55. package/dist/tools/__tests__/mutation-test.test.d.ts +1 -0
  56. package/dist/tools/__tests__/test-runner-history.test.d.ts +1 -0
  57. package/dist/tools/__tests__/test-runner-impact.adversarial.test.d.ts +1 -0
  58. package/dist/tools/__tests__/test-runner-impact.test.d.ts +1 -0
  59. package/dist/tools/__tests__/test-runner-source-files.test.d.ts +1 -0
  60. package/dist/tools/diff-summary.d.ts +12 -0
  61. package/dist/tools/diff.d.ts +3 -0
  62. package/dist/tools/get-approved-plan.d.ts +4 -0
  63. package/dist/tools/get-qa-gate-profile.d.ts +27 -0
  64. package/dist/tools/index.d.ts +9 -0
  65. package/dist/tools/mutation-test.d.ts +2 -0
  66. package/dist/tools/mutation-test.security.test.d.ts +1 -0
  67. package/dist/tools/set-qa-gates.d.ts +37 -0
  68. package/dist/tools/test-impact.d.ts +2 -0
  69. package/dist/tools/test-runner.d.ts +4 -4
  70. package/dist/tools/tool-names.d.ts +1 -1
  71. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -18314,9 +18314,9 @@ var init_evidence_summary_service = __esm(() => {
18314
18314
  });
18315
18315
 
18316
18316
  // src/cli/index.ts
18317
- import * as fs19 from "fs";
18317
+ import * as fs21 from "fs";
18318
18318
  import * as os6 from "os";
18319
- import * as path29 from "path";
18319
+ import * as path31 from "path";
18320
18320
 
18321
18321
  // src/commands/acknowledge-spec-drift.ts
18322
18322
  init_utils2();
@@ -18461,6 +18461,7 @@ init_zod();
18461
18461
  // src/tools/tool-names.ts
18462
18462
  var TOOL_NAMES = [
18463
18463
  "diff",
18464
+ "diff_summary",
18464
18465
  "syntax_check",
18465
18466
  "placeholder_scan",
18466
18467
  "imports",
@@ -18483,6 +18484,8 @@ var TOOL_NAMES = [
18483
18484
  "checkpoint",
18484
18485
  "pkg_audit",
18485
18486
  "test_runner",
18487
+ "test_impact",
18488
+ "mutation_test",
18486
18489
  "detect_domains",
18487
18490
  "gitingest",
18488
18491
  "retrieve_summary",
@@ -18507,7 +18510,9 @@ var TOOL_NAMES = [
18507
18510
  "suggest_patch",
18508
18511
  "req_coverage",
18509
18512
  "get_approved_plan",
18510
- "repo_map"
18513
+ "repo_map",
18514
+ "get_qa_gate_profile",
18515
+ "set_qa_gates"
18511
18516
  ];
18512
18517
  var TOOL_NAME_SET = new Set(TOOL_NAMES);
18513
18518
 
@@ -18546,6 +18551,7 @@ var AGENT_TOOL_MAP = {
18546
18551
  "knowledge_query",
18547
18552
  "lint",
18548
18553
  "diff",
18554
+ "diff_summary",
18549
18555
  "pkg_audit",
18550
18556
  "pre_check_batch",
18551
18557
  "quality_budget",
@@ -18557,6 +18563,8 @@ var AGENT_TOOL_MAP = {
18557
18563
  "secretscan",
18558
18564
  "symbols",
18559
18565
  "test_runner",
18566
+ "test_impact",
18567
+ "mutation_test",
18560
18568
  "todo_extract",
18561
18569
  "update_task_status",
18562
18570
  "lint_spec",
@@ -18577,7 +18585,9 @@ var AGENT_TOOL_MAP = {
18577
18585
  "knowledge_remove",
18578
18586
  "co_change_analyzer",
18579
18587
  "suggest_patch",
18580
- "repo_map"
18588
+ "repo_map",
18589
+ "get_qa_gate_profile",
18590
+ "set_qa_gates"
18581
18591
  ],
18582
18592
  explorer: [
18583
18593
  "complexity_hotspots",
@@ -18611,6 +18621,8 @@ var AGENT_TOOL_MAP = {
18611
18621
  ],
18612
18622
  test_engineer: [
18613
18623
  "test_runner",
18624
+ "test_impact",
18625
+ "mutation_test",
18614
18626
  "diff",
18615
18627
  "symbols",
18616
18628
  "extract_code_blocks",
@@ -18634,6 +18646,7 @@ var AGENT_TOOL_MAP = {
18634
18646
  ],
18635
18647
  reviewer: [
18636
18648
  "diff",
18649
+ "diff_summary",
18637
18650
  "imports",
18638
18651
  "lint",
18639
18652
  "pkg_audit",
@@ -18644,6 +18657,7 @@ var AGENT_TOOL_MAP = {
18644
18657
  "retrieve_summary",
18645
18658
  "extract_code_blocks",
18646
18659
  "test_runner",
18660
+ "test_impact",
18647
18661
  "sast_scan",
18648
18662
  "placeholder_scan",
18649
18663
  "knowledge_recall",
@@ -19818,6 +19832,24 @@ async function handleBenchmarkCommand(directory, args) {
19818
19832
  `);
19819
19833
  }
19820
19834
 
19835
+ // src/commands/brainstorm.ts
19836
+ function sanitizeTopic(raw) {
19837
+ const collapsed = raw.replace(/\s+/g, " ").trim();
19838
+ const stripped = collapsed.replace(/\[\s*MODE\s*:[^\]]*\]/gi, "");
19839
+ const normalized = stripped.replace(/\s+/g, " ").trim();
19840
+ const MAX_TOPIC_LEN = 2000;
19841
+ if (normalized.length <= MAX_TOPIC_LEN)
19842
+ return normalized;
19843
+ return `${normalized.slice(0, MAX_TOPIC_LEN)}\u2026`;
19844
+ }
19845
+ async function handleBrainstormCommand(_directory, args) {
19846
+ const description = sanitizeTopic(args.join(" "));
19847
+ if (description) {
19848
+ return `[MODE: BRAINSTORM] ${description}`;
19849
+ }
19850
+ return "[MODE: BRAINSTORM] Please enter MODE: BRAINSTORM and begin the structured brainstorm workflow (CONTEXT SCAN \u2192 DIALOGUE \u2192 APPROACHES \u2192 DESIGN SECTIONS \u2192 SPEC WRITE + SELF-REVIEW \u2192 QA GATE SELECTION \u2192 TRANSITION).";
19851
+ }
19852
+
19821
19853
  // src/commands/checkpoint.ts
19822
19854
  init_zod();
19823
19855
 
@@ -38449,8 +38481,8 @@ async function handlePlanCommand(directory, args) {
38449
38481
  // src/services/preflight-service.ts
38450
38482
  init_manager2();
38451
38483
  init_manager();
38452
- import * as fs14 from "fs";
38453
- import * as path24 from "path";
38484
+ import * as fs16 from "fs";
38485
+ import * as path26 from "path";
38454
38486
 
38455
38487
  // src/tools/lint.ts
38456
38488
  import * as fs10 from "fs";
@@ -39585,12 +39617,577 @@ async function runSecretscan(directory) {
39585
39617
  }
39586
39618
 
39587
39619
  // src/tools/test-runner.ts
39588
- import * as fs13 from "fs";
39589
- import * as path23 from "path";
39620
+ import * as fs15 from "fs";
39621
+ import * as path25 from "path";
39622
+
39623
+ // src/test-impact/analyzer.ts
39624
+ import fs12 from "fs";
39625
+ import path22 from "path";
39626
+ var IMPORT_REGEX_ES = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
39627
+ var IMPORT_REGEX_REQUIRE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
39628
+ var IMPORT_REGEX_REEXPORT = /export\s+(?:\{[^}]*\}|\*)\s+from\s+['"]([^'"]+)['"]/g;
39629
+ var EXTENSIONS_TO_TRY = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
39630
+ function normalizePath(p) {
39631
+ return p.replace(/\\/g, "/");
39632
+ }
39633
+ function isCacheStale(impactMap, generatedAtMs) {
39634
+ for (const sourcePath of Object.keys(impactMap)) {
39635
+ try {
39636
+ const stat2 = fs12.statSync(sourcePath);
39637
+ if (stat2.mtimeMs > generatedAtMs) {
39638
+ return true;
39639
+ }
39640
+ } catch {
39641
+ return true;
39642
+ }
39643
+ }
39644
+ return false;
39645
+ }
39646
+ function resolveRelativeImport(fromDir, importPath) {
39647
+ if (!importPath.startsWith(".")) {
39648
+ return null;
39649
+ }
39650
+ const resolved = path22.resolve(fromDir, importPath);
39651
+ if (path22.extname(resolved)) {
39652
+ if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
39653
+ return normalizePath(resolved);
39654
+ }
39655
+ } else {
39656
+ for (const ext of EXTENSIONS_TO_TRY) {
39657
+ const withExt = resolved + ext;
39658
+ if (fs12.existsSync(withExt) && fs12.statSync(withExt).isFile()) {
39659
+ return normalizePath(withExt);
39660
+ }
39661
+ }
39662
+ }
39663
+ return null;
39664
+ }
39665
+ function findTestFilesSync(cwd) {
39666
+ const testFiles = [];
39667
+ const skipDirs = new Set([
39668
+ "node_modules",
39669
+ "dist",
39670
+ ".git",
39671
+ ".swarm",
39672
+ ".cache"
39673
+ ]);
39674
+ function walk(dir, visitedInodes) {
39675
+ let entries;
39676
+ try {
39677
+ entries = fs12.readdirSync(dir, { withFileTypes: true });
39678
+ } catch {
39679
+ return;
39680
+ }
39681
+ let dirInode;
39682
+ try {
39683
+ dirInode = fs12.statSync(dir).ino;
39684
+ } catch {
39685
+ return;
39686
+ }
39687
+ if (dirInode !== 0) {
39688
+ if (visitedInodes.has(dirInode)) {
39689
+ return;
39690
+ }
39691
+ visitedInodes.add(dirInode);
39692
+ }
39693
+ for (const entry of entries) {
39694
+ if (entry.isDirectory()) {
39695
+ if (!skipDirs.has(entry.name)) {
39696
+ walk(path22.join(dir, entry.name), visitedInodes);
39697
+ }
39698
+ } else if (entry.isFile()) {
39699
+ const name = entry.name;
39700
+ if (/\.(test|spec)\.(ts|tsx|js|jsx)$/.test(name) || dir.includes("__tests__") && /\.(ts|tsx|js|jsx)$/.test(name)) {
39701
+ testFiles.push(normalizePath(path22.join(dir, entry.name)));
39702
+ }
39703
+ }
39704
+ }
39705
+ }
39706
+ walk(cwd, new Set);
39707
+ return [...new Set(testFiles)];
39708
+ }
39709
+ function extractImports(content) {
39710
+ function execRegex(regex, content2) {
39711
+ const results = [];
39712
+ regex.lastIndex = 0;
39713
+ let match;
39714
+ while ((match = regex.exec(content2)) !== null) {
39715
+ results.push(match[1]);
39716
+ }
39717
+ return results;
39718
+ }
39719
+ return [
39720
+ ...execRegex(IMPORT_REGEX_ES, content),
39721
+ ...execRegex(IMPORT_REGEX_REQUIRE, content),
39722
+ ...execRegex(IMPORT_REGEX_REEXPORT, content)
39723
+ ];
39724
+ }
39725
+ async function buildImpactMapInternal(cwd) {
39726
+ const testFiles = findTestFilesSync(cwd);
39727
+ const impactMap = {};
39728
+ for (const testFile of testFiles) {
39729
+ let content;
39730
+ try {
39731
+ content = fs12.readFileSync(testFile, "utf-8");
39732
+ } catch {
39733
+ continue;
39734
+ }
39735
+ if (content.substring(0, 8192).includes("\x00")) {
39736
+ continue;
39737
+ }
39738
+ const imports = extractImports(content);
39739
+ const testDir = path22.dirname(testFile);
39740
+ for (const importPath of imports) {
39741
+ const resolvedSource = resolveRelativeImport(testDir, importPath);
39742
+ if (resolvedSource === null) {
39743
+ continue;
39744
+ }
39745
+ if (!impactMap[resolvedSource]) {
39746
+ impactMap[resolvedSource] = [];
39747
+ }
39748
+ if (!impactMap[resolvedSource].includes(testFile)) {
39749
+ impactMap[resolvedSource].push(testFile);
39750
+ }
39751
+ }
39752
+ }
39753
+ return impactMap;
39754
+ }
39755
+ async function buildImpactMap(cwd) {
39756
+ const impactMap = await buildImpactMapInternal(cwd);
39757
+ await saveImpactMap(cwd, impactMap);
39758
+ return impactMap;
39759
+ }
39760
+ async function loadImpactMap(cwd) {
39761
+ const cachePath = path22.join(cwd, ".swarm", "cache", "impact-map.json");
39762
+ if (fs12.existsSync(cachePath)) {
39763
+ try {
39764
+ const content = fs12.readFileSync(cachePath, "utf-8");
39765
+ const data = JSON.parse(content);
39766
+ const map3 = data.map;
39767
+ const generatedAt = new Date(data.generatedAt).getTime();
39768
+ if (!isCacheStale(map3, generatedAt)) {
39769
+ return map3;
39770
+ }
39771
+ } catch {}
39772
+ }
39773
+ return buildImpactMap(cwd);
39774
+ }
39775
+ async function saveImpactMap(cwd, impactMap) {
39776
+ const cacheDir = path22.join(cwd, ".swarm", "cache");
39777
+ const cachePath = path22.join(cacheDir, "impact-map.json");
39778
+ if (!fs12.existsSync(cacheDir)) {
39779
+ fs12.mkdirSync(cacheDir, { recursive: true });
39780
+ }
39781
+ const data = {
39782
+ generatedAt: new Date().toISOString(),
39783
+ fileCount: Object.keys(impactMap).length,
39784
+ map: impactMap
39785
+ };
39786
+ fs12.writeFileSync(cachePath, JSON.stringify(data, null, 2), "utf-8");
39787
+ }
39788
+ async function analyzeImpact(changedFiles, cwd) {
39789
+ if (!Array.isArray(changedFiles)) {
39790
+ const emptyMap = {};
39791
+ return {
39792
+ impactedTests: [],
39793
+ unrelatedTests: [],
39794
+ untestedFiles: [],
39795
+ impactMap: emptyMap
39796
+ };
39797
+ }
39798
+ const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !f.includes("\x00"));
39799
+ const impactMap = await loadImpactMap(cwd);
39800
+ const impactedTestsSet = new Set;
39801
+ const untestedFiles = [];
39802
+ for (const changedFile of validFiles) {
39803
+ const normalizedChanged = normalizePath(path22.resolve(changedFile));
39804
+ const tests = impactMap[normalizedChanged];
39805
+ if (tests && tests.length > 0) {
39806
+ for (const test of tests) {
39807
+ impactedTestsSet.add(test);
39808
+ }
39809
+ } else {
39810
+ let found = false;
39811
+ for (const [sourcePath, tests2] of Object.entries(impactMap)) {
39812
+ if (sourcePath.endsWith(changedFile) || changedFile.endsWith(sourcePath)) {
39813
+ for (const test of tests2) {
39814
+ impactedTestsSet.add(test);
39815
+ }
39816
+ found = true;
39817
+ break;
39818
+ }
39819
+ }
39820
+ if (!found) {
39821
+ untestedFiles.push(changedFile);
39822
+ }
39823
+ }
39824
+ }
39825
+ const impactedTests = [...impactedTestsSet];
39826
+ const allTestFiles = new Set;
39827
+ for (const tests of Object.values(impactMap)) {
39828
+ for (const test of tests) {
39829
+ allTestFiles.add(test);
39830
+ }
39831
+ }
39832
+ const unrelatedTests = [...allTestFiles].filter((t) => !impactedTestsSet.has(t));
39833
+ return {
39834
+ impactedTests,
39835
+ unrelatedTests,
39836
+ untestedFiles,
39837
+ impactMap
39838
+ };
39839
+ }
39840
+
39841
+ // src/test-impact/failure-classifier.ts
39842
+ function computeConfidence2(historyLength) {
39843
+ if (historyLength >= 5) {
39844
+ return 1;
39845
+ }
39846
+ if (historyLength >= 3) {
39847
+ return 0.5;
39848
+ }
39849
+ if (historyLength >= 1) {
39850
+ return 0.3;
39851
+ }
39852
+ return 0.1;
39853
+ }
39854
+ function countAlternations(results) {
39855
+ if (results.length < 2) {
39856
+ return 0;
39857
+ }
39858
+ let alternations = 0;
39859
+ for (let i = 1;i < results.length; i++) {
39860
+ if (results[i] !== results[i - 1]) {
39861
+ alternations++;
39862
+ }
39863
+ }
39864
+ return alternations;
39865
+ }
39866
+ function stringHash(str) {
39867
+ let h1 = 3735928559;
39868
+ let h2 = 1103547991;
39869
+ for (let i = 0;i < str.length; i++) {
39870
+ const ch = str.charCodeAt(i);
39871
+ h1 = Math.imul(h1 ^ ch, 2654435761);
39872
+ h2 = Math.imul(h2 ^ ch, 1597334677);
39873
+ }
39874
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507);
39875
+ h1 ^= Math.imul(h2 ^ h2 >>> 13, 3266489909);
39876
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507);
39877
+ h2 ^= Math.imul(h1 ^ h1 >>> 13, 3266489909);
39878
+ return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16);
39879
+ }
39880
+ function classifyFailure(currentResult, history) {
39881
+ const normalizedFile = currentResult.testFile.toLowerCase();
39882
+ const normalizedName = currentResult.testName.toLowerCase();
39883
+ const testHistory = history.filter((r) => r.testFile.toLowerCase() === normalizedFile && r.testName.toLowerCase() === normalizedName).sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
39884
+ const lastThree = testHistory.slice(0, 3);
39885
+ const lastTen = testHistory.slice(0, 10);
39886
+ const normalizedTestFile = currentResult.testFile.toLowerCase();
39887
+ const isInChangedFiles = currentResult.changedFiles.some((f) => f.toLowerCase() === normalizedTestFile);
39888
+ const hasRecentPass = lastThree.every((r) => r.result === "pass");
39889
+ const hasRecentFailure = lastThree.some((r) => r.result === "fail");
39890
+ const alternationCount = countAlternations(lastTen.map((r) => r.result));
39891
+ if (lastThree.length >= 3 && hasRecentPass && currentResult.result === "fail" && isInChangedFiles) {
39892
+ return {
39893
+ testFile: currentResult.testFile,
39894
+ testName: currentResult.testName,
39895
+ classification: "new_regression",
39896
+ errorMessage: currentResult.errorMessage,
39897
+ stackPrefix: currentResult.stackPrefix,
39898
+ durationMs: currentResult.durationMs,
39899
+ confidence: computeConfidence2(testHistory.length)
39900
+ };
39901
+ }
39902
+ if (lastThree.length > 0 && hasRecentFailure && !isInChangedFiles) {
39903
+ return {
39904
+ testFile: currentResult.testFile,
39905
+ testName: currentResult.testName,
39906
+ classification: "pre_existing",
39907
+ errorMessage: currentResult.errorMessage,
39908
+ stackPrefix: currentResult.stackPrefix,
39909
+ durationMs: currentResult.durationMs,
39910
+ confidence: computeConfidence2(testHistory.length)
39911
+ };
39912
+ }
39913
+ if (alternationCount >= 2) {
39914
+ return {
39915
+ testFile: currentResult.testFile,
39916
+ testName: currentResult.testName,
39917
+ classification: "flaky",
39918
+ errorMessage: currentResult.errorMessage,
39919
+ stackPrefix: currentResult.stackPrefix,
39920
+ durationMs: currentResult.durationMs,
39921
+ confidence: computeConfidence2(testHistory.length)
39922
+ };
39923
+ }
39924
+ return {
39925
+ testFile: currentResult.testFile,
39926
+ testName: currentResult.testName,
39927
+ classification: "unknown",
39928
+ errorMessage: currentResult.errorMessage,
39929
+ stackPrefix: currentResult.stackPrefix,
39930
+ durationMs: currentResult.durationMs,
39931
+ confidence: computeConfidence2(testHistory.length)
39932
+ };
39933
+ }
39934
+ function clusterFailures(failures) {
39935
+ const clusterMap = new Map;
39936
+ for (const failure of failures) {
39937
+ const key = (failure.stackPrefix || "") + (failure.errorMessage || "");
39938
+ if (!clusterMap.has(key)) {
39939
+ clusterMap.set(key, {
39940
+ failures: [],
39941
+ stackPrefix: failure.stackPrefix,
39942
+ errorMessage: failure.errorMessage
39943
+ });
39944
+ }
39945
+ clusterMap.get(key).failures.push(failure);
39946
+ }
39947
+ const clusters = [];
39948
+ for (const [key, data] of clusterMap) {
39949
+ const classificationCounts = new Map;
39950
+ for (const f of data.failures) {
39951
+ const count = classificationCounts.get(f.classification) || 0;
39952
+ classificationCounts.set(f.classification, count + 1);
39953
+ }
39954
+ let dominantClassification = "unknown";
39955
+ let maxCount = 0;
39956
+ for (const [cls, count] of classificationCounts) {
39957
+ if (count > maxCount) {
39958
+ maxCount = count;
39959
+ dominantClassification = cls;
39960
+ }
39961
+ }
39962
+ const affectedTestFiles = [
39963
+ ...new Set(data.failures.map((f) => f.testFile))
39964
+ ];
39965
+ clusters.push({
39966
+ clusterId: stringHash(key),
39967
+ rootCause: key,
39968
+ stackPrefix: data.stackPrefix,
39969
+ errorMessage: data.errorMessage,
39970
+ failures: data.failures,
39971
+ classification: dominantClassification,
39972
+ affectedTestFiles
39973
+ });
39974
+ }
39975
+ return clusters;
39976
+ }
39977
+ function classifyAndCluster(testResults, history) {
39978
+ const failingResults = testResults.filter((r) => r.result === "fail");
39979
+ const classified = failingResults.map((r) => classifyFailure(r, history));
39980
+ const clusters = clusterFailures(classified);
39981
+ return { classified, clusters };
39982
+ }
39983
+
39984
+ // src/test-impact/flaky-detector.ts
39985
+ var FLAKY_THRESHOLD = 0.3;
39986
+ var MIN_RUNS_FOR_QUARANTINE = 5;
39987
+ var MAX_HISTORY_RUNS = 20;
39988
+ function detectFlakyTests(allHistory) {
39989
+ const grouped = new Map;
39990
+ for (const record3 of allHistory) {
39991
+ const key = `${record3.testFile.toLowerCase()}|${record3.testName.toLowerCase()}`;
39992
+ if (!grouped.has(key)) {
39993
+ grouped.set(key, {
39994
+ records: [],
39995
+ originalFile: record3.testFile,
39996
+ originalName: record3.testName
39997
+ });
39998
+ }
39999
+ grouped.get(key).records.push(record3);
40000
+ }
40001
+ const results = [];
40002
+ for (const [_key, entry] of grouped) {
40003
+ const records = entry.records;
40004
+ if (records.length === 0) {
40005
+ continue;
40006
+ }
40007
+ const sorted = records.slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
40008
+ const recent = sorted.slice(-MAX_HISTORY_RUNS);
40009
+ const totalRuns = recent.length;
40010
+ if (totalRuns < 2) {
40011
+ continue;
40012
+ }
40013
+ let alternationCount = 0;
40014
+ for (let i = 1;i < recent.length; i++) {
40015
+ if (recent[i].result !== recent[i - 1].result) {
40016
+ alternationCount++;
40017
+ }
40018
+ }
40019
+ const flakyScore = totalRuns >= 2 ? alternationCount / totalRuns : 0;
40020
+ const isQuarantined = flakyScore > FLAKY_THRESHOLD && totalRuns >= MIN_RUNS_FOR_QUARANTINE;
40021
+ const recentResults = recent.map((r) => r.result);
40022
+ const testFile = entry.originalFile;
40023
+ const testName = entry.originalName;
40024
+ let recommendation;
40025
+ if (isQuarantined) {
40026
+ if (alternationCount === totalRuns - 1) {
40027
+ recommendation = "Highly unstable \u2014 investigate test isolation issues, mock cleanup, or shared state";
40028
+ } else if (flakyScore > 0.5) {
40029
+ recommendation = "Severely flaky \u2014 consider quarantining and rewriting test with proper isolation";
40030
+ } else if (flakyScore > FLAKY_THRESHOLD) {
40031
+ recommendation = "Moderately flaky \u2014 review for timing dependencies, async issues, or environmental factors";
40032
+ }
40033
+ }
40034
+ results.push({
40035
+ testFile,
40036
+ testName,
40037
+ flakyScore,
40038
+ totalRuns,
40039
+ alternationCount,
40040
+ isQuarantined,
40041
+ recentResults,
40042
+ recommendation
40043
+ });
40044
+ }
40045
+ return results;
40046
+ }
40047
+
40048
+ // src/test-impact/history-store.ts
40049
+ import fs13 from "fs";
40050
+ import path23 from "path";
40051
+ var MAX_HISTORY_PER_TEST = 20;
40052
+ var MAX_ERROR_LENGTH = 500;
40053
+ var MAX_STACK_LENGTH = 200;
40054
+ var MAX_CHANGED_FILES = 50;
40055
+ function getHistoryPath(workingDir) {
40056
+ return path23.join(workingDir || process.cwd(), ".swarm", "cache", "test-history.jsonl");
40057
+ }
40058
+ function sanitizeErrorMessage(errorMessage) {
40059
+ if (errorMessage === undefined) {
40060
+ return;
40061
+ }
40062
+ if (errorMessage.length > MAX_ERROR_LENGTH) {
40063
+ return errorMessage.substring(0, MAX_ERROR_LENGTH);
40064
+ }
40065
+ return errorMessage;
40066
+ }
40067
+ function sanitizeStackPrefix(stackPrefix) {
40068
+ if (stackPrefix === undefined) {
40069
+ return;
40070
+ }
40071
+ if (stackPrefix.length > MAX_STACK_LENGTH) {
40072
+ return stackPrefix.substring(0, MAX_STACK_LENGTH);
40073
+ }
40074
+ return stackPrefix;
40075
+ }
40076
+ var DANGEROUS_PROPERTY_NAMES = new Set([
40077
+ "__proto__",
40078
+ "constructor",
40079
+ "prototype"
40080
+ ]);
40081
+ function sanitizeChangedFiles(changedFiles) {
40082
+ const validFiles = changedFiles.filter((f) => typeof f === "string" && f.length > 0 && !DANGEROUS_PROPERTY_NAMES.has(f));
40083
+ return validFiles.slice(0, MAX_CHANGED_FILES);
40084
+ }
40085
+ function appendTestRun(record3, workingDir) {
40086
+ if (typeof record3.testFile !== "string" || record3.testFile.length === 0) {
40087
+ throw new TypeError("testFile must be a non-empty string");
40088
+ }
40089
+ if (typeof record3.testName !== "string" || record3.testName.length === 0) {
40090
+ throw new TypeError("testName must be a non-empty string");
40091
+ }
40092
+ if (typeof record3.taskId !== "string" || record3.taskId.length === 0) {
40093
+ throw new TypeError("taskId must be a non-empty string");
40094
+ }
40095
+ if (record3.result !== "pass" && record3.result !== "fail" && record3.result !== "skip") {
40096
+ throw new TypeError('result must be "pass", "fail", or "skip"');
40097
+ }
40098
+ if (typeof record3.durationMs !== "number" || !Number.isFinite(record3.durationMs)) {
40099
+ throw new TypeError("durationMs must be a finite number");
40100
+ }
40101
+ if (record3.timestamp !== undefined && Number.isNaN(Date.parse(record3.timestamp))) {
40102
+ throw new TypeError("timestamp must be a valid ISO 8601 string");
40103
+ }
40104
+ if (record3.changedFiles !== undefined && !Array.isArray(record3.changedFiles)) {
40105
+ throw new TypeError("changedFiles must be an array");
40106
+ }
40107
+ const sanitizedRecord = {
40108
+ ...record3,
40109
+ timestamp: record3.timestamp || new Date().toISOString(),
40110
+ durationMs: Math.max(0, record3.durationMs),
40111
+ errorMessage: sanitizeErrorMessage(record3.errorMessage),
40112
+ stackPrefix: sanitizeStackPrefix(record3.stackPrefix),
40113
+ changedFiles: sanitizeChangedFiles(record3.changedFiles || [])
40114
+ };
40115
+ const historyPath = getHistoryPath(workingDir);
40116
+ const historyDir = path23.dirname(historyPath);
40117
+ if (!fs13.existsSync(historyDir)) {
40118
+ fs13.mkdirSync(historyDir, { recursive: true });
40119
+ }
40120
+ const existingRecords = readAllRecords(historyPath);
40121
+ existingRecords.push(sanitizedRecord);
40122
+ const recordsByFile = new Map;
40123
+ for (const rec of existingRecords) {
40124
+ const normalizedFile = rec.testFile.toLowerCase();
40125
+ if (!recordsByFile.has(normalizedFile)) {
40126
+ recordsByFile.set(normalizedFile, []);
40127
+ }
40128
+ recordsByFile.get(normalizedFile).push(rec);
40129
+ }
40130
+ const prunedRecords = [];
40131
+ for (const [, records] of recordsByFile) {
40132
+ records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
40133
+ const toKeep = records.slice(-MAX_HISTORY_PER_TEST);
40134
+ prunedRecords.push(...toKeep);
40135
+ }
40136
+ prunedRecords.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
40137
+ try {
40138
+ const lines = prunedRecords.map((rec) => JSON.stringify(rec));
40139
+ const content = lines.join(`
40140
+ `) + `
40141
+ `;
40142
+ const tempPath = historyPath + ".tmp";
40143
+ fs13.writeFileSync(tempPath, content, "utf-8");
40144
+ fs13.renameSync(tempPath, historyPath);
40145
+ } catch (err) {
40146
+ try {
40147
+ const tempPath = historyPath + ".tmp";
40148
+ if (fs13.existsSync(tempPath)) {
40149
+ fs13.unlinkSync(tempPath);
40150
+ }
40151
+ } catch {}
40152
+ throw new Error(`Failed to write test history: ${err instanceof Error ? err.message : String(err)}`);
40153
+ }
40154
+ }
40155
+ function readAllRecords(historyPath) {
40156
+ if (!fs13.existsSync(historyPath)) {
40157
+ return [];
40158
+ }
40159
+ try {
40160
+ const content = fs13.readFileSync(historyPath, "utf-8");
40161
+ const lines = content.split(`
40162
+ `);
40163
+ const records = [];
40164
+ for (const line of lines) {
40165
+ const trimmed = line.trim();
40166
+ if (trimmed.length === 0) {
40167
+ continue;
40168
+ }
40169
+ try {
40170
+ const parsed = JSON.parse(trimmed);
40171
+ if (typeof parsed === "object" && parsed !== null && "testFile" in parsed && "testName" in parsed && "result" in parsed) {
40172
+ records.push(parsed);
40173
+ }
40174
+ } catch {}
40175
+ }
40176
+ return records;
40177
+ } catch (err) {
40178
+ throw new Error(`Failed to read test history: ${err instanceof Error ? err.message : String(err)}`);
40179
+ }
40180
+ }
40181
+ function getAllHistory(workingDir) {
40182
+ const historyPath = getHistoryPath(workingDir);
40183
+ const records = readAllRecords(historyPath);
40184
+ records.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
40185
+ return records;
40186
+ }
39590
40187
 
39591
40188
  // src/tools/resolve-working-directory.ts
39592
- import * as fs12 from "fs";
39593
- import * as path22 from "path";
40189
+ import * as fs14 from "fs";
40190
+ import * as path24 from "path";
39594
40191
  function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
39595
40192
  if (workingDirectory == null || workingDirectory === "") {
39596
40193
  return { success: true, directory: fallbackDirectory };
@@ -39610,17 +40207,17 @@ function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
39610
40207
  };
39611
40208
  }
39612
40209
  }
39613
- const normalizedDir = path22.normalize(workingDirectory);
39614
- const pathParts = normalizedDir.split(path22.sep);
40210
+ const normalizedDir = path24.normalize(workingDirectory);
40211
+ const pathParts = normalizedDir.split(path24.sep);
39615
40212
  if (pathParts.includes("..")) {
39616
40213
  return {
39617
40214
  success: false,
39618
40215
  message: "Invalid working_directory: path traversal sequences (..) are not allowed"
39619
40216
  };
39620
40217
  }
39621
- const resolvedDir = path22.resolve(normalizedDir);
40218
+ const resolvedDir = path24.resolve(normalizedDir);
39622
40219
  try {
39623
- const realPath = fs12.realpathSync(resolvedDir);
40220
+ const realPath = fs14.realpathSync(resolvedDir);
39624
40221
  return { success: true, directory: realPath };
39625
40222
  } catch {
39626
40223
  return {
@@ -39656,7 +40253,7 @@ function validateArgs2(args) {
39656
40253
  return false;
39657
40254
  const obj = args;
39658
40255
  if (obj.scope !== undefined) {
39659
- if (obj.scope !== "all" && obj.scope !== "convention" && obj.scope !== "graph") {
40256
+ if (obj.scope !== "all" && obj.scope !== "convention" && obj.scope !== "graph" && obj.scope !== "impact") {
39660
40257
  return false;
39661
40258
  }
39662
40259
  }
@@ -39701,19 +40298,19 @@ function hasDevDependency(devDeps, ...patterns) {
39701
40298
  return hasPackageJsonDependency(devDeps, ...patterns);
39702
40299
  }
39703
40300
  function detectGoTest(cwd) {
39704
- return fs13.existsSync(path23.join(cwd, "go.mod")) && isCommandAvailable("go");
40301
+ return fs15.existsSync(path25.join(cwd, "go.mod")) && isCommandAvailable("go");
39705
40302
  }
39706
40303
  function detectJavaMaven(cwd) {
39707
- return fs13.existsSync(path23.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
40304
+ return fs15.existsSync(path25.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
39708
40305
  }
39709
40306
  function detectGradle(cwd) {
39710
- const hasBuildFile = fs13.existsSync(path23.join(cwd, "build.gradle")) || fs13.existsSync(path23.join(cwd, "build.gradle.kts"));
39711
- const hasGradlew = fs13.existsSync(path23.join(cwd, "gradlew")) || fs13.existsSync(path23.join(cwd, "gradlew.bat"));
40307
+ const hasBuildFile = fs15.existsSync(path25.join(cwd, "build.gradle")) || fs15.existsSync(path25.join(cwd, "build.gradle.kts"));
40308
+ const hasGradlew = fs15.existsSync(path25.join(cwd, "gradlew")) || fs15.existsSync(path25.join(cwd, "gradlew.bat"));
39712
40309
  return hasBuildFile && (hasGradlew || isCommandAvailable("gradle"));
39713
40310
  }
39714
40311
  function detectDotnetTest(cwd) {
39715
40312
  try {
39716
- const files = fs13.readdirSync(cwd);
40313
+ const files = fs15.readdirSync(cwd);
39717
40314
  const hasCsproj = files.some((f) => f.endsWith(".csproj"));
39718
40315
  return hasCsproj && isCommandAvailable("dotnet");
39719
40316
  } catch {
@@ -39721,32 +40318,32 @@ function detectDotnetTest(cwd) {
39721
40318
  }
39722
40319
  }
39723
40320
  function detectCTest(cwd) {
39724
- const hasSource = fs13.existsSync(path23.join(cwd, "CMakeLists.txt"));
39725
- const hasBuildCache = fs13.existsSync(path23.join(cwd, "CMakeCache.txt")) || fs13.existsSync(path23.join(cwd, "build", "CMakeCache.txt"));
40321
+ const hasSource = fs15.existsSync(path25.join(cwd, "CMakeLists.txt"));
40322
+ const hasBuildCache = fs15.existsSync(path25.join(cwd, "CMakeCache.txt")) || fs15.existsSync(path25.join(cwd, "build", "CMakeCache.txt"));
39726
40323
  return (hasSource || hasBuildCache) && isCommandAvailable("ctest");
39727
40324
  }
39728
40325
  function detectSwiftTest(cwd) {
39729
- return fs13.existsSync(path23.join(cwd, "Package.swift")) && isCommandAvailable("swift");
40326
+ return fs15.existsSync(path25.join(cwd, "Package.swift")) && isCommandAvailable("swift");
39730
40327
  }
39731
40328
  function detectDartTest(cwd) {
39732
- return fs13.existsSync(path23.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
40329
+ return fs15.existsSync(path25.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
39733
40330
  }
39734
40331
  function detectRSpec(cwd) {
39735
- const hasRSpecFile = fs13.existsSync(path23.join(cwd, ".rspec"));
39736
- const hasGemfile = fs13.existsSync(path23.join(cwd, "Gemfile"));
39737
- const hasSpecDir = fs13.existsSync(path23.join(cwd, "spec"));
40332
+ const hasRSpecFile = fs15.existsSync(path25.join(cwd, ".rspec"));
40333
+ const hasGemfile = fs15.existsSync(path25.join(cwd, "Gemfile"));
40334
+ const hasSpecDir = fs15.existsSync(path25.join(cwd, "spec"));
39738
40335
  const hasRSpec = hasRSpecFile || hasGemfile && hasSpecDir;
39739
40336
  return hasRSpec && (isCommandAvailable("bundle") || isCommandAvailable("rspec"));
39740
40337
  }
39741
40338
  function detectMinitest(cwd) {
39742
- return fs13.existsSync(path23.join(cwd, "test")) && (fs13.existsSync(path23.join(cwd, "Gemfile")) || fs13.existsSync(path23.join(cwd, "Rakefile"))) && isCommandAvailable("ruby");
40339
+ return fs15.existsSync(path25.join(cwd, "test")) && (fs15.existsSync(path25.join(cwd, "Gemfile")) || fs15.existsSync(path25.join(cwd, "Rakefile"))) && isCommandAvailable("ruby");
39743
40340
  }
39744
40341
  async function detectTestFramework(cwd) {
39745
40342
  const baseDir = cwd;
39746
40343
  try {
39747
- const packageJsonPath = path23.join(baseDir, "package.json");
39748
- if (fs13.existsSync(packageJsonPath)) {
39749
- const content = fs13.readFileSync(packageJsonPath, "utf-8");
40344
+ const packageJsonPath = path25.join(baseDir, "package.json");
40345
+ if (fs15.existsSync(packageJsonPath)) {
40346
+ const content = fs15.readFileSync(packageJsonPath, "utf-8");
39750
40347
  const pkg = JSON.parse(content);
39751
40348
  const _deps = pkg.dependencies || {};
39752
40349
  const devDeps = pkg.devDependencies || {};
@@ -39765,38 +40362,38 @@ async function detectTestFramework(cwd) {
39765
40362
  return "jest";
39766
40363
  if (hasDevDependency(devDeps, "mocha", "@types/mocha"))
39767
40364
  return "mocha";
39768
- if (fs13.existsSync(path23.join(baseDir, "bun.lockb")) || fs13.existsSync(path23.join(baseDir, "bun.lock"))) {
40365
+ if (fs15.existsSync(path25.join(baseDir, "bun.lockb")) || fs15.existsSync(path25.join(baseDir, "bun.lock"))) {
39769
40366
  if (scripts.test?.includes("bun"))
39770
40367
  return "bun";
39771
40368
  }
39772
40369
  }
39773
40370
  } catch {}
39774
40371
  try {
39775
- const pyprojectTomlPath = path23.join(baseDir, "pyproject.toml");
39776
- const setupCfgPath = path23.join(baseDir, "setup.cfg");
39777
- const requirementsTxtPath = path23.join(baseDir, "requirements.txt");
39778
- if (fs13.existsSync(pyprojectTomlPath)) {
39779
- const content = fs13.readFileSync(pyprojectTomlPath, "utf-8");
40372
+ const pyprojectTomlPath = path25.join(baseDir, "pyproject.toml");
40373
+ const setupCfgPath = path25.join(baseDir, "setup.cfg");
40374
+ const requirementsTxtPath = path25.join(baseDir, "requirements.txt");
40375
+ if (fs15.existsSync(pyprojectTomlPath)) {
40376
+ const content = fs15.readFileSync(pyprojectTomlPath, "utf-8");
39780
40377
  if (content.includes("[tool.pytest"))
39781
40378
  return "pytest";
39782
40379
  if (content.includes("pytest"))
39783
40380
  return "pytest";
39784
40381
  }
39785
- if (fs13.existsSync(setupCfgPath)) {
39786
- const content = fs13.readFileSync(setupCfgPath, "utf-8");
40382
+ if (fs15.existsSync(setupCfgPath)) {
40383
+ const content = fs15.readFileSync(setupCfgPath, "utf-8");
39787
40384
  if (content.includes("[pytest]"))
39788
40385
  return "pytest";
39789
40386
  }
39790
- if (fs13.existsSync(requirementsTxtPath)) {
39791
- const content = fs13.readFileSync(requirementsTxtPath, "utf-8");
40387
+ if (fs15.existsSync(requirementsTxtPath)) {
40388
+ const content = fs15.readFileSync(requirementsTxtPath, "utf-8");
39792
40389
  if (content.includes("pytest"))
39793
40390
  return "pytest";
39794
40391
  }
39795
40392
  } catch {}
39796
40393
  try {
39797
- const cargoTomlPath = path23.join(baseDir, "Cargo.toml");
39798
- if (fs13.existsSync(cargoTomlPath)) {
39799
- const content = fs13.readFileSync(cargoTomlPath, "utf-8");
40394
+ const cargoTomlPath = path25.join(baseDir, "Cargo.toml");
40395
+ if (fs15.existsSync(cargoTomlPath)) {
40396
+ const content = fs15.readFileSync(cargoTomlPath, "utf-8");
39800
40397
  if (content.includes("[dev-dependencies]")) {
39801
40398
  if (content.includes("tokio") || content.includes("mockall") || content.includes("pretty_assertions")) {
39802
40399
  return "cargo";
@@ -39805,10 +40402,10 @@ async function detectTestFramework(cwd) {
39805
40402
  }
39806
40403
  } catch {}
39807
40404
  try {
39808
- const pesterConfigPath = path23.join(baseDir, "pester.config.ps1");
39809
- const pesterConfigJsonPath = path23.join(baseDir, "pester.config.ps1.json");
39810
- const pesterPs1Path = path23.join(baseDir, "tests.ps1");
39811
- if (fs13.existsSync(pesterConfigPath) || fs13.existsSync(pesterConfigJsonPath) || fs13.existsSync(pesterPs1Path)) {
40405
+ const pesterConfigPath = path25.join(baseDir, "pester.config.ps1");
40406
+ const pesterConfigJsonPath = path25.join(baseDir, "pester.config.ps1.json");
40407
+ const pesterPs1Path = path25.join(baseDir, "tests.ps1");
40408
+ if (fs15.existsSync(pesterConfigPath) || fs15.existsSync(pesterConfigJsonPath) || fs15.existsSync(pesterPs1Path)) {
39812
40409
  return "pester";
39813
40410
  }
39814
40411
  } catch {}
@@ -39859,8 +40456,8 @@ function getTestFilesFromConvention(sourceFiles) {
39859
40456
  const testFiles = [];
39860
40457
  for (const file3 of sourceFiles) {
39861
40458
  const normalizedPath = file3.replace(/\\/g, "/");
39862
- const basename4 = path23.basename(file3);
39863
- const dirname10 = path23.dirname(file3);
40459
+ const basename4 = path25.basename(file3);
40460
+ const dirname10 = path25.dirname(file3);
39864
40461
  if (hasCompoundTestExtension(basename4) || basename4.includes(".spec.") || basename4.includes(".test.") || normalizedPath.includes("/__tests__/") || normalizedPath.includes("/tests/") || normalizedPath.includes("/test/")) {
39865
40462
  if (!testFiles.includes(file3)) {
39866
40463
  testFiles.push(file3);
@@ -39869,16 +40466,16 @@ function getTestFilesFromConvention(sourceFiles) {
39869
40466
  }
39870
40467
  for (const _pattern of TEST_PATTERNS) {
39871
40468
  const nameWithoutExt = basename4.replace(/\.[^.]+$/, "");
39872
- const ext = path23.extname(basename4);
40469
+ const ext = path25.extname(basename4);
39873
40470
  const possibleTestFiles = [
39874
- path23.join(dirname10, `${nameWithoutExt}.spec${ext}`),
39875
- path23.join(dirname10, `${nameWithoutExt}.test${ext}`),
39876
- path23.join(dirname10, "__tests__", `${nameWithoutExt}${ext}`),
39877
- path23.join(dirname10, "tests", `${nameWithoutExt}${ext}`),
39878
- path23.join(dirname10, "test", `${nameWithoutExt}${ext}`)
40471
+ path25.join(dirname10, `${nameWithoutExt}.spec${ext}`),
40472
+ path25.join(dirname10, `${nameWithoutExt}.test${ext}`),
40473
+ path25.join(dirname10, "__tests__", `${nameWithoutExt}${ext}`),
40474
+ path25.join(dirname10, "tests", `${nameWithoutExt}${ext}`),
40475
+ path25.join(dirname10, "test", `${nameWithoutExt}${ext}`)
39879
40476
  ];
39880
40477
  for (const testFile of possibleTestFiles) {
39881
- if (fs13.existsSync(testFile) && !testFiles.includes(testFile)) {
40478
+ if (fs15.existsSync(testFile) && !testFiles.includes(testFile)) {
39882
40479
  testFiles.push(testFile);
39883
40480
  }
39884
40481
  }
@@ -39894,8 +40491,8 @@ async function getTestFilesFromGraph(sourceFiles) {
39894
40491
  }
39895
40492
  for (const testFile of candidateTestFiles) {
39896
40493
  try {
39897
- const content = fs13.readFileSync(testFile, "utf-8");
39898
- const testDir = path23.dirname(testFile);
40494
+ const content = fs15.readFileSync(testFile, "utf-8");
40495
+ const testDir = path25.dirname(testFile);
39899
40496
  const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
39900
40497
  let match;
39901
40498
  match = importRegex.exec(content);
@@ -39903,8 +40500,8 @@ async function getTestFilesFromGraph(sourceFiles) {
39903
40500
  const importPath = match[1];
39904
40501
  let resolvedImport;
39905
40502
  if (importPath.startsWith(".")) {
39906
- resolvedImport = path23.resolve(testDir, importPath);
39907
- const existingExt = path23.extname(resolvedImport);
40503
+ resolvedImport = path25.resolve(testDir, importPath);
40504
+ const existingExt = path25.extname(resolvedImport);
39908
40505
  if (!existingExt) {
39909
40506
  for (const extToTry of [
39910
40507
  ".ts",
@@ -39915,7 +40512,7 @@ async function getTestFilesFromGraph(sourceFiles) {
39915
40512
  ".cjs"
39916
40513
  ]) {
39917
40514
  const withExt = resolvedImport + extToTry;
39918
- if (sourceFiles.includes(withExt) || fs13.existsSync(withExt)) {
40515
+ if (sourceFiles.includes(withExt) || fs15.existsSync(withExt)) {
39919
40516
  resolvedImport = withExt;
39920
40517
  break;
39921
40518
  }
@@ -39924,12 +40521,12 @@ async function getTestFilesFromGraph(sourceFiles) {
39924
40521
  } else {
39925
40522
  continue;
39926
40523
  }
39927
- const importBasename = path23.basename(resolvedImport, path23.extname(resolvedImport));
39928
- const importDir = path23.dirname(resolvedImport);
40524
+ const importBasename = path25.basename(resolvedImport, path25.extname(resolvedImport));
40525
+ const importDir = path25.dirname(resolvedImport);
39929
40526
  for (const sourceFile of sourceFiles) {
39930
- const sourceDir = path23.dirname(sourceFile);
39931
- const sourceBasename = path23.basename(sourceFile, path23.extname(sourceFile));
39932
- const isRelatedDir = importDir === sourceDir || importDir === path23.join(sourceDir, "__tests__") || importDir === path23.join(sourceDir, "tests") || importDir === path23.join(sourceDir, "test");
40527
+ const sourceDir = path25.dirname(sourceFile);
40528
+ const sourceBasename = path25.basename(sourceFile, path25.extname(sourceFile));
40529
+ const isRelatedDir = importDir === sourceDir || importDir === path25.join(sourceDir, "__tests__") || importDir === path25.join(sourceDir, "tests") || importDir === path25.join(sourceDir, "test");
39933
40530
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
39934
40531
  if (!testFiles.includes(testFile)) {
39935
40532
  testFiles.push(testFile);
@@ -39944,8 +40541,8 @@ async function getTestFilesFromGraph(sourceFiles) {
39944
40541
  while (match !== null) {
39945
40542
  const importPath = match[1];
39946
40543
  if (importPath.startsWith(".")) {
39947
- let resolvedImport = path23.resolve(testDir, importPath);
39948
- const existingExt = path23.extname(resolvedImport);
40544
+ let resolvedImport = path25.resolve(testDir, importPath);
40545
+ const existingExt = path25.extname(resolvedImport);
39949
40546
  if (!existingExt) {
39950
40547
  for (const extToTry of [
39951
40548
  ".ts",
@@ -39956,18 +40553,18 @@ async function getTestFilesFromGraph(sourceFiles) {
39956
40553
  ".cjs"
39957
40554
  ]) {
39958
40555
  const withExt = resolvedImport + extToTry;
39959
- if (sourceFiles.includes(withExt) || fs13.existsSync(withExt)) {
40556
+ if (sourceFiles.includes(withExt) || fs15.existsSync(withExt)) {
39960
40557
  resolvedImport = withExt;
39961
40558
  break;
39962
40559
  }
39963
40560
  }
39964
40561
  }
39965
- const importDir = path23.dirname(resolvedImport);
39966
- const importBasename = path23.basename(resolvedImport, path23.extname(resolvedImport));
40562
+ const importDir = path25.dirname(resolvedImport);
40563
+ const importBasename = path25.basename(resolvedImport, path25.extname(resolvedImport));
39967
40564
  for (const sourceFile of sourceFiles) {
39968
- const sourceDir = path23.dirname(sourceFile);
39969
- const sourceBasename = path23.basename(sourceFile, path23.extname(sourceFile));
39970
- const isRelatedDir = importDir === sourceDir || importDir === path23.join(sourceDir, "__tests__") || importDir === path23.join(sourceDir, "tests") || importDir === path23.join(sourceDir, "test");
40565
+ const sourceDir = path25.dirname(sourceFile);
40566
+ const sourceBasename = path25.basename(sourceFile, path25.extname(sourceFile));
40567
+ const isRelatedDir = importDir === sourceDir || importDir === path25.join(sourceDir, "__tests__") || importDir === path25.join(sourceDir, "tests") || importDir === path25.join(sourceDir, "test");
39971
40568
  if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
39972
40569
  if (!testFiles.includes(testFile)) {
39973
40570
  testFiles.push(testFile);
@@ -40052,8 +40649,8 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
40052
40649
  return ["mvn", "test"];
40053
40650
  case "gradle": {
40054
40651
  const isWindows = process.platform === "win32";
40055
- const hasGradlewBat = fs13.existsSync(path23.join(baseDir, "gradlew.bat"));
40056
- const hasGradlew = fs13.existsSync(path23.join(baseDir, "gradlew"));
40652
+ const hasGradlewBat = fs15.existsSync(path25.join(baseDir, "gradlew.bat"));
40653
+ const hasGradlew = fs15.existsSync(path25.join(baseDir, "gradlew"));
40057
40654
  if (hasGradlewBat && isWindows)
40058
40655
  return ["gradlew.bat", "test"];
40059
40656
  if (hasGradlew)
@@ -40070,7 +40667,7 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
40070
40667
  "cmake-build-release",
40071
40668
  "out"
40072
40669
  ];
40073
- const actualBuildDir = buildDirCandidates.find((d) => fs13.existsSync(path23.join(baseDir, d, "CMakeCache.txt"))) ?? "build";
40670
+ const actualBuildDir = buildDirCandidates.find((d) => fs15.existsSync(path25.join(baseDir, d, "CMakeCache.txt"))) ?? "build";
40074
40671
  return ["ctest", "--test-dir", actualBuildDir];
40075
40672
  }
40076
40673
  case "swift-test":
@@ -40496,10 +41093,57 @@ var SKIP_DIRECTORIES = new Set([
40496
41093
  ".bundle",
40497
41094
  ".tox"
40498
41095
  ]);
41096
+ function recordAndAnalyzeResults(result, testFiles, workingDir, sourceFiles) {
41097
+ if (!result.totals || result.totals.total === 0)
41098
+ return;
41099
+ const now = new Date().toISOString();
41100
+ const changedFiles = (sourceFiles && sourceFiles.length > 0 ? sourceFiles : testFiles).map((f) => f.replace(/\\/g, "/"));
41101
+ for (const testFile of testFiles) {
41102
+ try {
41103
+ appendTestRun({
41104
+ timestamp: now,
41105
+ taskId: "auto",
41106
+ testFile: testFile.replace(/\\/g, "/"),
41107
+ testName: "(aggregate)",
41108
+ result: result.success ? "pass" : "fail",
41109
+ durationMs: result.duration_ms || 0,
41110
+ changedFiles
41111
+ }, workingDir);
41112
+ } catch {}
41113
+ }
41114
+ }
41115
+ function analyzeFailures(workingDir) {
41116
+ const report = {
41117
+ flakyTests: [],
41118
+ failureClusters: [],
41119
+ quarantinedFailures: []
41120
+ };
41121
+ try {
41122
+ const history = getAllHistory(workingDir);
41123
+ if (history.length === 0)
41124
+ return report;
41125
+ report.flakyTests = detectFlakyTests(history);
41126
+ const failingResults = history.filter((r) => r.result === "fail");
41127
+ if (failingResults.length > 0) {
41128
+ const { clusters } = classifyAndCluster(failingResults, history);
41129
+ report.failureClusters = clusters.map((c) => ({
41130
+ rootCause: c.rootCause,
41131
+ affectedFiles: c.affectedTestFiles,
41132
+ classification: c.classification
41133
+ }));
41134
+ }
41135
+ for (const entry of report.flakyTests) {
41136
+ if (entry.isQuarantined) {
41137
+ report.quarantinedFailures.push(`${entry.testFile}: ${entry.testName}`);
41138
+ }
41139
+ }
41140
+ } catch {}
41141
+ return report;
41142
+ }
40499
41143
  var test_runner = createSwarmTool({
40500
- description: 'Run project tests with framework detection. Supports bun, vitest, jest, mocha, pytest, cargo, pester, go-test, maven, gradle, dotnet-test, ctest, swift-test, dart-test, rspec, and minitest. Returns deterministic normalized JSON with framework, scope, command, totals, coverage, duration, success status, and failures. Use scope "all" for full suite, "convention" to map source files to test files, or "graph" to find related tests via imports.',
41144
+ description: 'Run project tests with framework detection. Supports bun, vitest, jest, mocha, pytest, cargo, pester, go-test, maven, gradle, dotnet-test, ctest, swift-test, dart-test, rspec, and minitest. Returns deterministic normalized JSON with framework, scope, command, totals, coverage, duration, success status, and failures. Use scope "all" for full suite, "convention" to map source files to test files, "graph" to find related tests via imports, or "impact" to find tests covering changed files using test-impact analysis.',
40501
41145
  args: {
40502
- scope: tool.schema.enum(["all", "convention", "graph"]).optional().describe('Test scope: "all" runs full suite, "convention" maps source files to test files by naming, "graph" finds related tests via imports'),
41146
+ scope: tool.schema.enum(["all", "convention", "graph", "impact"]).optional().describe('Test scope: "all" runs full suite, "convention" maps source files to test files by naming, "graph" finds related tests via imports, "impact" finds tests covering changed files via test-impact analysis'),
40503
41147
  files: tool.schema.array(tool.schema.string()).optional().describe("Specific files to test (used with convention or graph scope)"),
40504
41148
  coverage: tool.schema.boolean().optional().describe("Enable coverage reporting if supported"),
40505
41149
  timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default 60000, max 300000)"),
@@ -40570,7 +41214,7 @@ var test_runner = createSwarmTool({
40570
41214
  framework: "none",
40571
41215
  scope: "all",
40572
41216
  error: "Invalid arguments",
40573
- message: 'scope must be "all", "convention", or "graph"; files must be array of strings; coverage must be boolean; timeout_ms must be a positive number',
41217
+ message: 'scope must be "all", "convention", "graph", or "impact"; files must be array of strings; coverage must be boolean; timeout_ms must be a positive number',
40574
41218
  outcome: "error"
40575
41219
  };
40576
41220
  return JSON.stringify(errorResult, null, 2);
@@ -40589,7 +41233,7 @@ var test_runner = createSwarmTool({
40589
41233
  return JSON.stringify(errorResult, null, 2);
40590
41234
  }
40591
41235
  }
40592
- if ((scope === "convention" || scope === "graph") && (!args.files || args.files.length === 0)) {
41236
+ if ((scope === "convention" || scope === "graph" || scope === "impact") && (!args.files || args.files.length === 0)) {
40593
41237
  const errorResult = {
40594
41238
  success: false,
40595
41239
  framework: "none",
@@ -40626,7 +41270,7 @@ var test_runner = createSwarmTool({
40626
41270
  let effectiveScope = scope;
40627
41271
  if (scope === "all") {} else if (scope === "convention") {
40628
41272
  const sourceFiles = args.files.filter((f) => {
40629
- const ext = path23.extname(f).toLowerCase();
41273
+ const ext = path25.extname(f).toLowerCase();
40630
41274
  return SOURCE_EXTENSIONS.has(ext);
40631
41275
  });
40632
41276
  if (sourceFiles.length === 0) {
@@ -40643,7 +41287,7 @@ var test_runner = createSwarmTool({
40643
41287
  testFiles = getTestFilesFromConvention(sourceFiles);
40644
41288
  } else if (scope === "graph") {
40645
41289
  const sourceFiles = args.files.filter((f) => {
40646
- const ext = path23.extname(f).toLowerCase();
41290
+ const ext = path25.extname(f).toLowerCase();
40647
41291
  return SOURCE_EXTENSIONS.has(ext);
40648
41292
  });
40649
41293
  if (sourceFiles.length === 0) {
@@ -40665,6 +41309,53 @@ var test_runner = createSwarmTool({
40665
41309
  effectiveScope = "convention";
40666
41310
  testFiles = getTestFilesFromConvention(sourceFiles);
40667
41311
  }
41312
+ } else if (scope === "impact") {
41313
+ const sourceFiles = args.files.filter((f) => {
41314
+ const ext = path25.extname(f).toLowerCase();
41315
+ return SOURCE_EXTENSIONS.has(ext);
41316
+ });
41317
+ if (sourceFiles.length === 0) {
41318
+ const errorResult = {
41319
+ success: false,
41320
+ framework,
41321
+ scope,
41322
+ error: "Provided files contain no source files with recognized extensions",
41323
+ message: "The files array must contain at least one source file with a recognized extension (.ts, .tsx, .js, .jsx, .py, .rs, .ps1, etc.).",
41324
+ outcome: "error"
41325
+ };
41326
+ return JSON.stringify(errorResult, null, 2);
41327
+ }
41328
+ try {
41329
+ const impactResult = await analyzeImpact(sourceFiles, workingDir);
41330
+ if (impactResult.impactedTests.length > 0) {
41331
+ testFiles = impactResult.impactedTests.map((absPath) => {
41332
+ const relativePath = path25.relative(workingDir, absPath);
41333
+ return path25.isAbsolute(relativePath) ? absPath : relativePath;
41334
+ });
41335
+ } else {
41336
+ graphFallbackReason = "no impacted tests found via impact analysis, falling back to graph";
41337
+ effectiveScope = "graph";
41338
+ const graphTestFiles = await getTestFilesFromGraph(sourceFiles);
41339
+ if (graphTestFiles.length > 0) {
41340
+ testFiles = graphTestFiles;
41341
+ } else {
41342
+ graphFallbackReason = "imports resolution returned no results, falling back to convention";
41343
+ effectiveScope = "convention";
41344
+ testFiles = getTestFilesFromConvention(sourceFiles);
41345
+ }
41346
+ }
41347
+ } catch {
41348
+ graphFallbackReason = "impact analysis failed, falling back to graph";
41349
+ effectiveScope = "graph";
41350
+ const graphTestFiles = await getTestFilesFromGraph(sourceFiles);
41351
+ if (graphTestFiles.length > 0) {
41352
+ testFiles = graphTestFiles;
41353
+ } else {
41354
+ graphFallbackReason = "imports resolution returned no results, falling back to convention";
41355
+ effectiveScope = "convention";
41356
+ testFiles = getTestFilesFromConvention(sourceFiles);
41357
+ }
41358
+ }
40668
41359
  }
40669
41360
  if (scope !== "all" && testFiles.length === 0) {
40670
41361
  const baseMessage = "No matching test files found for the provided source files. Check that test files exist with matching naming conventions (.spec.*, .test.*, __tests__/, tests/, test/).";
@@ -40675,7 +41366,8 @@ var test_runner = createSwarmTool({
40675
41366
  error: "Provided source files resolved to zero test files",
40676
41367
  message: graphFallbackReason ? `${baseMessage} (${graphFallbackReason})` : baseMessage,
40677
41368
  outcome: "skip",
40678
- ...scope === "graph" && { attempted_scope: "graph" }
41369
+ ...scope === "graph" && { attempted_scope: "graph" },
41370
+ ...scope === "impact" && { attempted_scope: "graph" }
40679
41371
  };
40680
41372
  return JSON.stringify(errorResult, null, 2);
40681
41373
  }
@@ -40692,6 +41384,18 @@ var test_runner = createSwarmTool({
40692
41384
  return JSON.stringify(errorResult, null, 2);
40693
41385
  }
40694
41386
  const result = await runTests(framework, effectiveScope, testFiles, coverage, timeout_ms, workingDir);
41387
+ recordAndAnalyzeResults(result, testFiles, workingDir, _files.length > 0 ? _files : undefined);
41388
+ let historyReport;
41389
+ if (!result.success && result.totals && result.totals.failed > 0) {
41390
+ historyReport = analyzeFailures(workingDir);
41391
+ if (historyReport.quarantinedFailures.length > 0) {
41392
+ result.message = (result.message || "") + ` | QUARANTINED (flaky): ${historyReport.quarantinedFailures.join(", ")}`;
41393
+ }
41394
+ if (historyReport.failureClusters.length > 0) {
41395
+ const clusterSummary = historyReport.failureClusters.slice(0, 3).map((c) => `${c.classification}: ${c.rootCause.substring(0, 80)}`).join("; ");
41396
+ result.message = `${result.message || ""} | FAILURE ANALYSIS: ${clusterSummary}`;
41397
+ }
41398
+ }
40695
41399
  if (graphFallbackReason && result.message) {
40696
41400
  result.message = `${result.message} (${graphFallbackReason})`;
40697
41401
  }
@@ -40719,8 +41423,8 @@ function validateDirectoryPath(dir) {
40719
41423
  if (dir.includes("..")) {
40720
41424
  throw new Error("Directory path must not contain path traversal sequences");
40721
41425
  }
40722
- const normalized = path24.normalize(dir);
40723
- const absolutePath = path24.isAbsolute(normalized) ? normalized : path24.resolve(normalized);
41426
+ const normalized = path26.normalize(dir);
41427
+ const absolutePath = path26.isAbsolute(normalized) ? normalized : path26.resolve(normalized);
40724
41428
  return absolutePath;
40725
41429
  }
40726
41430
  function validateTimeout(timeoutMs, defaultValue) {
@@ -40743,9 +41447,9 @@ function validateTimeout(timeoutMs, defaultValue) {
40743
41447
  }
40744
41448
  function getPackageVersion(dir) {
40745
41449
  try {
40746
- const packagePath = path24.join(dir, "package.json");
40747
- if (fs14.existsSync(packagePath)) {
40748
- const content = fs14.readFileSync(packagePath, "utf-8");
41450
+ const packagePath = path26.join(dir, "package.json");
41451
+ if (fs16.existsSync(packagePath)) {
41452
+ const content = fs16.readFileSync(packagePath, "utf-8");
40749
41453
  const pkg = JSON.parse(content);
40750
41454
  return pkg.version ?? null;
40751
41455
  }
@@ -40754,9 +41458,9 @@ function getPackageVersion(dir) {
40754
41458
  }
40755
41459
  function getChangelogVersion(dir) {
40756
41460
  try {
40757
- const changelogPath = path24.join(dir, "CHANGELOG.md");
40758
- if (fs14.existsSync(changelogPath)) {
40759
- const content = fs14.readFileSync(changelogPath, "utf-8");
41461
+ const changelogPath = path26.join(dir, "CHANGELOG.md");
41462
+ if (fs16.existsSync(changelogPath)) {
41463
+ const content = fs16.readFileSync(changelogPath, "utf-8");
40760
41464
  const match = content.match(/^##\s*\[?(\d+\.\d+\.\d+)\]?/m);
40761
41465
  if (match) {
40762
41466
  return match[1];
@@ -40768,10 +41472,10 @@ function getChangelogVersion(dir) {
40768
41472
  function getVersionFileVersion(dir) {
40769
41473
  const possibleFiles = ["VERSION.txt", "version.txt", "VERSION", "version"];
40770
41474
  for (const file3 of possibleFiles) {
40771
- const filePath = path24.join(dir, file3);
40772
- if (fs14.existsSync(filePath)) {
41475
+ const filePath = path26.join(dir, file3);
41476
+ if (fs16.existsSync(filePath)) {
40773
41477
  try {
40774
- const content = fs14.readFileSync(filePath, "utf-8").trim();
41478
+ const content = fs16.readFileSync(filePath, "utf-8").trim();
40775
41479
  const match = content.match(/(\d+\.\d+\.\d+)/);
40776
41480
  if (match) {
40777
41481
  return match[1];
@@ -41095,8 +41799,8 @@ async function runEvidenceCheck(dir) {
41095
41799
  async function runRequirementCoverageCheck(dir, currentPhase) {
41096
41800
  const startTime = Date.now();
41097
41801
  try {
41098
- const specPath = path24.join(dir, ".swarm", "spec.md");
41099
- if (!fs14.existsSync(specPath)) {
41802
+ const specPath = path26.join(dir, ".swarm", "spec.md");
41803
+ if (!fs16.existsSync(specPath)) {
41100
41804
  return {
41101
41805
  type: "req_coverage",
41102
41806
  status: "skip",
@@ -41309,9 +42013,9 @@ async function handlePreflightCommand(directory, _args) {
41309
42013
  return formatPreflightMarkdown(report);
41310
42014
  }
41311
42015
  // src/knowledge/hive-promoter.ts
41312
- import * as fs15 from "fs";
42016
+ import * as fs17 from "fs";
41313
42017
  import * as os5 from "os";
41314
- import * as path25 from "path";
42018
+ import * as path27 from "path";
41315
42019
  var DANGEROUS_PATTERNS = [
41316
42020
  [/rm\s+-rf/, "rm\\s+-rf"],
41317
42021
  [/:\s*!\s*\|/, ":\\s*!\\s*\\|"],
@@ -41357,13 +42061,13 @@ function getHiveFilePath() {
41357
42061
  const home = os5.homedir();
41358
42062
  let dataDir;
41359
42063
  if (platform === "win32") {
41360
- dataDir = path25.join(process.env.LOCALAPPDATA || path25.join(home, "AppData", "Local"), "opencode-swarm", "Data");
42064
+ dataDir = path27.join(process.env.LOCALAPPDATA || path27.join(home, "AppData", "Local"), "opencode-swarm", "Data");
41361
42065
  } else if (platform === "darwin") {
41362
- dataDir = path25.join(home, "Library", "Application Support", "opencode-swarm");
42066
+ dataDir = path27.join(home, "Library", "Application Support", "opencode-swarm");
41363
42067
  } else {
41364
- dataDir = path25.join(process.env.XDG_DATA_HOME || path25.join(home, ".local", "share"), "opencode-swarm");
42068
+ dataDir = path27.join(process.env.XDG_DATA_HOME || path27.join(home, ".local", "share"), "opencode-swarm");
41365
42069
  }
41366
- return path25.join(dataDir, "hive-knowledge.jsonl");
42070
+ return path27.join(dataDir, "hive-knowledge.jsonl");
41367
42071
  }
41368
42072
  async function promoteToHive(_directory, lesson, category) {
41369
42073
  const trimmed = (lesson ?? "").trim();
@@ -41375,9 +42079,9 @@ async function promoteToHive(_directory, lesson, category) {
41375
42079
  throw new Error(`Lesson rejected by validator: ${validation.reason}`);
41376
42080
  }
41377
42081
  const hivePath = getHiveFilePath();
41378
- const hiveDir = path25.dirname(hivePath);
41379
- if (!fs15.existsSync(hiveDir)) {
41380
- fs15.mkdirSync(hiveDir, { recursive: true });
42082
+ const hiveDir = path27.dirname(hivePath);
42083
+ if (!fs17.existsSync(hiveDir)) {
42084
+ fs17.mkdirSync(hiveDir, { recursive: true });
41381
42085
  }
41382
42086
  const now = new Date;
41383
42087
  const entry = {
@@ -41391,16 +42095,16 @@ async function promoteToHive(_directory, lesson, category) {
41391
42095
  promotedAt: now.toISOString(),
41392
42096
  retrievalOutcomes: { applied: 0, succeededAfter: 0, failedAfter: 0 }
41393
42097
  };
41394
- fs15.appendFileSync(hivePath, `${JSON.stringify(entry)}
42098
+ fs17.appendFileSync(hivePath, `${JSON.stringify(entry)}
41395
42099
  `, "utf-8");
41396
42100
  const preview = `${trimmed.slice(0, 50)}${trimmed.length > 50 ? "..." : ""}`;
41397
42101
  return `Promoted to hive: "${preview}" (confidence: 1.0, source: manual)`;
41398
42102
  }
41399
42103
  async function promoteFromSwarm(directory, lessonId) {
41400
- const knowledgePath = path25.join(directory, ".swarm", "knowledge.jsonl");
42104
+ const knowledgePath = path27.join(directory, ".swarm", "knowledge.jsonl");
41401
42105
  const entries = [];
41402
- if (fs15.existsSync(knowledgePath)) {
41403
- const content = fs15.readFileSync(knowledgePath, "utf-8");
42106
+ if (fs17.existsSync(knowledgePath)) {
42107
+ const content = fs17.readFileSync(knowledgePath, "utf-8");
41404
42108
  for (const line of content.split(`
41405
42109
  `)) {
41406
42110
  const t = line.trim();
@@ -41424,9 +42128,9 @@ async function promoteFromSwarm(directory, lessonId) {
41424
42128
  throw new Error(`Lesson rejected by validator: ${validation.reason}`);
41425
42129
  }
41426
42130
  const hivePath = getHiveFilePath();
41427
- const hiveDir = path25.dirname(hivePath);
41428
- if (!fs15.existsSync(hiveDir)) {
41429
- fs15.mkdirSync(hiveDir, { recursive: true });
42131
+ const hiveDir = path27.dirname(hivePath);
42132
+ if (!fs17.existsSync(hiveDir)) {
42133
+ fs17.mkdirSync(hiveDir, { recursive: true });
41430
42134
  }
41431
42135
  const now = new Date;
41432
42136
  const hiveEntry = {
@@ -41440,7 +42144,7 @@ async function promoteFromSwarm(directory, lessonId) {
41440
42144
  promotedAt: now.toISOString(),
41441
42145
  retrievalOutcomes: { applied: 0, succeededAfter: 0, failedAfter: 0 }
41442
42146
  };
41443
- fs15.appendFileSync(hivePath, `${JSON.stringify(hiveEntry)}
42147
+ fs17.appendFileSync(hivePath, `${JSON.stringify(hiveEntry)}
41444
42148
  `, "utf-8");
41445
42149
  const preview = `${lessonText.slice(0, 50)}${lessonText.length > 50 ? "..." : ""}`;
41446
42150
  return `Promoted to hive: "${preview}" (confidence: 1.0, source: manual)`;
@@ -41493,8 +42197,326 @@ async function handlePromoteCommand(directory, args) {
41493
42197
  }
41494
42198
  }
41495
42199
 
42200
+ // src/db/qa-gate-profile.ts
42201
+ import { createHash as createHash4 } from "crypto";
42202
+
42203
+ // src/db/project-db.ts
42204
+ import { Database } from "bun:sqlite";
42205
+ import { existsSync as existsSync16, mkdirSync as mkdirSync7 } from "fs";
42206
+ import { join as join22, resolve as resolve11 } from "path";
42207
+ var MIGRATIONS = [
42208
+ {
42209
+ version: 1,
42210
+ name: "create_project_constraints",
42211
+ sql: `CREATE TABLE project_constraints (
42212
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42213
+ constraint_type TEXT NOT NULL,
42214
+ content TEXT NOT NULL,
42215
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
42216
+ )`
42217
+ },
42218
+ {
42219
+ version: 2,
42220
+ name: "create_qa_gate_profile",
42221
+ sql: `CREATE TABLE qa_gate_profile (
42222
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
42223
+ plan_id TEXT NOT NULL UNIQUE,
42224
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
42225
+ project_type TEXT,
42226
+ gates TEXT NOT NULL DEFAULT '{}',
42227
+ locked_at TEXT,
42228
+ locked_by_snapshot_seq INTEGER
42229
+ )`
42230
+ },
42231
+ {
42232
+ version: 3,
42233
+ name: "create_qa_gate_profile_immutability_trigger",
42234
+ sql: `CREATE TRIGGER IF NOT EXISTS trg_qa_gate_profile_no_update_after_lock
42235
+ BEFORE UPDATE ON qa_gate_profile
42236
+ WHEN OLD.locked_at IS NOT NULL
42237
+ BEGIN
42238
+ SELECT RAISE(ABORT, 'qa_gate_profile row is locked and cannot be modified after critic approval');
42239
+ END`
42240
+ }
42241
+ ];
42242
+ var _projectDbs = new Map;
42243
+ function runProjectMigrations(db) {
42244
+ db.run(`CREATE TABLE IF NOT EXISTS schema_migrations (
42245
+ version INTEGER PRIMARY KEY,
42246
+ name TEXT NOT NULL,
42247
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
42248
+ )`);
42249
+ const row = db.query("SELECT MAX(version) as version FROM schema_migrations").get();
42250
+ const currentVersion = row?.version ?? 0;
42251
+ for (const migration of MIGRATIONS) {
42252
+ if (migration.version <= currentVersion)
42253
+ continue;
42254
+ const apply = db.transaction(() => {
42255
+ db.run(migration.sql);
42256
+ db.run("INSERT INTO schema_migrations (version, name) VALUES (?, ?)", [
42257
+ migration.version,
42258
+ migration.name
42259
+ ]);
42260
+ });
42261
+ apply();
42262
+ }
42263
+ }
42264
+ function projectDbPath(directory) {
42265
+ return join22(resolve11(directory), ".swarm", "swarm.db");
42266
+ }
42267
+ function projectDbExists(directory) {
42268
+ return existsSync16(projectDbPath(directory));
42269
+ }
42270
+ function getProjectDb(directory) {
42271
+ const key = resolve11(directory);
42272
+ const existing = _projectDbs.get(key);
42273
+ if (existing)
42274
+ return existing;
42275
+ const swarmDir = join22(key, ".swarm");
42276
+ mkdirSync7(swarmDir, { recursive: true });
42277
+ const db = new Database(join22(swarmDir, "swarm.db"));
42278
+ db.run("PRAGMA journal_mode = WAL;");
42279
+ db.run("PRAGMA synchronous = NORMAL;");
42280
+ db.run("PRAGMA busy_timeout = 5000;");
42281
+ db.run("PRAGMA foreign_keys = ON;");
42282
+ runProjectMigrations(db);
42283
+ _projectDbs.set(key, db);
42284
+ return db;
42285
+ }
42286
+
42287
+ // src/db/qa-gate-profile.ts
42288
+ var DEFAULT_QA_GATES = {
42289
+ reviewer: true,
42290
+ test_engineer: true,
42291
+ council_mode: false,
42292
+ sme_enabled: true,
42293
+ critic_pre_plan: true,
42294
+ hallucination_guard: false,
42295
+ sast_enabled: true
42296
+ };
42297
+ function rowToProfile(row) {
42298
+ let parsed = {};
42299
+ try {
42300
+ parsed = JSON.parse(row.gates);
42301
+ } catch {
42302
+ parsed = {};
42303
+ }
42304
+ const gates = { ...DEFAULT_QA_GATES, ...parsed };
42305
+ return {
42306
+ id: row.id,
42307
+ plan_id: row.plan_id,
42308
+ created_at: row.created_at,
42309
+ project_type: row.project_type,
42310
+ gates,
42311
+ locked_at: row.locked_at,
42312
+ locked_by_snapshot_seq: row.locked_by_snapshot_seq
42313
+ };
42314
+ }
42315
+ function getProfile(directory, planId) {
42316
+ if (!projectDbExists(directory))
42317
+ return null;
42318
+ const db = getProjectDb(directory);
42319
+ const row = db.query("SELECT * FROM qa_gate_profile WHERE plan_id = ?").get(planId);
42320
+ return row ? rowToProfile(row) : null;
42321
+ }
42322
+ function getOrCreateProfile(directory, planId, projectType) {
42323
+ const existing = getProfile(directory, planId);
42324
+ if (existing)
42325
+ return existing;
42326
+ const db = getProjectDb(directory);
42327
+ const gatesJson = JSON.stringify(DEFAULT_QA_GATES);
42328
+ const insert = db.transaction(() => {
42329
+ db.run("INSERT INTO qa_gate_profile (plan_id, project_type, gates) VALUES (?, ?, ?)", [planId, projectType ?? null, gatesJson]);
42330
+ });
42331
+ try {
42332
+ insert();
42333
+ } catch (err) {
42334
+ const msg = err instanceof Error ? err.message : String(err);
42335
+ if (!msg.toLowerCase().includes("unique")) {
42336
+ throw err;
42337
+ }
42338
+ }
42339
+ const after = getProfile(directory, planId);
42340
+ if (!after) {
42341
+ throw new Error(`Failed to create or load QA gate profile for plan_id=${planId}`);
42342
+ }
42343
+ return after;
42344
+ }
42345
+ function setGates(directory, planId, gates) {
42346
+ const current = getProfile(directory, planId);
42347
+ if (!current) {
42348
+ throw new Error(`No QA gate profile found for plan_id=${planId} \u2014 call getOrCreateProfile first`);
42349
+ }
42350
+ if (current.locked_at !== null) {
42351
+ throw new Error("Cannot modify gates: QA gate profile is locked after critic approval");
42352
+ }
42353
+ const merged = { ...current.gates };
42354
+ for (const key of Object.keys(gates)) {
42355
+ const incoming = gates[key];
42356
+ if (incoming === undefined)
42357
+ continue;
42358
+ if (incoming === false && current.gates[key] === true) {
42359
+ throw new Error(`Cannot disable gate '${key}': sessions can only ratchet tighter`);
42360
+ }
42361
+ if (incoming === true) {
42362
+ merged[key] = true;
42363
+ }
42364
+ }
42365
+ const db = getProjectDb(directory);
42366
+ db.run("UPDATE qa_gate_profile SET gates = ? WHERE plan_id = ?", [
42367
+ JSON.stringify(merged),
42368
+ planId
42369
+ ]);
42370
+ const updated = getProfile(directory, planId);
42371
+ if (!updated) {
42372
+ throw new Error(`Failed to re-read QA gate profile after update for plan_id=${planId}`);
42373
+ }
42374
+ return updated;
42375
+ }
42376
+ function computeProfileHash(profile) {
42377
+ const payload = JSON.stringify({
42378
+ plan_id: profile.plan_id,
42379
+ gates: profile.gates
42380
+ });
42381
+ return createHash4("sha256").update(payload).digest("hex");
42382
+ }
42383
+ function getEffectiveGates(profile, sessionOverrides) {
42384
+ const merged = { ...profile.gates };
42385
+ for (const key of Object.keys(sessionOverrides)) {
42386
+ if (sessionOverrides[key] === true) {
42387
+ merged[key] = true;
42388
+ }
42389
+ }
42390
+ return merged;
42391
+ }
42392
+
42393
+ // src/commands/qa-gates.ts
42394
+ init_manager();
42395
+ var ALL_GATE_NAMES = [
42396
+ "reviewer",
42397
+ "test_engineer",
42398
+ "council_mode",
42399
+ "sme_enabled",
42400
+ "critic_pre_plan",
42401
+ "hallucination_guard",
42402
+ "sast_enabled"
42403
+ ];
42404
+ function derivePlanId(plan) {
42405
+ return `${plan.swarm}-${plan.title}`.replace(/[^a-zA-Z0-9-_]/g, "_");
42406
+ }
42407
+ function isGateName(name) {
42408
+ return ALL_GATE_NAMES.includes(name);
42409
+ }
42410
+ function formatGates(gates) {
42411
+ return ALL_GATE_NAMES.map((g) => ` - ${g}: ${gates[g] ? "on" : "off"}`).join(`
42412
+ `);
42413
+ }
42414
+ async function handleQaGatesCommand(directory, args, sessionID) {
42415
+ const plan = await loadPlanJsonOnly(directory);
42416
+ if (!plan) {
42417
+ return "Error: plan.json not found or invalid. Create a plan first (e.g. /swarm specify or save_plan).";
42418
+ }
42419
+ const planId = derivePlanId(plan);
42420
+ const subcommand = args[0]?.toLowerCase();
42421
+ const gateArgs = args.slice(1);
42422
+ if (!subcommand || subcommand === "show" || subcommand === "status") {
42423
+ const profile = getProfile(directory, planId);
42424
+ const spec = profile ? profile.gates : DEFAULT_QA_GATES;
42425
+ const session = sessionID ? getAgentSession(sessionID) : null;
42426
+ const overrides = session?.qaGateSessionOverrides ?? {};
42427
+ const effective = profile ? getEffectiveGates(profile, overrides) : { ...DEFAULT_QA_GATES, ...overrides };
42428
+ const lines = [];
42429
+ lines.push(`QA Gate Profile for plan_id=${planId}`);
42430
+ if (!profile) {
42431
+ lines.push(" (no profile persisted yet \u2014 showing defaults)");
42432
+ } else {
42433
+ lines.push(` locked: ${profile.locked_at ? `yes @ ${profile.locked_at} (seq ${profile.locked_by_snapshot_seq ?? "?"})` : "no"}`);
42434
+ lines.push(` profile_hash: ${computeProfileHash(profile)}`);
42435
+ }
42436
+ lines.push("Spec-level gates:");
42437
+ lines.push(formatGates(spec));
42438
+ lines.push("Session overrides (ratchet-tighter only):");
42439
+ if (Object.keys(overrides).length === 0) {
42440
+ lines.push(" (none)");
42441
+ } else {
42442
+ for (const k of ALL_GATE_NAMES) {
42443
+ if (overrides[k] === true)
42444
+ lines.push(` - ${k}: on (override)`);
42445
+ }
42446
+ }
42447
+ lines.push("Effective gates:");
42448
+ lines.push(formatGates(effective));
42449
+ return lines.join(`
42450
+ `);
42451
+ }
42452
+ if (subcommand === "enable") {
42453
+ if (gateArgs.length === 0) {
42454
+ return "Usage: /swarm qa-gates enable <gate> [<gate> ...]";
42455
+ }
42456
+ const invalid = gateArgs.filter((g) => !isGateName(g));
42457
+ if (invalid.length > 0) {
42458
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
42459
+ }
42460
+ getOrCreateProfile(directory, planId);
42461
+ const patch = {};
42462
+ for (const g of gateArgs) {
42463
+ if (isGateName(g))
42464
+ patch[g] = true;
42465
+ }
42466
+ try {
42467
+ const updated = setGates(directory, planId, patch);
42468
+ return [
42469
+ `Enabled gates persisted for plan_id=${planId}:`,
42470
+ formatGates(updated.gates),
42471
+ `profile_hash: ${computeProfileHash(updated)}`
42472
+ ].join(`
42473
+ `);
42474
+ } catch (err) {
42475
+ const msg = err instanceof Error ? err.message : String(err);
42476
+ return `Error: ${msg}`;
42477
+ }
42478
+ }
42479
+ if (subcommand === "override") {
42480
+ if (!sessionID) {
42481
+ return "Error: session overrides require an active session context.";
42482
+ }
42483
+ if (gateArgs.length === 0) {
42484
+ return "Usage: /swarm qa-gates override <gate> [<gate> ...]";
42485
+ }
42486
+ const invalid = gateArgs.filter((g) => !isGateName(g));
42487
+ if (invalid.length > 0) {
42488
+ return `Error: unknown gate(s): ${invalid.join(", ")}. Valid gates: ${ALL_GATE_NAMES.join(", ")}`;
42489
+ }
42490
+ const session = getAgentSession(sessionID);
42491
+ if (!session) {
42492
+ return "Error: no active session found for override.";
42493
+ }
42494
+ const current = session.qaGateSessionOverrides ?? {};
42495
+ const next = { ...current };
42496
+ for (const g of gateArgs) {
42497
+ if (isGateName(g))
42498
+ next[g] = true;
42499
+ }
42500
+ session.qaGateSessionOverrides = next;
42501
+ return [
42502
+ `Session overrides updated for plan_id=${planId}:`,
42503
+ Object.keys(next).filter((k) => next[k] === true).map((k) => ` - ${k}: on`).join(`
42504
+ `) || " (none)"
42505
+ ].join(`
42506
+ `);
42507
+ }
42508
+ return [
42509
+ "Usage:",
42510
+ " /swarm qa-gates show current profile + effective gates",
42511
+ " /swarm qa-gates enable <gate>... persist-enable gate(s) (rejected if locked)",
42512
+ " /swarm qa-gates override <gate>... session-only enable (ratchet-tighter)",
42513
+ `Valid gates: ${ALL_GATE_NAMES.join(", ")}`
42514
+ ].join(`
42515
+ `);
42516
+ }
42517
+
41496
42518
  // src/commands/reset.ts
41497
- import * as fs16 from "fs";
42519
+ import * as fs18 from "fs";
41498
42520
 
41499
42521
  // src/background/manager.ts
41500
42522
  init_utils();
@@ -41549,13 +42571,13 @@ class CircuitBreaker {
41549
42571
  if (this.config.callTimeoutMs <= 0) {
41550
42572
  return fn();
41551
42573
  }
41552
- return new Promise((resolve11, reject) => {
42574
+ return new Promise((resolve12, reject) => {
41553
42575
  const timeout = setTimeout(() => {
41554
42576
  reject(new Error(`Call timeout after ${this.config.callTimeoutMs}ms`));
41555
42577
  }, this.config.callTimeoutMs);
41556
42578
  fn().then((result) => {
41557
42579
  clearTimeout(timeout);
41558
- resolve11(result);
42580
+ resolve12(result);
41559
42581
  }).catch((error93) => {
41560
42582
  clearTimeout(timeout);
41561
42583
  reject(error93);
@@ -41839,7 +42861,7 @@ class AutomationQueue {
41839
42861
 
41840
42862
  // src/background/worker.ts
41841
42863
  function sleep(ms) {
41842
- return new Promise((resolve11) => setTimeout(resolve11, ms));
42864
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
41843
42865
  }
41844
42866
 
41845
42867
  class WorkerManager {
@@ -42195,8 +43217,8 @@ async function handleResetCommand(directory, args) {
42195
43217
  for (const filename of filesToReset) {
42196
43218
  try {
42197
43219
  const resolvedPath = validateSwarmPath(directory, filename);
42198
- if (fs16.existsSync(resolvedPath)) {
42199
- fs16.unlinkSync(resolvedPath);
43220
+ if (fs18.existsSync(resolvedPath)) {
43221
+ fs18.unlinkSync(resolvedPath);
42200
43222
  results.push(`- \u2705 Deleted ${filename}`);
42201
43223
  } else {
42202
43224
  results.push(`- \u23ED\uFE0F ${filename} not found (skipped)`);
@@ -42213,8 +43235,8 @@ async function handleResetCommand(directory, args) {
42213
43235
  }
42214
43236
  try {
42215
43237
  const summariesPath = validateSwarmPath(directory, "summaries");
42216
- if (fs16.existsSync(summariesPath)) {
42217
- fs16.rmSync(summariesPath, { recursive: true, force: true });
43238
+ if (fs18.existsSync(summariesPath)) {
43239
+ fs18.rmSync(summariesPath, { recursive: true, force: true });
42218
43240
  results.push("- \u2705 Deleted summaries/ directory");
42219
43241
  } else {
42220
43242
  results.push("- \u23ED\uFE0F summaries/ not found (skipped)");
@@ -42234,14 +43256,14 @@ async function handleResetCommand(directory, args) {
42234
43256
 
42235
43257
  // src/commands/reset-session.ts
42236
43258
  init_utils2();
42237
- import * as fs17 from "fs";
42238
- import * as path26 from "path";
43259
+ import * as fs19 from "fs";
43260
+ import * as path28 from "path";
42239
43261
  async function handleResetSessionCommand(directory, _args) {
42240
43262
  const results = [];
42241
43263
  try {
42242
43264
  const statePath = validateSwarmPath(directory, "session/state.json");
42243
- if (fs17.existsSync(statePath)) {
42244
- fs17.unlinkSync(statePath);
43265
+ if (fs19.existsSync(statePath)) {
43266
+ fs19.unlinkSync(statePath);
42245
43267
  results.push("\u2705 Deleted .swarm/session/state.json");
42246
43268
  } else {
42247
43269
  results.push("\u23ED\uFE0F state.json not found (already clean)");
@@ -42250,15 +43272,15 @@ async function handleResetSessionCommand(directory, _args) {
42250
43272
  results.push("\u274C Failed to delete state.json");
42251
43273
  }
42252
43274
  try {
42253
- const sessionDir = path26.dirname(validateSwarmPath(directory, "session/state.json"));
42254
- if (fs17.existsSync(sessionDir)) {
42255
- const files = fs17.readdirSync(sessionDir);
43275
+ const sessionDir = path28.dirname(validateSwarmPath(directory, "session/state.json"));
43276
+ if (fs19.existsSync(sessionDir)) {
43277
+ const files = fs19.readdirSync(sessionDir);
42256
43278
  const otherFiles = files.filter((f) => f !== "state.json");
42257
43279
  let deletedCount = 0;
42258
43280
  for (const file3 of otherFiles) {
42259
- const filePath = path26.join(sessionDir, file3);
42260
- if (fs17.lstatSync(filePath).isFile()) {
42261
- fs17.unlinkSync(filePath);
43281
+ const filePath = path28.join(sessionDir, file3);
43282
+ if (fs19.lstatSync(filePath).isFile()) {
43283
+ fs19.unlinkSync(filePath);
42262
43284
  deletedCount++;
42263
43285
  }
42264
43286
  }
@@ -42286,7 +43308,7 @@ async function handleResetSessionCommand(directory, _args) {
42286
43308
  // src/summaries/manager.ts
42287
43309
  init_utils2();
42288
43310
  init_utils();
42289
- import * as path27 from "path";
43311
+ import * as path29 from "path";
42290
43312
  var SUMMARY_ID_REGEX = /^S\d+$/;
42291
43313
  function sanitizeSummaryId(id) {
42292
43314
  if (!id || id.length === 0) {
@@ -42310,7 +43332,7 @@ function sanitizeSummaryId(id) {
42310
43332
  }
42311
43333
  async function loadFullOutput(directory, id) {
42312
43334
  const sanitizedId = sanitizeSummaryId(id);
42313
- const relativePath = path27.join("summaries", `${sanitizedId}.json`);
43335
+ const relativePath = path29.join("summaries", `${sanitizedId}.json`);
42314
43336
  validateSwarmPath(directory, relativePath);
42315
43337
  const content = await readSwarmFileAsync(directory, relativePath);
42316
43338
  if (content === null) {
@@ -42363,18 +43385,18 @@ ${error93 instanceof Error ? error93.message : String(error93)}`;
42363
43385
 
42364
43386
  // src/commands/rollback.ts
42365
43387
  init_utils2();
42366
- import * as fs18 from "fs";
42367
- import * as path28 from "path";
43388
+ import * as fs20 from "fs";
43389
+ import * as path30 from "path";
42368
43390
  async function handleRollbackCommand(directory, args) {
42369
43391
  const phaseArg = args[0];
42370
43392
  if (!phaseArg) {
42371
43393
  const manifestPath2 = validateSwarmPath(directory, "checkpoints/manifest.json");
42372
- if (!fs18.existsSync(manifestPath2)) {
43394
+ if (!fs20.existsSync(manifestPath2)) {
42373
43395
  return "No checkpoints found. Use `/swarm checkpoint` to create checkpoints.";
42374
43396
  }
42375
43397
  let manifest2;
42376
43398
  try {
42377
- manifest2 = JSON.parse(fs18.readFileSync(manifestPath2, "utf-8"));
43399
+ manifest2 = JSON.parse(fs20.readFileSync(manifestPath2, "utf-8"));
42378
43400
  } catch {
42379
43401
  return "Error: Checkpoint manifest is corrupted. Delete .swarm/checkpoints/manifest.json and re-checkpoint.";
42380
43402
  }
@@ -42396,12 +43418,12 @@ async function handleRollbackCommand(directory, args) {
42396
43418
  return "Error: Phase number must be a positive integer.";
42397
43419
  }
42398
43420
  const manifestPath = validateSwarmPath(directory, "checkpoints/manifest.json");
42399
- if (!fs18.existsSync(manifestPath)) {
43421
+ if (!fs20.existsSync(manifestPath)) {
42400
43422
  return `Error: No checkpoints found. Cannot rollback to phase ${targetPhase}.`;
42401
43423
  }
42402
43424
  let manifest;
42403
43425
  try {
42404
- manifest = JSON.parse(fs18.readFileSync(manifestPath, "utf-8"));
43426
+ manifest = JSON.parse(fs20.readFileSync(manifestPath, "utf-8"));
42405
43427
  } catch {
42406
43428
  return `Error: Checkpoint manifest is corrupted. Delete .swarm/checkpoints/manifest.json and re-checkpoint.`;
42407
43429
  }
@@ -42411,10 +43433,10 @@ async function handleRollbackCommand(directory, args) {
42411
43433
  return `Error: Checkpoint for phase ${targetPhase} not found. Available phases: ${available}`;
42412
43434
  }
42413
43435
  const checkpointDir = validateSwarmPath(directory, `checkpoints/phase-${targetPhase}`);
42414
- if (!fs18.existsSync(checkpointDir)) {
43436
+ if (!fs20.existsSync(checkpointDir)) {
42415
43437
  return `Error: Checkpoint directory for phase ${targetPhase} does not exist.`;
42416
43438
  }
42417
- const checkpointFiles = fs18.readdirSync(checkpointDir);
43439
+ const checkpointFiles = fs20.readdirSync(checkpointDir);
42418
43440
  if (checkpointFiles.length === 0) {
42419
43441
  return `Error: Checkpoint for phase ${targetPhase} is empty. Cannot rollback.`;
42420
43442
  }
@@ -42422,10 +43444,10 @@ async function handleRollbackCommand(directory, args) {
42422
43444
  const successes = [];
42423
43445
  const failures = [];
42424
43446
  for (const file3 of checkpointFiles) {
42425
- const src = path28.join(checkpointDir, file3);
42426
- const dest = path28.join(swarmDir, file3);
43447
+ const src = path30.join(checkpointDir, file3);
43448
+ const dest = path30.join(swarmDir, file3);
42427
43449
  try {
42428
- fs18.cpSync(src, dest, { recursive: true, force: true });
43450
+ fs20.cpSync(src, dest, { recursive: true, force: true });
42429
43451
  successes.push(file3);
42430
43452
  } catch (error93) {
42431
43453
  failures.push({ file: file3, error: error93.message });
@@ -42442,7 +43464,7 @@ async function handleRollbackCommand(directory, args) {
42442
43464
  timestamp: new Date().toISOString()
42443
43465
  };
42444
43466
  try {
42445
- fs18.appendFileSync(eventsPath, `${JSON.stringify(rollbackEvent)}
43467
+ fs20.appendFileSync(eventsPath, `${JSON.stringify(rollbackEvent)}
42446
43468
  `);
42447
43469
  } catch (error93) {
42448
43470
  console.error("Failed to write rollback event:", error93 instanceof Error ? error93.message : String(error93));
@@ -42485,11 +43507,11 @@ async function handleSimulateCommand(directory, args) {
42485
43507
  ];
42486
43508
  const report = reportLines.filter(Boolean).join(`
42487
43509
  `);
42488
- const fs19 = await import("fs/promises");
42489
- const path29 = await import("path");
42490
- const reportPath = path29.join(directory, ".swarm", "simulate-report.md");
42491
- await fs19.mkdir(path29.dirname(reportPath), { recursive: true });
42492
- await fs19.writeFile(reportPath, report, "utf-8");
43510
+ const fs21 = await import("fs/promises");
43511
+ const path31 = await import("path");
43512
+ const reportPath = path31.join(directory, ".swarm", "simulate-report.md");
43513
+ await fs21.mkdir(path31.dirname(reportPath), { recursive: true });
43514
+ await fs21.writeFile(reportPath, report, "utf-8");
42493
43515
  return `${darkMatterPairs.length} hidden coupling pairs detected`;
42494
43516
  }
42495
43517
 
@@ -42930,6 +43952,18 @@ var COMMAND_REGISTRY = {
42930
43952
  description: "Generate or import a feature specification [description]",
42931
43953
  args: "[description-text]"
42932
43954
  },
43955
+ brainstorm: {
43956
+ handler: (ctx) => handleBrainstormCommand(ctx.directory, ctx.args),
43957
+ description: "Enter architect MODE: BRAINSTORM \u2014 structured seven-phase planning workflow [topic]",
43958
+ args: "[topic-text]",
43959
+ details: "Triggers the architect to run the brainstorm workflow: CONTEXT SCAN, single-question DIALOGUE, APPROACHES, DESIGN SECTIONS, SPEC WRITE + SELF-REVIEW, QA GATE SELECTION, TRANSITION. Use for new plans where requirements need to be drawn out before writing spec.md / plan.md."
43960
+ },
43961
+ "qa-gates": {
43962
+ handler: (ctx) => handleQaGatesCommand(ctx.directory, ctx.args, ctx.sessionID),
43963
+ description: "View or modify QA gate profile for the current plan [enable|override <gate>...]",
43964
+ args: "[show|enable|override] <gate>...",
43965
+ details: "show: display spec-level, session-override, and effective QA gates for the current plan. enable: persist gate(s) into the locked-once profile (architect; rejected after critic approval lock). override: session-only ratchet-tighter enable. Valid gates: reviewer, test_engineer, council_mode, sme_enabled, critic_pre_plan, hallucination_guard, sast_enabled."
43966
+ },
42933
43967
  promote: {
42934
43968
  handler: (ctx) => handlePromoteCommand(ctx.directory, ctx.args),
42935
43969
  description: "Manually promote lesson to hive knowledge",
@@ -43040,18 +44074,18 @@ function resolveCommand(tokens) {
43040
44074
  }
43041
44075
 
43042
44076
  // src/cli/index.ts
43043
- var CONFIG_DIR = path29.join(process.env.XDG_CONFIG_HOME || path29.join(os6.homedir(), ".config"), "opencode");
43044
- var OPENCODE_CONFIG_PATH = path29.join(CONFIG_DIR, "opencode.json");
43045
- var PLUGIN_CONFIG_PATH = path29.join(CONFIG_DIR, "opencode-swarm.json");
43046
- var PROMPTS_DIR = path29.join(CONFIG_DIR, "opencode-swarm");
44077
+ var CONFIG_DIR = path31.join(process.env.XDG_CONFIG_HOME || path31.join(os6.homedir(), ".config"), "opencode");
44078
+ var OPENCODE_CONFIG_PATH = path31.join(CONFIG_DIR, "opencode.json");
44079
+ var PLUGIN_CONFIG_PATH = path31.join(CONFIG_DIR, "opencode-swarm.json");
44080
+ var PROMPTS_DIR = path31.join(CONFIG_DIR, "opencode-swarm");
43047
44081
  function ensureDir(dir) {
43048
- if (!fs19.existsSync(dir)) {
43049
- fs19.mkdirSync(dir, { recursive: true });
44082
+ if (!fs21.existsSync(dir)) {
44083
+ fs21.mkdirSync(dir, { recursive: true });
43050
44084
  }
43051
44085
  }
43052
44086
  function loadJson(filepath) {
43053
44087
  try {
43054
- const content = fs19.readFileSync(filepath, "utf-8");
44088
+ const content = fs21.readFileSync(filepath, "utf-8");
43055
44089
  const stripped = content.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (match, comment) => comment ? "" : match).replace(/,(\s*[}\]])/g, "$1");
43056
44090
  return JSON.parse(stripped);
43057
44091
  } catch {
@@ -43059,7 +44093,7 @@ function loadJson(filepath) {
43059
44093
  }
43060
44094
  }
43061
44095
  function saveJson(filepath, data) {
43062
- fs19.writeFileSync(filepath, `${JSON.stringify(data, null, 2)}
44096
+ fs21.writeFileSync(filepath, `${JSON.stringify(data, null, 2)}
43063
44097
  `, "utf-8");
43064
44098
  }
43065
44099
  async function install() {
@@ -43067,7 +44101,7 @@ async function install() {
43067
44101
  `);
43068
44102
  ensureDir(CONFIG_DIR);
43069
44103
  ensureDir(PROMPTS_DIR);
43070
- const LEGACY_CONFIG_PATH = path29.join(CONFIG_DIR, "config.json");
44104
+ const LEGACY_CONFIG_PATH = path31.join(CONFIG_DIR, "config.json");
43071
44105
  let opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
43072
44106
  if (!opencodeConfig) {
43073
44107
  const legacyConfig = loadJson(LEGACY_CONFIG_PATH);
@@ -43092,7 +44126,7 @@ async function install() {
43092
44126
  saveJson(OPENCODE_CONFIG_PATH, opencodeConfig);
43093
44127
  console.log("\u2713 Added opencode-swarm to OpenCode plugins");
43094
44128
  console.log("\u2713 Disabled default OpenCode agents (explore, general)");
43095
- if (!fs19.existsSync(PLUGIN_CONFIG_PATH)) {
44129
+ if (!fs21.existsSync(PLUGIN_CONFIG_PATH)) {
43096
44130
  const defaultConfig = {
43097
44131
  agents: {
43098
44132
  coder: { model: "opencode/minimax-m2.5-free" },
@@ -43135,7 +44169,7 @@ async function uninstall() {
43135
44169
  `);
43136
44170
  const opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
43137
44171
  if (!opencodeConfig) {
43138
- if (fs19.existsSync(OPENCODE_CONFIG_PATH)) {
44172
+ if (fs21.existsSync(OPENCODE_CONFIG_PATH)) {
43139
44173
  console.log(`\u2717 Could not parse opencode config at: ${OPENCODE_CONFIG_PATH}`);
43140
44174
  return 1;
43141
44175
  } else {
@@ -43167,13 +44201,13 @@ async function uninstall() {
43167
44201
  console.log("\u2713 Re-enabled default OpenCode agents (explore, general)");
43168
44202
  if (process.argv.includes("--clean")) {
43169
44203
  let cleaned = false;
43170
- if (fs19.existsSync(PLUGIN_CONFIG_PATH)) {
43171
- fs19.unlinkSync(PLUGIN_CONFIG_PATH);
44204
+ if (fs21.existsSync(PLUGIN_CONFIG_PATH)) {
44205
+ fs21.unlinkSync(PLUGIN_CONFIG_PATH);
43172
44206
  console.log(`\u2713 Removed plugin config: ${PLUGIN_CONFIG_PATH}`);
43173
44207
  cleaned = true;
43174
44208
  }
43175
- if (fs19.existsSync(PROMPTS_DIR)) {
43176
- fs19.rmSync(PROMPTS_DIR, { recursive: true });
44209
+ if (fs21.existsSync(PROMPTS_DIR)) {
44210
+ fs21.rmSync(PROMPTS_DIR, { recursive: true });
43177
44211
  console.log(`\u2713 Removed custom prompts: ${PROMPTS_DIR}`);
43178
44212
  cleaned = true;
43179
44213
  }