opencode-swarm 6.68.0 → 6.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +844 -162
- package/dist/diff/__tests__/semantic-classifier.test.d.ts +1 -0
- package/dist/diff/__tests__/summary-generator.test.d.ts +1 -0
- package/dist/diff/semantic-classifier.d.ts +55 -0
- package/dist/diff/summary-generator.d.ts +33 -0
- package/dist/index.js +2858 -871
- package/dist/mutation/__tests__/engine.adversarial.test.d.ts +1 -0
- package/dist/mutation/__tests__/engine.test.d.ts +1 -0
- package/dist/mutation/__tests__/equivalence.adversarial.test.d.ts +1 -0
- package/dist/mutation/__tests__/equivalence.test.d.ts +1 -0
- package/dist/mutation/__tests__/gate.adversarial.test.d.ts +1 -0
- package/dist/mutation/__tests__/gate.test.d.ts +1 -0
- package/dist/mutation/engine.d.ts +47 -0
- package/dist/mutation/equivalence.d.ts +35 -0
- package/dist/mutation/gate.d.ts +28 -0
- package/dist/test-impact/__tests__/analyzer-import-fix.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/analyzer-import-fix.test.d.ts +1 -0
- package/dist/test-impact/__tests__/analyzer.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/analyzer.test.d.ts +1 -0
- package/dist/test-impact/__tests__/council-fixes.test.d.ts +1 -0
- package/dist/test-impact/__tests__/failure-classifier.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/failure-classifier.test.d.ts +1 -0
- package/dist/test-impact/__tests__/flaky-detector.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/flaky-detector.test.d.ts +1 -0
- package/dist/test-impact/__tests__/history-store.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/history-store.test.d.ts +1 -0
- package/dist/test-impact/__tests__/test-impact.adversarial.test.d.ts +1 -0
- package/dist/test-impact/__tests__/test-impact.test.d.ts +1 -0
- package/dist/test-impact/analyzer.d.ts +9 -0
- package/dist/test-impact/failure-classifier.d.ts +26 -0
- package/dist/test-impact/flaky-detector.d.ts +14 -0
- package/dist/test-impact/history-store.d.ts +15 -0
- package/dist/tools/__tests__/barrel-exports.test.d.ts +1 -0
- package/dist/tools/__tests__/diff-ast-fallback.test.d.ts +1 -0
- package/dist/tools/__tests__/diff-markdown-summary.test.d.ts +1 -0
- package/dist/tools/__tests__/diff-semantic.test.d.ts +1 -0
- package/dist/tools/__tests__/diff-summary.adversarial.test.d.ts +1 -0
- package/dist/tools/__tests__/diff-summary.test.d.ts +1 -0
- package/dist/tools/__tests__/mutation-test.adversarial.test.d.ts +1 -0
- package/dist/tools/__tests__/mutation-test.sourcefiles.test.d.ts +1 -0
- package/dist/tools/__tests__/mutation-test.test.d.ts +1 -0
- package/dist/tools/__tests__/test-runner-history.test.d.ts +1 -0
- package/dist/tools/__tests__/test-runner-impact.adversarial.test.d.ts +1 -0
- package/dist/tools/__tests__/test-runner-impact.test.d.ts +1 -0
- package/dist/tools/__tests__/test-runner-source-files.test.d.ts +1 -0
- package/dist/tools/diff-summary.d.ts +12 -0
- package/dist/tools/diff.d.ts +3 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/mutation-test.d.ts +2 -0
- package/dist/tools/mutation-test.security.test.d.ts +1 -0
- package/dist/tools/test-impact.d.ts +2 -0
- package/dist/tools/test-runner.d.ts +4 -4
- package/dist/tools/tool-names.d.ts +1 -1
- 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
|
|
18317
|
+
import * as fs21 from "fs";
|
|
18318
18318
|
import * as os6 from "os";
|
|
18319
|
-
import * as
|
|
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",
|
|
@@ -18548,6 +18551,7 @@ var AGENT_TOOL_MAP = {
|
|
|
18548
18551
|
"knowledge_query",
|
|
18549
18552
|
"lint",
|
|
18550
18553
|
"diff",
|
|
18554
|
+
"diff_summary",
|
|
18551
18555
|
"pkg_audit",
|
|
18552
18556
|
"pre_check_batch",
|
|
18553
18557
|
"quality_budget",
|
|
@@ -18559,6 +18563,8 @@ var AGENT_TOOL_MAP = {
|
|
|
18559
18563
|
"secretscan",
|
|
18560
18564
|
"symbols",
|
|
18561
18565
|
"test_runner",
|
|
18566
|
+
"test_impact",
|
|
18567
|
+
"mutation_test",
|
|
18562
18568
|
"todo_extract",
|
|
18563
18569
|
"update_task_status",
|
|
18564
18570
|
"lint_spec",
|
|
@@ -18615,6 +18621,8 @@ var AGENT_TOOL_MAP = {
|
|
|
18615
18621
|
],
|
|
18616
18622
|
test_engineer: [
|
|
18617
18623
|
"test_runner",
|
|
18624
|
+
"test_impact",
|
|
18625
|
+
"mutation_test",
|
|
18618
18626
|
"diff",
|
|
18619
18627
|
"symbols",
|
|
18620
18628
|
"extract_code_blocks",
|
|
@@ -18638,6 +18646,7 @@ var AGENT_TOOL_MAP = {
|
|
|
18638
18646
|
],
|
|
18639
18647
|
reviewer: [
|
|
18640
18648
|
"diff",
|
|
18649
|
+
"diff_summary",
|
|
18641
18650
|
"imports",
|
|
18642
18651
|
"lint",
|
|
18643
18652
|
"pkg_audit",
|
|
@@ -18648,6 +18657,7 @@ var AGENT_TOOL_MAP = {
|
|
|
18648
18657
|
"retrieve_summary",
|
|
18649
18658
|
"extract_code_blocks",
|
|
18650
18659
|
"test_runner",
|
|
18660
|
+
"test_impact",
|
|
18651
18661
|
"sast_scan",
|
|
18652
18662
|
"placeholder_scan",
|
|
18653
18663
|
"knowledge_recall",
|
|
@@ -38471,8 +38481,8 @@ async function handlePlanCommand(directory, args) {
|
|
|
38471
38481
|
// src/services/preflight-service.ts
|
|
38472
38482
|
init_manager2();
|
|
38473
38483
|
init_manager();
|
|
38474
|
-
import * as
|
|
38475
|
-
import * as
|
|
38484
|
+
import * as fs16 from "fs";
|
|
38485
|
+
import * as path26 from "path";
|
|
38476
38486
|
|
|
38477
38487
|
// src/tools/lint.ts
|
|
38478
38488
|
import * as fs10 from "fs";
|
|
@@ -39607,12 +39617,577 @@ async function runSecretscan(directory) {
|
|
|
39607
39617
|
}
|
|
39608
39618
|
|
|
39609
39619
|
// src/tools/test-runner.ts
|
|
39610
|
-
import * as
|
|
39611
|
-
import * as
|
|
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
|
+
}
|
|
39612
40187
|
|
|
39613
40188
|
// src/tools/resolve-working-directory.ts
|
|
39614
|
-
import * as
|
|
39615
|
-
import * as
|
|
40189
|
+
import * as fs14 from "fs";
|
|
40190
|
+
import * as path24 from "path";
|
|
39616
40191
|
function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
|
|
39617
40192
|
if (workingDirectory == null || workingDirectory === "") {
|
|
39618
40193
|
return { success: true, directory: fallbackDirectory };
|
|
@@ -39632,17 +40207,17 @@ function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
|
|
|
39632
40207
|
};
|
|
39633
40208
|
}
|
|
39634
40209
|
}
|
|
39635
|
-
const normalizedDir =
|
|
39636
|
-
const pathParts = normalizedDir.split(
|
|
40210
|
+
const normalizedDir = path24.normalize(workingDirectory);
|
|
40211
|
+
const pathParts = normalizedDir.split(path24.sep);
|
|
39637
40212
|
if (pathParts.includes("..")) {
|
|
39638
40213
|
return {
|
|
39639
40214
|
success: false,
|
|
39640
40215
|
message: "Invalid working_directory: path traversal sequences (..) are not allowed"
|
|
39641
40216
|
};
|
|
39642
40217
|
}
|
|
39643
|
-
const resolvedDir =
|
|
40218
|
+
const resolvedDir = path24.resolve(normalizedDir);
|
|
39644
40219
|
try {
|
|
39645
|
-
const realPath =
|
|
40220
|
+
const realPath = fs14.realpathSync(resolvedDir);
|
|
39646
40221
|
return { success: true, directory: realPath };
|
|
39647
40222
|
} catch {
|
|
39648
40223
|
return {
|
|
@@ -39678,7 +40253,7 @@ function validateArgs2(args) {
|
|
|
39678
40253
|
return false;
|
|
39679
40254
|
const obj = args;
|
|
39680
40255
|
if (obj.scope !== undefined) {
|
|
39681
|
-
if (obj.scope !== "all" && obj.scope !== "convention" && obj.scope !== "graph") {
|
|
40256
|
+
if (obj.scope !== "all" && obj.scope !== "convention" && obj.scope !== "graph" && obj.scope !== "impact") {
|
|
39682
40257
|
return false;
|
|
39683
40258
|
}
|
|
39684
40259
|
}
|
|
@@ -39723,19 +40298,19 @@ function hasDevDependency(devDeps, ...patterns) {
|
|
|
39723
40298
|
return hasPackageJsonDependency(devDeps, ...patterns);
|
|
39724
40299
|
}
|
|
39725
40300
|
function detectGoTest(cwd) {
|
|
39726
|
-
return
|
|
40301
|
+
return fs15.existsSync(path25.join(cwd, "go.mod")) && isCommandAvailable("go");
|
|
39727
40302
|
}
|
|
39728
40303
|
function detectJavaMaven(cwd) {
|
|
39729
|
-
return
|
|
40304
|
+
return fs15.existsSync(path25.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
|
|
39730
40305
|
}
|
|
39731
40306
|
function detectGradle(cwd) {
|
|
39732
|
-
const hasBuildFile =
|
|
39733
|
-
const hasGradlew =
|
|
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"));
|
|
39734
40309
|
return hasBuildFile && (hasGradlew || isCommandAvailable("gradle"));
|
|
39735
40310
|
}
|
|
39736
40311
|
function detectDotnetTest(cwd) {
|
|
39737
40312
|
try {
|
|
39738
|
-
const files =
|
|
40313
|
+
const files = fs15.readdirSync(cwd);
|
|
39739
40314
|
const hasCsproj = files.some((f) => f.endsWith(".csproj"));
|
|
39740
40315
|
return hasCsproj && isCommandAvailable("dotnet");
|
|
39741
40316
|
} catch {
|
|
@@ -39743,32 +40318,32 @@ function detectDotnetTest(cwd) {
|
|
|
39743
40318
|
}
|
|
39744
40319
|
}
|
|
39745
40320
|
function detectCTest(cwd) {
|
|
39746
|
-
const hasSource =
|
|
39747
|
-
const hasBuildCache =
|
|
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"));
|
|
39748
40323
|
return (hasSource || hasBuildCache) && isCommandAvailable("ctest");
|
|
39749
40324
|
}
|
|
39750
40325
|
function detectSwiftTest(cwd) {
|
|
39751
|
-
return
|
|
40326
|
+
return fs15.existsSync(path25.join(cwd, "Package.swift")) && isCommandAvailable("swift");
|
|
39752
40327
|
}
|
|
39753
40328
|
function detectDartTest(cwd) {
|
|
39754
|
-
return
|
|
40329
|
+
return fs15.existsSync(path25.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
|
|
39755
40330
|
}
|
|
39756
40331
|
function detectRSpec(cwd) {
|
|
39757
|
-
const hasRSpecFile =
|
|
39758
|
-
const hasGemfile =
|
|
39759
|
-
const hasSpecDir =
|
|
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"));
|
|
39760
40335
|
const hasRSpec = hasRSpecFile || hasGemfile && hasSpecDir;
|
|
39761
40336
|
return hasRSpec && (isCommandAvailable("bundle") || isCommandAvailable("rspec"));
|
|
39762
40337
|
}
|
|
39763
40338
|
function detectMinitest(cwd) {
|
|
39764
|
-
return
|
|
40339
|
+
return fs15.existsSync(path25.join(cwd, "test")) && (fs15.existsSync(path25.join(cwd, "Gemfile")) || fs15.existsSync(path25.join(cwd, "Rakefile"))) && isCommandAvailable("ruby");
|
|
39765
40340
|
}
|
|
39766
40341
|
async function detectTestFramework(cwd) {
|
|
39767
40342
|
const baseDir = cwd;
|
|
39768
40343
|
try {
|
|
39769
|
-
const packageJsonPath =
|
|
39770
|
-
if (
|
|
39771
|
-
const content =
|
|
40344
|
+
const packageJsonPath = path25.join(baseDir, "package.json");
|
|
40345
|
+
if (fs15.existsSync(packageJsonPath)) {
|
|
40346
|
+
const content = fs15.readFileSync(packageJsonPath, "utf-8");
|
|
39772
40347
|
const pkg = JSON.parse(content);
|
|
39773
40348
|
const _deps = pkg.dependencies || {};
|
|
39774
40349
|
const devDeps = pkg.devDependencies || {};
|
|
@@ -39787,38 +40362,38 @@ async function detectTestFramework(cwd) {
|
|
|
39787
40362
|
return "jest";
|
|
39788
40363
|
if (hasDevDependency(devDeps, "mocha", "@types/mocha"))
|
|
39789
40364
|
return "mocha";
|
|
39790
|
-
if (
|
|
40365
|
+
if (fs15.existsSync(path25.join(baseDir, "bun.lockb")) || fs15.existsSync(path25.join(baseDir, "bun.lock"))) {
|
|
39791
40366
|
if (scripts.test?.includes("bun"))
|
|
39792
40367
|
return "bun";
|
|
39793
40368
|
}
|
|
39794
40369
|
}
|
|
39795
40370
|
} catch {}
|
|
39796
40371
|
try {
|
|
39797
|
-
const pyprojectTomlPath =
|
|
39798
|
-
const setupCfgPath =
|
|
39799
|
-
const requirementsTxtPath =
|
|
39800
|
-
if (
|
|
39801
|
-
const content =
|
|
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");
|
|
39802
40377
|
if (content.includes("[tool.pytest"))
|
|
39803
40378
|
return "pytest";
|
|
39804
40379
|
if (content.includes("pytest"))
|
|
39805
40380
|
return "pytest";
|
|
39806
40381
|
}
|
|
39807
|
-
if (
|
|
39808
|
-
const content =
|
|
40382
|
+
if (fs15.existsSync(setupCfgPath)) {
|
|
40383
|
+
const content = fs15.readFileSync(setupCfgPath, "utf-8");
|
|
39809
40384
|
if (content.includes("[pytest]"))
|
|
39810
40385
|
return "pytest";
|
|
39811
40386
|
}
|
|
39812
|
-
if (
|
|
39813
|
-
const content =
|
|
40387
|
+
if (fs15.existsSync(requirementsTxtPath)) {
|
|
40388
|
+
const content = fs15.readFileSync(requirementsTxtPath, "utf-8");
|
|
39814
40389
|
if (content.includes("pytest"))
|
|
39815
40390
|
return "pytest";
|
|
39816
40391
|
}
|
|
39817
40392
|
} catch {}
|
|
39818
40393
|
try {
|
|
39819
|
-
const cargoTomlPath =
|
|
39820
|
-
if (
|
|
39821
|
-
const content =
|
|
40394
|
+
const cargoTomlPath = path25.join(baseDir, "Cargo.toml");
|
|
40395
|
+
if (fs15.existsSync(cargoTomlPath)) {
|
|
40396
|
+
const content = fs15.readFileSync(cargoTomlPath, "utf-8");
|
|
39822
40397
|
if (content.includes("[dev-dependencies]")) {
|
|
39823
40398
|
if (content.includes("tokio") || content.includes("mockall") || content.includes("pretty_assertions")) {
|
|
39824
40399
|
return "cargo";
|
|
@@ -39827,10 +40402,10 @@ async function detectTestFramework(cwd) {
|
|
|
39827
40402
|
}
|
|
39828
40403
|
} catch {}
|
|
39829
40404
|
try {
|
|
39830
|
-
const pesterConfigPath =
|
|
39831
|
-
const pesterConfigJsonPath =
|
|
39832
|
-
const pesterPs1Path =
|
|
39833
|
-
if (
|
|
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)) {
|
|
39834
40409
|
return "pester";
|
|
39835
40410
|
}
|
|
39836
40411
|
} catch {}
|
|
@@ -39881,8 +40456,8 @@ function getTestFilesFromConvention(sourceFiles) {
|
|
|
39881
40456
|
const testFiles = [];
|
|
39882
40457
|
for (const file3 of sourceFiles) {
|
|
39883
40458
|
const normalizedPath = file3.replace(/\\/g, "/");
|
|
39884
|
-
const basename4 =
|
|
39885
|
-
const dirname10 =
|
|
40459
|
+
const basename4 = path25.basename(file3);
|
|
40460
|
+
const dirname10 = path25.dirname(file3);
|
|
39886
40461
|
if (hasCompoundTestExtension(basename4) || basename4.includes(".spec.") || basename4.includes(".test.") || normalizedPath.includes("/__tests__/") || normalizedPath.includes("/tests/") || normalizedPath.includes("/test/")) {
|
|
39887
40462
|
if (!testFiles.includes(file3)) {
|
|
39888
40463
|
testFiles.push(file3);
|
|
@@ -39891,16 +40466,16 @@ function getTestFilesFromConvention(sourceFiles) {
|
|
|
39891
40466
|
}
|
|
39892
40467
|
for (const _pattern of TEST_PATTERNS) {
|
|
39893
40468
|
const nameWithoutExt = basename4.replace(/\.[^.]+$/, "");
|
|
39894
|
-
const ext =
|
|
40469
|
+
const ext = path25.extname(basename4);
|
|
39895
40470
|
const possibleTestFiles = [
|
|
39896
|
-
|
|
39897
|
-
|
|
39898
|
-
|
|
39899
|
-
|
|
39900
|
-
|
|
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}`)
|
|
39901
40476
|
];
|
|
39902
40477
|
for (const testFile of possibleTestFiles) {
|
|
39903
|
-
if (
|
|
40478
|
+
if (fs15.existsSync(testFile) && !testFiles.includes(testFile)) {
|
|
39904
40479
|
testFiles.push(testFile);
|
|
39905
40480
|
}
|
|
39906
40481
|
}
|
|
@@ -39916,8 +40491,8 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39916
40491
|
}
|
|
39917
40492
|
for (const testFile of candidateTestFiles) {
|
|
39918
40493
|
try {
|
|
39919
|
-
const content =
|
|
39920
|
-
const testDir =
|
|
40494
|
+
const content = fs15.readFileSync(testFile, "utf-8");
|
|
40495
|
+
const testDir = path25.dirname(testFile);
|
|
39921
40496
|
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
39922
40497
|
let match;
|
|
39923
40498
|
match = importRegex.exec(content);
|
|
@@ -39925,8 +40500,8 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39925
40500
|
const importPath = match[1];
|
|
39926
40501
|
let resolvedImport;
|
|
39927
40502
|
if (importPath.startsWith(".")) {
|
|
39928
|
-
resolvedImport =
|
|
39929
|
-
const existingExt =
|
|
40503
|
+
resolvedImport = path25.resolve(testDir, importPath);
|
|
40504
|
+
const existingExt = path25.extname(resolvedImport);
|
|
39930
40505
|
if (!existingExt) {
|
|
39931
40506
|
for (const extToTry of [
|
|
39932
40507
|
".ts",
|
|
@@ -39937,7 +40512,7 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39937
40512
|
".cjs"
|
|
39938
40513
|
]) {
|
|
39939
40514
|
const withExt = resolvedImport + extToTry;
|
|
39940
|
-
if (sourceFiles.includes(withExt) ||
|
|
40515
|
+
if (sourceFiles.includes(withExt) || fs15.existsSync(withExt)) {
|
|
39941
40516
|
resolvedImport = withExt;
|
|
39942
40517
|
break;
|
|
39943
40518
|
}
|
|
@@ -39946,12 +40521,12 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39946
40521
|
} else {
|
|
39947
40522
|
continue;
|
|
39948
40523
|
}
|
|
39949
|
-
const importBasename =
|
|
39950
|
-
const importDir =
|
|
40524
|
+
const importBasename = path25.basename(resolvedImport, path25.extname(resolvedImport));
|
|
40525
|
+
const importDir = path25.dirname(resolvedImport);
|
|
39951
40526
|
for (const sourceFile of sourceFiles) {
|
|
39952
|
-
const sourceDir =
|
|
39953
|
-
const sourceBasename =
|
|
39954
|
-
const isRelatedDir = importDir === sourceDir || importDir ===
|
|
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");
|
|
39955
40530
|
if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
|
|
39956
40531
|
if (!testFiles.includes(testFile)) {
|
|
39957
40532
|
testFiles.push(testFile);
|
|
@@ -39966,8 +40541,8 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39966
40541
|
while (match !== null) {
|
|
39967
40542
|
const importPath = match[1];
|
|
39968
40543
|
if (importPath.startsWith(".")) {
|
|
39969
|
-
let resolvedImport =
|
|
39970
|
-
const existingExt =
|
|
40544
|
+
let resolvedImport = path25.resolve(testDir, importPath);
|
|
40545
|
+
const existingExt = path25.extname(resolvedImport);
|
|
39971
40546
|
if (!existingExt) {
|
|
39972
40547
|
for (const extToTry of [
|
|
39973
40548
|
".ts",
|
|
@@ -39978,18 +40553,18 @@ async function getTestFilesFromGraph(sourceFiles) {
|
|
|
39978
40553
|
".cjs"
|
|
39979
40554
|
]) {
|
|
39980
40555
|
const withExt = resolvedImport + extToTry;
|
|
39981
|
-
if (sourceFiles.includes(withExt) ||
|
|
40556
|
+
if (sourceFiles.includes(withExt) || fs15.existsSync(withExt)) {
|
|
39982
40557
|
resolvedImport = withExt;
|
|
39983
40558
|
break;
|
|
39984
40559
|
}
|
|
39985
40560
|
}
|
|
39986
40561
|
}
|
|
39987
|
-
const importDir =
|
|
39988
|
-
const importBasename =
|
|
40562
|
+
const importDir = path25.dirname(resolvedImport);
|
|
40563
|
+
const importBasename = path25.basename(resolvedImport, path25.extname(resolvedImport));
|
|
39989
40564
|
for (const sourceFile of sourceFiles) {
|
|
39990
|
-
const sourceDir =
|
|
39991
|
-
const sourceBasename =
|
|
39992
|
-
const isRelatedDir = importDir === sourceDir || importDir ===
|
|
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");
|
|
39993
40568
|
if (resolvedImport === sourceFile || importBasename === sourceBasename && isRelatedDir) {
|
|
39994
40569
|
if (!testFiles.includes(testFile)) {
|
|
39995
40570
|
testFiles.push(testFile);
|
|
@@ -40074,8 +40649,8 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
|
|
|
40074
40649
|
return ["mvn", "test"];
|
|
40075
40650
|
case "gradle": {
|
|
40076
40651
|
const isWindows = process.platform === "win32";
|
|
40077
|
-
const hasGradlewBat =
|
|
40078
|
-
const hasGradlew =
|
|
40652
|
+
const hasGradlewBat = fs15.existsSync(path25.join(baseDir, "gradlew.bat"));
|
|
40653
|
+
const hasGradlew = fs15.existsSync(path25.join(baseDir, "gradlew"));
|
|
40079
40654
|
if (hasGradlewBat && isWindows)
|
|
40080
40655
|
return ["gradlew.bat", "test"];
|
|
40081
40656
|
if (hasGradlew)
|
|
@@ -40092,7 +40667,7 @@ function buildTestCommand(framework, scope, files, coverage, baseDir) {
|
|
|
40092
40667
|
"cmake-build-release",
|
|
40093
40668
|
"out"
|
|
40094
40669
|
];
|
|
40095
|
-
const actualBuildDir = buildDirCandidates.find((d) =>
|
|
40670
|
+
const actualBuildDir = buildDirCandidates.find((d) => fs15.existsSync(path25.join(baseDir, d, "CMakeCache.txt"))) ?? "build";
|
|
40096
40671
|
return ["ctest", "--test-dir", actualBuildDir];
|
|
40097
40672
|
}
|
|
40098
40673
|
case "swift-test":
|
|
@@ -40518,10 +41093,57 @@ var SKIP_DIRECTORIES = new Set([
|
|
|
40518
41093
|
".bundle",
|
|
40519
41094
|
".tox"
|
|
40520
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
|
+
}
|
|
40521
41143
|
var test_runner = createSwarmTool({
|
|
40522
|
-
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,
|
|
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.',
|
|
40523
41145
|
args: {
|
|
40524
|
-
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'),
|
|
40525
41147
|
files: tool.schema.array(tool.schema.string()).optional().describe("Specific files to test (used with convention or graph scope)"),
|
|
40526
41148
|
coverage: tool.schema.boolean().optional().describe("Enable coverage reporting if supported"),
|
|
40527
41149
|
timeout_ms: tool.schema.number().optional().describe("Timeout in milliseconds (default 60000, max 300000)"),
|
|
@@ -40592,7 +41214,7 @@ var test_runner = createSwarmTool({
|
|
|
40592
41214
|
framework: "none",
|
|
40593
41215
|
scope: "all",
|
|
40594
41216
|
error: "Invalid arguments",
|
|
40595
|
-
message: 'scope must be "all", "convention", or "
|
|
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',
|
|
40596
41218
|
outcome: "error"
|
|
40597
41219
|
};
|
|
40598
41220
|
return JSON.stringify(errorResult, null, 2);
|
|
@@ -40611,7 +41233,7 @@ var test_runner = createSwarmTool({
|
|
|
40611
41233
|
return JSON.stringify(errorResult, null, 2);
|
|
40612
41234
|
}
|
|
40613
41235
|
}
|
|
40614
|
-
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)) {
|
|
40615
41237
|
const errorResult = {
|
|
40616
41238
|
success: false,
|
|
40617
41239
|
framework: "none",
|
|
@@ -40648,7 +41270,7 @@ var test_runner = createSwarmTool({
|
|
|
40648
41270
|
let effectiveScope = scope;
|
|
40649
41271
|
if (scope === "all") {} else if (scope === "convention") {
|
|
40650
41272
|
const sourceFiles = args.files.filter((f) => {
|
|
40651
|
-
const ext =
|
|
41273
|
+
const ext = path25.extname(f).toLowerCase();
|
|
40652
41274
|
return SOURCE_EXTENSIONS.has(ext);
|
|
40653
41275
|
});
|
|
40654
41276
|
if (sourceFiles.length === 0) {
|
|
@@ -40665,7 +41287,7 @@ var test_runner = createSwarmTool({
|
|
|
40665
41287
|
testFiles = getTestFilesFromConvention(sourceFiles);
|
|
40666
41288
|
} else if (scope === "graph") {
|
|
40667
41289
|
const sourceFiles = args.files.filter((f) => {
|
|
40668
|
-
const ext =
|
|
41290
|
+
const ext = path25.extname(f).toLowerCase();
|
|
40669
41291
|
return SOURCE_EXTENSIONS.has(ext);
|
|
40670
41292
|
});
|
|
40671
41293
|
if (sourceFiles.length === 0) {
|
|
@@ -40687,6 +41309,53 @@ var test_runner = createSwarmTool({
|
|
|
40687
41309
|
effectiveScope = "convention";
|
|
40688
41310
|
testFiles = getTestFilesFromConvention(sourceFiles);
|
|
40689
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
|
+
}
|
|
40690
41359
|
}
|
|
40691
41360
|
if (scope !== "all" && testFiles.length === 0) {
|
|
40692
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/).";
|
|
@@ -40697,7 +41366,8 @@ var test_runner = createSwarmTool({
|
|
|
40697
41366
|
error: "Provided source files resolved to zero test files",
|
|
40698
41367
|
message: graphFallbackReason ? `${baseMessage} (${graphFallbackReason})` : baseMessage,
|
|
40699
41368
|
outcome: "skip",
|
|
40700
|
-
...scope === "graph" && { attempted_scope: "graph" }
|
|
41369
|
+
...scope === "graph" && { attempted_scope: "graph" },
|
|
41370
|
+
...scope === "impact" && { attempted_scope: "graph" }
|
|
40701
41371
|
};
|
|
40702
41372
|
return JSON.stringify(errorResult, null, 2);
|
|
40703
41373
|
}
|
|
@@ -40714,6 +41384,18 @@ var test_runner = createSwarmTool({
|
|
|
40714
41384
|
return JSON.stringify(errorResult, null, 2);
|
|
40715
41385
|
}
|
|
40716
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
|
+
}
|
|
40717
41399
|
if (graphFallbackReason && result.message) {
|
|
40718
41400
|
result.message = `${result.message} (${graphFallbackReason})`;
|
|
40719
41401
|
}
|
|
@@ -40741,8 +41423,8 @@ function validateDirectoryPath(dir) {
|
|
|
40741
41423
|
if (dir.includes("..")) {
|
|
40742
41424
|
throw new Error("Directory path must not contain path traversal sequences");
|
|
40743
41425
|
}
|
|
40744
|
-
const normalized =
|
|
40745
|
-
const absolutePath =
|
|
41426
|
+
const normalized = path26.normalize(dir);
|
|
41427
|
+
const absolutePath = path26.isAbsolute(normalized) ? normalized : path26.resolve(normalized);
|
|
40746
41428
|
return absolutePath;
|
|
40747
41429
|
}
|
|
40748
41430
|
function validateTimeout(timeoutMs, defaultValue) {
|
|
@@ -40765,9 +41447,9 @@ function validateTimeout(timeoutMs, defaultValue) {
|
|
|
40765
41447
|
}
|
|
40766
41448
|
function getPackageVersion(dir) {
|
|
40767
41449
|
try {
|
|
40768
|
-
const packagePath =
|
|
40769
|
-
if (
|
|
40770
|
-
const content =
|
|
41450
|
+
const packagePath = path26.join(dir, "package.json");
|
|
41451
|
+
if (fs16.existsSync(packagePath)) {
|
|
41452
|
+
const content = fs16.readFileSync(packagePath, "utf-8");
|
|
40771
41453
|
const pkg = JSON.parse(content);
|
|
40772
41454
|
return pkg.version ?? null;
|
|
40773
41455
|
}
|
|
@@ -40776,9 +41458,9 @@ function getPackageVersion(dir) {
|
|
|
40776
41458
|
}
|
|
40777
41459
|
function getChangelogVersion(dir) {
|
|
40778
41460
|
try {
|
|
40779
|
-
const changelogPath =
|
|
40780
|
-
if (
|
|
40781
|
-
const content =
|
|
41461
|
+
const changelogPath = path26.join(dir, "CHANGELOG.md");
|
|
41462
|
+
if (fs16.existsSync(changelogPath)) {
|
|
41463
|
+
const content = fs16.readFileSync(changelogPath, "utf-8");
|
|
40782
41464
|
const match = content.match(/^##\s*\[?(\d+\.\d+\.\d+)\]?/m);
|
|
40783
41465
|
if (match) {
|
|
40784
41466
|
return match[1];
|
|
@@ -40790,10 +41472,10 @@ function getChangelogVersion(dir) {
|
|
|
40790
41472
|
function getVersionFileVersion(dir) {
|
|
40791
41473
|
const possibleFiles = ["VERSION.txt", "version.txt", "VERSION", "version"];
|
|
40792
41474
|
for (const file3 of possibleFiles) {
|
|
40793
|
-
const filePath =
|
|
40794
|
-
if (
|
|
41475
|
+
const filePath = path26.join(dir, file3);
|
|
41476
|
+
if (fs16.existsSync(filePath)) {
|
|
40795
41477
|
try {
|
|
40796
|
-
const content =
|
|
41478
|
+
const content = fs16.readFileSync(filePath, "utf-8").trim();
|
|
40797
41479
|
const match = content.match(/(\d+\.\d+\.\d+)/);
|
|
40798
41480
|
if (match) {
|
|
40799
41481
|
return match[1];
|
|
@@ -41117,8 +41799,8 @@ async function runEvidenceCheck(dir) {
|
|
|
41117
41799
|
async function runRequirementCoverageCheck(dir, currentPhase) {
|
|
41118
41800
|
const startTime = Date.now();
|
|
41119
41801
|
try {
|
|
41120
|
-
const specPath =
|
|
41121
|
-
if (!
|
|
41802
|
+
const specPath = path26.join(dir, ".swarm", "spec.md");
|
|
41803
|
+
if (!fs16.existsSync(specPath)) {
|
|
41122
41804
|
return {
|
|
41123
41805
|
type: "req_coverage",
|
|
41124
41806
|
status: "skip",
|
|
@@ -41331,9 +42013,9 @@ async function handlePreflightCommand(directory, _args) {
|
|
|
41331
42013
|
return formatPreflightMarkdown(report);
|
|
41332
42014
|
}
|
|
41333
42015
|
// src/knowledge/hive-promoter.ts
|
|
41334
|
-
import * as
|
|
42016
|
+
import * as fs17 from "fs";
|
|
41335
42017
|
import * as os5 from "os";
|
|
41336
|
-
import * as
|
|
42018
|
+
import * as path27 from "path";
|
|
41337
42019
|
var DANGEROUS_PATTERNS = [
|
|
41338
42020
|
[/rm\s+-rf/, "rm\\s+-rf"],
|
|
41339
42021
|
[/:\s*!\s*\|/, ":\\s*!\\s*\\|"],
|
|
@@ -41379,13 +42061,13 @@ function getHiveFilePath() {
|
|
|
41379
42061
|
const home = os5.homedir();
|
|
41380
42062
|
let dataDir;
|
|
41381
42063
|
if (platform === "win32") {
|
|
41382
|
-
dataDir =
|
|
42064
|
+
dataDir = path27.join(process.env.LOCALAPPDATA || path27.join(home, "AppData", "Local"), "opencode-swarm", "Data");
|
|
41383
42065
|
} else if (platform === "darwin") {
|
|
41384
|
-
dataDir =
|
|
42066
|
+
dataDir = path27.join(home, "Library", "Application Support", "opencode-swarm");
|
|
41385
42067
|
} else {
|
|
41386
|
-
dataDir =
|
|
42068
|
+
dataDir = path27.join(process.env.XDG_DATA_HOME || path27.join(home, ".local", "share"), "opencode-swarm");
|
|
41387
42069
|
}
|
|
41388
|
-
return
|
|
42070
|
+
return path27.join(dataDir, "hive-knowledge.jsonl");
|
|
41389
42071
|
}
|
|
41390
42072
|
async function promoteToHive(_directory, lesson, category) {
|
|
41391
42073
|
const trimmed = (lesson ?? "").trim();
|
|
@@ -41397,9 +42079,9 @@ async function promoteToHive(_directory, lesson, category) {
|
|
|
41397
42079
|
throw new Error(`Lesson rejected by validator: ${validation.reason}`);
|
|
41398
42080
|
}
|
|
41399
42081
|
const hivePath = getHiveFilePath();
|
|
41400
|
-
const hiveDir =
|
|
41401
|
-
if (!
|
|
41402
|
-
|
|
42082
|
+
const hiveDir = path27.dirname(hivePath);
|
|
42083
|
+
if (!fs17.existsSync(hiveDir)) {
|
|
42084
|
+
fs17.mkdirSync(hiveDir, { recursive: true });
|
|
41403
42085
|
}
|
|
41404
42086
|
const now = new Date;
|
|
41405
42087
|
const entry = {
|
|
@@ -41413,16 +42095,16 @@ async function promoteToHive(_directory, lesson, category) {
|
|
|
41413
42095
|
promotedAt: now.toISOString(),
|
|
41414
42096
|
retrievalOutcomes: { applied: 0, succeededAfter: 0, failedAfter: 0 }
|
|
41415
42097
|
};
|
|
41416
|
-
|
|
42098
|
+
fs17.appendFileSync(hivePath, `${JSON.stringify(entry)}
|
|
41417
42099
|
`, "utf-8");
|
|
41418
42100
|
const preview = `${trimmed.slice(0, 50)}${trimmed.length > 50 ? "..." : ""}`;
|
|
41419
42101
|
return `Promoted to hive: "${preview}" (confidence: 1.0, source: manual)`;
|
|
41420
42102
|
}
|
|
41421
42103
|
async function promoteFromSwarm(directory, lessonId) {
|
|
41422
|
-
const knowledgePath =
|
|
42104
|
+
const knowledgePath = path27.join(directory, ".swarm", "knowledge.jsonl");
|
|
41423
42105
|
const entries = [];
|
|
41424
|
-
if (
|
|
41425
|
-
const content =
|
|
42106
|
+
if (fs17.existsSync(knowledgePath)) {
|
|
42107
|
+
const content = fs17.readFileSync(knowledgePath, "utf-8");
|
|
41426
42108
|
for (const line of content.split(`
|
|
41427
42109
|
`)) {
|
|
41428
42110
|
const t = line.trim();
|
|
@@ -41446,9 +42128,9 @@ async function promoteFromSwarm(directory, lessonId) {
|
|
|
41446
42128
|
throw new Error(`Lesson rejected by validator: ${validation.reason}`);
|
|
41447
42129
|
}
|
|
41448
42130
|
const hivePath = getHiveFilePath();
|
|
41449
|
-
const hiveDir =
|
|
41450
|
-
if (!
|
|
41451
|
-
|
|
42131
|
+
const hiveDir = path27.dirname(hivePath);
|
|
42132
|
+
if (!fs17.existsSync(hiveDir)) {
|
|
42133
|
+
fs17.mkdirSync(hiveDir, { recursive: true });
|
|
41452
42134
|
}
|
|
41453
42135
|
const now = new Date;
|
|
41454
42136
|
const hiveEntry = {
|
|
@@ -41462,7 +42144,7 @@ async function promoteFromSwarm(directory, lessonId) {
|
|
|
41462
42144
|
promotedAt: now.toISOString(),
|
|
41463
42145
|
retrievalOutcomes: { applied: 0, succeededAfter: 0, failedAfter: 0 }
|
|
41464
42146
|
};
|
|
41465
|
-
|
|
42147
|
+
fs17.appendFileSync(hivePath, `${JSON.stringify(hiveEntry)}
|
|
41466
42148
|
`, "utf-8");
|
|
41467
42149
|
const preview = `${lessonText.slice(0, 50)}${lessonText.length > 50 ? "..." : ""}`;
|
|
41468
42150
|
return `Promoted to hive: "${preview}" (confidence: 1.0, source: manual)`;
|
|
@@ -41834,7 +42516,7 @@ async function handleQaGatesCommand(directory, args, sessionID) {
|
|
|
41834
42516
|
}
|
|
41835
42517
|
|
|
41836
42518
|
// src/commands/reset.ts
|
|
41837
|
-
import * as
|
|
42519
|
+
import * as fs18 from "fs";
|
|
41838
42520
|
|
|
41839
42521
|
// src/background/manager.ts
|
|
41840
42522
|
init_utils();
|
|
@@ -42535,8 +43217,8 @@ async function handleResetCommand(directory, args) {
|
|
|
42535
43217
|
for (const filename of filesToReset) {
|
|
42536
43218
|
try {
|
|
42537
43219
|
const resolvedPath = validateSwarmPath(directory, filename);
|
|
42538
|
-
if (
|
|
42539
|
-
|
|
43220
|
+
if (fs18.existsSync(resolvedPath)) {
|
|
43221
|
+
fs18.unlinkSync(resolvedPath);
|
|
42540
43222
|
results.push(`- \u2705 Deleted ${filename}`);
|
|
42541
43223
|
} else {
|
|
42542
43224
|
results.push(`- \u23ED\uFE0F ${filename} not found (skipped)`);
|
|
@@ -42553,8 +43235,8 @@ async function handleResetCommand(directory, args) {
|
|
|
42553
43235
|
}
|
|
42554
43236
|
try {
|
|
42555
43237
|
const summariesPath = validateSwarmPath(directory, "summaries");
|
|
42556
|
-
if (
|
|
42557
|
-
|
|
43238
|
+
if (fs18.existsSync(summariesPath)) {
|
|
43239
|
+
fs18.rmSync(summariesPath, { recursive: true, force: true });
|
|
42558
43240
|
results.push("- \u2705 Deleted summaries/ directory");
|
|
42559
43241
|
} else {
|
|
42560
43242
|
results.push("- \u23ED\uFE0F summaries/ not found (skipped)");
|
|
@@ -42574,14 +43256,14 @@ async function handleResetCommand(directory, args) {
|
|
|
42574
43256
|
|
|
42575
43257
|
// src/commands/reset-session.ts
|
|
42576
43258
|
init_utils2();
|
|
42577
|
-
import * as
|
|
42578
|
-
import * as
|
|
43259
|
+
import * as fs19 from "fs";
|
|
43260
|
+
import * as path28 from "path";
|
|
42579
43261
|
async function handleResetSessionCommand(directory, _args) {
|
|
42580
43262
|
const results = [];
|
|
42581
43263
|
try {
|
|
42582
43264
|
const statePath = validateSwarmPath(directory, "session/state.json");
|
|
42583
|
-
if (
|
|
42584
|
-
|
|
43265
|
+
if (fs19.existsSync(statePath)) {
|
|
43266
|
+
fs19.unlinkSync(statePath);
|
|
42585
43267
|
results.push("\u2705 Deleted .swarm/session/state.json");
|
|
42586
43268
|
} else {
|
|
42587
43269
|
results.push("\u23ED\uFE0F state.json not found (already clean)");
|
|
@@ -42590,15 +43272,15 @@ async function handleResetSessionCommand(directory, _args) {
|
|
|
42590
43272
|
results.push("\u274C Failed to delete state.json");
|
|
42591
43273
|
}
|
|
42592
43274
|
try {
|
|
42593
|
-
const sessionDir =
|
|
42594
|
-
if (
|
|
42595
|
-
const files =
|
|
43275
|
+
const sessionDir = path28.dirname(validateSwarmPath(directory, "session/state.json"));
|
|
43276
|
+
if (fs19.existsSync(sessionDir)) {
|
|
43277
|
+
const files = fs19.readdirSync(sessionDir);
|
|
42596
43278
|
const otherFiles = files.filter((f) => f !== "state.json");
|
|
42597
43279
|
let deletedCount = 0;
|
|
42598
43280
|
for (const file3 of otherFiles) {
|
|
42599
|
-
const filePath =
|
|
42600
|
-
if (
|
|
42601
|
-
|
|
43281
|
+
const filePath = path28.join(sessionDir, file3);
|
|
43282
|
+
if (fs19.lstatSync(filePath).isFile()) {
|
|
43283
|
+
fs19.unlinkSync(filePath);
|
|
42602
43284
|
deletedCount++;
|
|
42603
43285
|
}
|
|
42604
43286
|
}
|
|
@@ -42626,7 +43308,7 @@ async function handleResetSessionCommand(directory, _args) {
|
|
|
42626
43308
|
// src/summaries/manager.ts
|
|
42627
43309
|
init_utils2();
|
|
42628
43310
|
init_utils();
|
|
42629
|
-
import * as
|
|
43311
|
+
import * as path29 from "path";
|
|
42630
43312
|
var SUMMARY_ID_REGEX = /^S\d+$/;
|
|
42631
43313
|
function sanitizeSummaryId(id) {
|
|
42632
43314
|
if (!id || id.length === 0) {
|
|
@@ -42650,7 +43332,7 @@ function sanitizeSummaryId(id) {
|
|
|
42650
43332
|
}
|
|
42651
43333
|
async function loadFullOutput(directory, id) {
|
|
42652
43334
|
const sanitizedId = sanitizeSummaryId(id);
|
|
42653
|
-
const relativePath =
|
|
43335
|
+
const relativePath = path29.join("summaries", `${sanitizedId}.json`);
|
|
42654
43336
|
validateSwarmPath(directory, relativePath);
|
|
42655
43337
|
const content = await readSwarmFileAsync(directory, relativePath);
|
|
42656
43338
|
if (content === null) {
|
|
@@ -42703,18 +43385,18 @@ ${error93 instanceof Error ? error93.message : String(error93)}`;
|
|
|
42703
43385
|
|
|
42704
43386
|
// src/commands/rollback.ts
|
|
42705
43387
|
init_utils2();
|
|
42706
|
-
import * as
|
|
42707
|
-
import * as
|
|
43388
|
+
import * as fs20 from "fs";
|
|
43389
|
+
import * as path30 from "path";
|
|
42708
43390
|
async function handleRollbackCommand(directory, args) {
|
|
42709
43391
|
const phaseArg = args[0];
|
|
42710
43392
|
if (!phaseArg) {
|
|
42711
43393
|
const manifestPath2 = validateSwarmPath(directory, "checkpoints/manifest.json");
|
|
42712
|
-
if (!
|
|
43394
|
+
if (!fs20.existsSync(manifestPath2)) {
|
|
42713
43395
|
return "No checkpoints found. Use `/swarm checkpoint` to create checkpoints.";
|
|
42714
43396
|
}
|
|
42715
43397
|
let manifest2;
|
|
42716
43398
|
try {
|
|
42717
|
-
manifest2 = JSON.parse(
|
|
43399
|
+
manifest2 = JSON.parse(fs20.readFileSync(manifestPath2, "utf-8"));
|
|
42718
43400
|
} catch {
|
|
42719
43401
|
return "Error: Checkpoint manifest is corrupted. Delete .swarm/checkpoints/manifest.json and re-checkpoint.";
|
|
42720
43402
|
}
|
|
@@ -42736,12 +43418,12 @@ async function handleRollbackCommand(directory, args) {
|
|
|
42736
43418
|
return "Error: Phase number must be a positive integer.";
|
|
42737
43419
|
}
|
|
42738
43420
|
const manifestPath = validateSwarmPath(directory, "checkpoints/manifest.json");
|
|
42739
|
-
if (!
|
|
43421
|
+
if (!fs20.existsSync(manifestPath)) {
|
|
42740
43422
|
return `Error: No checkpoints found. Cannot rollback to phase ${targetPhase}.`;
|
|
42741
43423
|
}
|
|
42742
43424
|
let manifest;
|
|
42743
43425
|
try {
|
|
42744
|
-
manifest = JSON.parse(
|
|
43426
|
+
manifest = JSON.parse(fs20.readFileSync(manifestPath, "utf-8"));
|
|
42745
43427
|
} catch {
|
|
42746
43428
|
return `Error: Checkpoint manifest is corrupted. Delete .swarm/checkpoints/manifest.json and re-checkpoint.`;
|
|
42747
43429
|
}
|
|
@@ -42751,10 +43433,10 @@ async function handleRollbackCommand(directory, args) {
|
|
|
42751
43433
|
return `Error: Checkpoint for phase ${targetPhase} not found. Available phases: ${available}`;
|
|
42752
43434
|
}
|
|
42753
43435
|
const checkpointDir = validateSwarmPath(directory, `checkpoints/phase-${targetPhase}`);
|
|
42754
|
-
if (!
|
|
43436
|
+
if (!fs20.existsSync(checkpointDir)) {
|
|
42755
43437
|
return `Error: Checkpoint directory for phase ${targetPhase} does not exist.`;
|
|
42756
43438
|
}
|
|
42757
|
-
const checkpointFiles =
|
|
43439
|
+
const checkpointFiles = fs20.readdirSync(checkpointDir);
|
|
42758
43440
|
if (checkpointFiles.length === 0) {
|
|
42759
43441
|
return `Error: Checkpoint for phase ${targetPhase} is empty. Cannot rollback.`;
|
|
42760
43442
|
}
|
|
@@ -42762,10 +43444,10 @@ async function handleRollbackCommand(directory, args) {
|
|
|
42762
43444
|
const successes = [];
|
|
42763
43445
|
const failures = [];
|
|
42764
43446
|
for (const file3 of checkpointFiles) {
|
|
42765
|
-
const src =
|
|
42766
|
-
const dest =
|
|
43447
|
+
const src = path30.join(checkpointDir, file3);
|
|
43448
|
+
const dest = path30.join(swarmDir, file3);
|
|
42767
43449
|
try {
|
|
42768
|
-
|
|
43450
|
+
fs20.cpSync(src, dest, { recursive: true, force: true });
|
|
42769
43451
|
successes.push(file3);
|
|
42770
43452
|
} catch (error93) {
|
|
42771
43453
|
failures.push({ file: file3, error: error93.message });
|
|
@@ -42782,7 +43464,7 @@ async function handleRollbackCommand(directory, args) {
|
|
|
42782
43464
|
timestamp: new Date().toISOString()
|
|
42783
43465
|
};
|
|
42784
43466
|
try {
|
|
42785
|
-
|
|
43467
|
+
fs20.appendFileSync(eventsPath, `${JSON.stringify(rollbackEvent)}
|
|
42786
43468
|
`);
|
|
42787
43469
|
} catch (error93) {
|
|
42788
43470
|
console.error("Failed to write rollback event:", error93 instanceof Error ? error93.message : String(error93));
|
|
@@ -42825,11 +43507,11 @@ async function handleSimulateCommand(directory, args) {
|
|
|
42825
43507
|
];
|
|
42826
43508
|
const report = reportLines.filter(Boolean).join(`
|
|
42827
43509
|
`);
|
|
42828
|
-
const
|
|
42829
|
-
const
|
|
42830
|
-
const reportPath =
|
|
42831
|
-
await
|
|
42832
|
-
await
|
|
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");
|
|
42833
43515
|
return `${darkMatterPairs.length} hidden coupling pairs detected`;
|
|
42834
43516
|
}
|
|
42835
43517
|
|
|
@@ -43392,18 +44074,18 @@ function resolveCommand(tokens) {
|
|
|
43392
44074
|
}
|
|
43393
44075
|
|
|
43394
44076
|
// src/cli/index.ts
|
|
43395
|
-
var CONFIG_DIR =
|
|
43396
|
-
var OPENCODE_CONFIG_PATH =
|
|
43397
|
-
var PLUGIN_CONFIG_PATH =
|
|
43398
|
-
var PROMPTS_DIR =
|
|
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");
|
|
43399
44081
|
function ensureDir(dir) {
|
|
43400
|
-
if (!
|
|
43401
|
-
|
|
44082
|
+
if (!fs21.existsSync(dir)) {
|
|
44083
|
+
fs21.mkdirSync(dir, { recursive: true });
|
|
43402
44084
|
}
|
|
43403
44085
|
}
|
|
43404
44086
|
function loadJson(filepath) {
|
|
43405
44087
|
try {
|
|
43406
|
-
const content =
|
|
44088
|
+
const content = fs21.readFileSync(filepath, "utf-8");
|
|
43407
44089
|
const stripped = content.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (match, comment) => comment ? "" : match).replace(/,(\s*[}\]])/g, "$1");
|
|
43408
44090
|
return JSON.parse(stripped);
|
|
43409
44091
|
} catch {
|
|
@@ -43411,7 +44093,7 @@ function loadJson(filepath) {
|
|
|
43411
44093
|
}
|
|
43412
44094
|
}
|
|
43413
44095
|
function saveJson(filepath, data) {
|
|
43414
|
-
|
|
44096
|
+
fs21.writeFileSync(filepath, `${JSON.stringify(data, null, 2)}
|
|
43415
44097
|
`, "utf-8");
|
|
43416
44098
|
}
|
|
43417
44099
|
async function install() {
|
|
@@ -43419,7 +44101,7 @@ async function install() {
|
|
|
43419
44101
|
`);
|
|
43420
44102
|
ensureDir(CONFIG_DIR);
|
|
43421
44103
|
ensureDir(PROMPTS_DIR);
|
|
43422
|
-
const LEGACY_CONFIG_PATH =
|
|
44104
|
+
const LEGACY_CONFIG_PATH = path31.join(CONFIG_DIR, "config.json");
|
|
43423
44105
|
let opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
|
|
43424
44106
|
if (!opencodeConfig) {
|
|
43425
44107
|
const legacyConfig = loadJson(LEGACY_CONFIG_PATH);
|
|
@@ -43444,7 +44126,7 @@ async function install() {
|
|
|
43444
44126
|
saveJson(OPENCODE_CONFIG_PATH, opencodeConfig);
|
|
43445
44127
|
console.log("\u2713 Added opencode-swarm to OpenCode plugins");
|
|
43446
44128
|
console.log("\u2713 Disabled default OpenCode agents (explore, general)");
|
|
43447
|
-
if (!
|
|
44129
|
+
if (!fs21.existsSync(PLUGIN_CONFIG_PATH)) {
|
|
43448
44130
|
const defaultConfig = {
|
|
43449
44131
|
agents: {
|
|
43450
44132
|
coder: { model: "opencode/minimax-m2.5-free" },
|
|
@@ -43487,7 +44169,7 @@ async function uninstall() {
|
|
|
43487
44169
|
`);
|
|
43488
44170
|
const opencodeConfig = loadJson(OPENCODE_CONFIG_PATH);
|
|
43489
44171
|
if (!opencodeConfig) {
|
|
43490
|
-
if (
|
|
44172
|
+
if (fs21.existsSync(OPENCODE_CONFIG_PATH)) {
|
|
43491
44173
|
console.log(`\u2717 Could not parse opencode config at: ${OPENCODE_CONFIG_PATH}`);
|
|
43492
44174
|
return 1;
|
|
43493
44175
|
} else {
|
|
@@ -43519,13 +44201,13 @@ async function uninstall() {
|
|
|
43519
44201
|
console.log("\u2713 Re-enabled default OpenCode agents (explore, general)");
|
|
43520
44202
|
if (process.argv.includes("--clean")) {
|
|
43521
44203
|
let cleaned = false;
|
|
43522
|
-
if (
|
|
43523
|
-
|
|
44204
|
+
if (fs21.existsSync(PLUGIN_CONFIG_PATH)) {
|
|
44205
|
+
fs21.unlinkSync(PLUGIN_CONFIG_PATH);
|
|
43524
44206
|
console.log(`\u2713 Removed plugin config: ${PLUGIN_CONFIG_PATH}`);
|
|
43525
44207
|
cleaned = true;
|
|
43526
44208
|
}
|
|
43527
|
-
if (
|
|
43528
|
-
|
|
44209
|
+
if (fs21.existsSync(PROMPTS_DIR)) {
|
|
44210
|
+
fs21.rmSync(PROMPTS_DIR, { recursive: true });
|
|
43529
44211
|
console.log(`\u2713 Removed custom prompts: ${PROMPTS_DIR}`);
|
|
43530
44212
|
cleaned = true;
|
|
43531
44213
|
}
|