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.
- package/dist/cli/index.js +1201 -167
- package/dist/commands/brainstorm.d.ts +13 -0
- package/dist/commands/brainstorm.test.d.ts +1 -0
- package/dist/commands/index.d.ts +2 -0
- package/dist/commands/qa-gates.d.ts +15 -0
- package/dist/commands/qa-gates.test.d.ts +1 -0
- package/dist/commands/registry.d.ts +12 -0
- package/dist/db/global-db.d.ts +22 -0
- package/dist/db/global-db.test.d.ts +7 -0
- package/dist/db/index.d.ts +13 -0
- package/dist/db/project-db.d.ts +40 -0
- package/dist/db/project-db.test.d.ts +4 -0
- package/dist/db/qa-gate-profile.d.ts +89 -0
- package/dist/db/qa-gate-profile.test.d.ts +4 -0
- 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 +3070 -919
- 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/state.d.ts +7 -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/get-approved-plan.d.ts +4 -0
- package/dist/tools/get-qa-gate-profile.d.ts +27 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/mutation-test.d.ts +2 -0
- package/dist/tools/mutation-test.security.test.d.ts +1 -0
- package/dist/tools/set-qa-gates.d.ts +37 -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",
|
|
@@ -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
|
|
38453
|
-
import * as
|
|
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
|
|
39589
|
-
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
|
+
}
|
|
39590
40187
|
|
|
39591
40188
|
// src/tools/resolve-working-directory.ts
|
|
39592
|
-
import * as
|
|
39593
|
-
import * as
|
|
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 =
|
|
39614
|
-
const pathParts = normalizedDir.split(
|
|
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 =
|
|
40218
|
+
const resolvedDir = path24.resolve(normalizedDir);
|
|
39622
40219
|
try {
|
|
39623
|
-
const realPath =
|
|
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
|
|
40301
|
+
return fs15.existsSync(path25.join(cwd, "go.mod")) && isCommandAvailable("go");
|
|
39705
40302
|
}
|
|
39706
40303
|
function detectJavaMaven(cwd) {
|
|
39707
|
-
return
|
|
40304
|
+
return fs15.existsSync(path25.join(cwd, "pom.xml")) && isCommandAvailable("mvn");
|
|
39708
40305
|
}
|
|
39709
40306
|
function detectGradle(cwd) {
|
|
39710
|
-
const hasBuildFile =
|
|
39711
|
-
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"));
|
|
39712
40309
|
return hasBuildFile && (hasGradlew || isCommandAvailable("gradle"));
|
|
39713
40310
|
}
|
|
39714
40311
|
function detectDotnetTest(cwd) {
|
|
39715
40312
|
try {
|
|
39716
|
-
const files =
|
|
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 =
|
|
39725
|
-
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"));
|
|
39726
40323
|
return (hasSource || hasBuildCache) && isCommandAvailable("ctest");
|
|
39727
40324
|
}
|
|
39728
40325
|
function detectSwiftTest(cwd) {
|
|
39729
|
-
return
|
|
40326
|
+
return fs15.existsSync(path25.join(cwd, "Package.swift")) && isCommandAvailable("swift");
|
|
39730
40327
|
}
|
|
39731
40328
|
function detectDartTest(cwd) {
|
|
39732
|
-
return
|
|
40329
|
+
return fs15.existsSync(path25.join(cwd, "pubspec.yaml")) && (isCommandAvailable("dart") || isCommandAvailable("flutter"));
|
|
39733
40330
|
}
|
|
39734
40331
|
function detectRSpec(cwd) {
|
|
39735
|
-
const hasRSpecFile =
|
|
39736
|
-
const hasGemfile =
|
|
39737
|
-
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"));
|
|
39738
40335
|
const hasRSpec = hasRSpecFile || hasGemfile && hasSpecDir;
|
|
39739
40336
|
return hasRSpec && (isCommandAvailable("bundle") || isCommandAvailable("rspec"));
|
|
39740
40337
|
}
|
|
39741
40338
|
function detectMinitest(cwd) {
|
|
39742
|
-
return
|
|
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 =
|
|
39748
|
-
if (
|
|
39749
|
-
const content =
|
|
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 (
|
|
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 =
|
|
39776
|
-
const setupCfgPath =
|
|
39777
|
-
const requirementsTxtPath =
|
|
39778
|
-
if (
|
|
39779
|
-
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");
|
|
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 (
|
|
39786
|
-
const content =
|
|
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 (
|
|
39791
|
-
const content =
|
|
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 =
|
|
39798
|
-
if (
|
|
39799
|
-
const content =
|
|
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 =
|
|
39809
|
-
const pesterConfigJsonPath =
|
|
39810
|
-
const pesterPs1Path =
|
|
39811
|
-
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)) {
|
|
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 =
|
|
39863
|
-
const dirname10 =
|
|
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 =
|
|
40469
|
+
const ext = path25.extname(basename4);
|
|
39873
40470
|
const possibleTestFiles = [
|
|
39874
|
-
|
|
39875
|
-
|
|
39876
|
-
|
|
39877
|
-
|
|
39878
|
-
|
|
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 (
|
|
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 =
|
|
39898
|
-
const testDir =
|
|
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 =
|
|
39907
|
-
const existingExt =
|
|
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) ||
|
|
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 =
|
|
39928
|
-
const importDir =
|
|
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 =
|
|
39931
|
-
const sourceBasename =
|
|
39932
|
-
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");
|
|
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 =
|
|
39948
|
-
const existingExt =
|
|
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) ||
|
|
40556
|
+
if (sourceFiles.includes(withExt) || fs15.existsSync(withExt)) {
|
|
39960
40557
|
resolvedImport = withExt;
|
|
39961
40558
|
break;
|
|
39962
40559
|
}
|
|
39963
40560
|
}
|
|
39964
40561
|
}
|
|
39965
|
-
const importDir =
|
|
39966
|
-
const importBasename =
|
|
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 =
|
|
39969
|
-
const sourceBasename =
|
|
39970
|
-
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");
|
|
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 =
|
|
40056
|
-
const hasGradlew =
|
|
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) =>
|
|
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,
|
|
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 "
|
|
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 =
|
|
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 =
|
|
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 =
|
|
40723
|
-
const absolutePath =
|
|
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 =
|
|
40747
|
-
if (
|
|
40748
|
-
const content =
|
|
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 =
|
|
40758
|
-
if (
|
|
40759
|
-
const content =
|
|
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 =
|
|
40772
|
-
if (
|
|
41475
|
+
const filePath = path26.join(dir, file3);
|
|
41476
|
+
if (fs16.existsSync(filePath)) {
|
|
40773
41477
|
try {
|
|
40774
|
-
const content =
|
|
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 =
|
|
41099
|
-
if (!
|
|
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
|
|
42016
|
+
import * as fs17 from "fs";
|
|
41313
42017
|
import * as os5 from "os";
|
|
41314
|
-
import * as
|
|
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 =
|
|
42064
|
+
dataDir = path27.join(process.env.LOCALAPPDATA || path27.join(home, "AppData", "Local"), "opencode-swarm", "Data");
|
|
41361
42065
|
} else if (platform === "darwin") {
|
|
41362
|
-
dataDir =
|
|
42066
|
+
dataDir = path27.join(home, "Library", "Application Support", "opencode-swarm");
|
|
41363
42067
|
} else {
|
|
41364
|
-
dataDir =
|
|
42068
|
+
dataDir = path27.join(process.env.XDG_DATA_HOME || path27.join(home, ".local", "share"), "opencode-swarm");
|
|
41365
42069
|
}
|
|
41366
|
-
return
|
|
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 =
|
|
41379
|
-
if (!
|
|
41380
|
-
|
|
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
|
-
|
|
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 =
|
|
42104
|
+
const knowledgePath = path27.join(directory, ".swarm", "knowledge.jsonl");
|
|
41401
42105
|
const entries = [];
|
|
41402
|
-
if (
|
|
41403
|
-
const content =
|
|
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 =
|
|
41428
|
-
if (!
|
|
41429
|
-
|
|
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
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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 (
|
|
42199
|
-
|
|
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 (
|
|
42217
|
-
|
|
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
|
|
42238
|
-
import * as
|
|
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 (
|
|
42244
|
-
|
|
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 =
|
|
42254
|
-
if (
|
|
42255
|
-
const files =
|
|
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 =
|
|
42260
|
-
if (
|
|
42261
|
-
|
|
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
|
|
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 =
|
|
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
|
|
42367
|
-
import * as
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
43436
|
+
if (!fs20.existsSync(checkpointDir)) {
|
|
42415
43437
|
return `Error: Checkpoint directory for phase ${targetPhase} does not exist.`;
|
|
42416
43438
|
}
|
|
42417
|
-
const checkpointFiles =
|
|
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 =
|
|
42426
|
-
const dest =
|
|
43447
|
+
const src = path30.join(checkpointDir, file3);
|
|
43448
|
+
const dest = path30.join(swarmDir, file3);
|
|
42427
43449
|
try {
|
|
42428
|
-
|
|
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
|
-
|
|
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
|
|
42489
|
-
const
|
|
42490
|
-
const reportPath =
|
|
42491
|
-
await
|
|
42492
|
-
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");
|
|
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 =
|
|
43044
|
-
var OPENCODE_CONFIG_PATH =
|
|
43045
|
-
var PLUGIN_CONFIG_PATH =
|
|
43046
|
-
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");
|
|
43047
44081
|
function ensureDir(dir) {
|
|
43048
|
-
if (!
|
|
43049
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
43171
|
-
|
|
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 (
|
|
43176
|
-
|
|
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
|
}
|