tend-cli 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +595 -90
- package/dist/config-DPlVYfKX.js +3665 -0
- package/dist/index.d.ts +121 -39
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/config-tbp_HMuZ.js +0 -184970
package/dist/bin.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ClaudeSession, EFFORT_LEVELS, EventBus, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildDiff, buildProgram, changedVsHead, createGit, detectBuildCommand, detectPackageManager, filesUnder, filterToChanged, formatClock, gateUnitChanges, loadConfig, makeDeterministicFixUnit, makeTheme, orchestrate, planRepair, planWorkFromRepairs, reasonLabel, renderSummary, resolveRetryTarget, restoreSnapshot, retryCommand, runEslintSonarjs, runScanner, scannerStatus, showCommand, snapshotUnitFiles, snapshotUnitNow, toRepoRelative, unitChanged, zeroUsage } from "./config-
|
|
2
|
+
import { ClaudeSession, EFFORT_LEVELS, EventBus, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, assertGitRepo, buildDiff, buildProgram, changedVsHead, createGit, detectBuildCommand, detectPackageManager, filesUnder, filterToChanged, formatClock, gateUnitChanges, loadConfig, makeDeterministicFixUnit, makeTheme, orchestrate, planRepair, planWorkFromRepairs, reasonLabel, renderSummary, resolveRetryTarget, restoreSnapshot, retryCommand, runEslintSonarjs, runScanner, scannerStatus, showCommand, snapshotUnitFiles, snapshotUnitNow, toRepoRelative, unitChanged, zeroUsage } from "./config-DPlVYfKX.js";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
+
import PQueue from "p-queue";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { tmpdir } from "node:os";
|
|
8
9
|
import { createRequire } from "node:module";
|
|
@@ -390,13 +391,51 @@ async function runScanners(deps, files, loop) {
|
|
|
390
391
|
files,
|
|
391
392
|
loop
|
|
392
393
|
};
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
394
|
+
const requested = deps.tools ? new Set(deps.tools) : void 0;
|
|
395
|
+
const shouldRun = (tool) => requested === void 0 || requested.has(tool);
|
|
396
|
+
const runWithEvents = async (scanner) => {
|
|
397
|
+
deps.bus?.emit({
|
|
398
|
+
type: "scanner-start",
|
|
399
|
+
loop,
|
|
400
|
+
tool: scanner.tool
|
|
401
|
+
});
|
|
402
|
+
const result = await runScanner(scanner, ctx, {
|
|
403
|
+
which: deps.which,
|
|
404
|
+
spawn: deps.spawn,
|
|
405
|
+
timeout: deps.timeoutMs
|
|
406
|
+
});
|
|
407
|
+
const status = scannerStatus(result);
|
|
408
|
+
deps.bus?.emit({
|
|
409
|
+
type: "scanner-result",
|
|
410
|
+
loop,
|
|
411
|
+
tool: scanner.tool,
|
|
412
|
+
status: status.status,
|
|
413
|
+
findings: result.findings.length,
|
|
414
|
+
reason: status.reason
|
|
415
|
+
});
|
|
416
|
+
return result;
|
|
417
|
+
};
|
|
418
|
+
const spawnedPromise = Promise.all(SPAWN_SCANNERS.filter((scanner) => shouldRun(scanner.tool)).map(runWithEvents));
|
|
419
|
+
const eslintPromise = shouldRun("sonarjs") ? (async () => {
|
|
420
|
+
deps.bus?.emit({
|
|
421
|
+
type: "scanner-start",
|
|
422
|
+
loop,
|
|
423
|
+
tool: "sonarjs"
|
|
424
|
+
});
|
|
425
|
+
const result = await runEslintSonarjs(ctx);
|
|
426
|
+
const status = scannerStatus(result);
|
|
427
|
+
deps.bus?.emit({
|
|
428
|
+
type: "scanner-result",
|
|
429
|
+
loop,
|
|
430
|
+
tool: "sonarjs",
|
|
431
|
+
status: status.status,
|
|
432
|
+
findings: result.findings.length,
|
|
433
|
+
reason: status.reason
|
|
434
|
+
});
|
|
435
|
+
return result;
|
|
436
|
+
})() : Promise.resolve(void 0);
|
|
437
|
+
const [spawned, eslint] = await Promise.all([spawnedPromise, eslintPromise]);
|
|
438
|
+
const results = eslint ? [...spawned, eslint] : spawned;
|
|
400
439
|
return {
|
|
401
440
|
results,
|
|
402
441
|
scannerStatuses: results.map(scannerStatus)
|
|
@@ -729,8 +768,8 @@ function renderPrompt(unit) {
|
|
|
729
768
|
});
|
|
730
769
|
}
|
|
731
770
|
function firstRelevantLines(output, max = 20) {
|
|
732
|
-
const lines = output.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
733
|
-
return lines.slice(0, max).join("\n") || "(none)";
|
|
771
|
+
const lines$1 = output.split("\n").map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
772
|
+
return lines$1.slice(0, max).join("\n") || "(none)";
|
|
734
773
|
}
|
|
735
774
|
function renderFindingsJson(findings) {
|
|
736
775
|
const data = findings.map((f) => ({
|
|
@@ -789,12 +828,21 @@ function classFromOutcome(reason, fallback) {
|
|
|
789
828
|
* session error reverts the files to the snapshot. Nothing changed → not a fix.
|
|
790
829
|
*/
|
|
791
830
|
function makeFixUnit(deps) {
|
|
792
|
-
return async (unit) => {
|
|
831
|
+
return async (unit, loop = 0) => {
|
|
832
|
+
const progress = (stage, detail) => {
|
|
833
|
+
deps.onProgress?.({
|
|
834
|
+
loop,
|
|
835
|
+
file: unit.file,
|
|
836
|
+
stage,
|
|
837
|
+
detail
|
|
838
|
+
});
|
|
839
|
+
};
|
|
793
840
|
const snapshotFiles = unit.strategy === "generated-source-repair" ? [...new Set([...unit.files, ...unit.verificationTargets ?? []])] : unit.files;
|
|
794
841
|
const before = snapshotUnitFiles(deps.cwd, snapshotFiles);
|
|
795
842
|
const restore = () => restoreSnapshot(deps.cwd, before);
|
|
796
843
|
const changedOnDisk = () => unitChanged(deps.cwd, unit.files, before);
|
|
797
844
|
let usage = zeroUsage();
|
|
845
|
+
progress("ai-edit");
|
|
798
846
|
const res = await deps.session.run({
|
|
799
847
|
file: unit.file,
|
|
800
848
|
findings: unit.findings,
|
|
@@ -812,6 +860,7 @@ function makeFixUnit(deps) {
|
|
|
812
860
|
};
|
|
813
861
|
}
|
|
814
862
|
if (!changedOnDisk()) {
|
|
863
|
+
progress("ai-no-edit-retry");
|
|
815
864
|
const retry = await deps.session.run({
|
|
816
865
|
file: unit.file,
|
|
817
866
|
findings: unit.findings,
|
|
@@ -837,14 +886,17 @@ function makeFixUnit(deps) {
|
|
|
837
886
|
};
|
|
838
887
|
}
|
|
839
888
|
async function scanNewFindings() {
|
|
889
|
+
progress("rescan");
|
|
840
890
|
const verificationTargets = unit.verificationTargets ?? unit.files;
|
|
841
|
-
const
|
|
891
|
+
const scannerTools = [...new Set(unit.findings.map((finding) => finding.tool))];
|
|
892
|
+
const afterFindings = await deps.scanFindings(verificationTargets, scannerTools);
|
|
842
893
|
const originalIds = new Set(unit.findings.map((f) => f.id));
|
|
843
894
|
return afterFindings.filter((f) => !originalIds.has(f.id));
|
|
844
895
|
}
|
|
845
896
|
async function runRegressionRepair(outcome$1) {
|
|
846
897
|
if (outcome$1.reason !== "regression" && outcome$1.reason !== "typecheck") return false;
|
|
847
898
|
const after = snapshotUnitNow(deps.cwd, snapshotFiles);
|
|
899
|
+
progress("regression-repair");
|
|
848
900
|
const repair = await deps.session.run({
|
|
849
901
|
file: unit.file,
|
|
850
902
|
findings: unit.findings,
|
|
@@ -867,6 +919,7 @@ function makeFixUnit(deps) {
|
|
|
867
919
|
async function gateCurrent() {
|
|
868
920
|
return gateUnitChanges(unit, before, deps, {
|
|
869
921
|
usage,
|
|
922
|
+
onProgress: progress,
|
|
870
923
|
repair: async (_attempt, regressed) => {
|
|
871
924
|
const after = snapshotUnitNow(deps.cwd, snapshotFiles);
|
|
872
925
|
const repair = await deps.session.run({
|
|
@@ -904,6 +957,244 @@ function makeFixUnit(deps) {
|
|
|
904
957
|
};
|
|
905
958
|
}
|
|
906
959
|
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/fixing/worker-sandbox.ts
|
|
962
|
+
var SandboxSetupError = class extends Error {
|
|
963
|
+
constructor(message) {
|
|
964
|
+
super(message);
|
|
965
|
+
this.name = "SandboxSetupError";
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
const cleanExcludes = [
|
|
969
|
+
"node_modules",
|
|
970
|
+
"node_modules/**",
|
|
971
|
+
"**/node_modules",
|
|
972
|
+
"**/node_modules/**"
|
|
973
|
+
];
|
|
974
|
+
function normalizeRel(path) {
|
|
975
|
+
return path.replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
976
|
+
}
|
|
977
|
+
function lines(raw) {
|
|
978
|
+
return raw.split("\n").map((line) => normalizeRel(line.trim())).filter(Boolean);
|
|
979
|
+
}
|
|
980
|
+
function unique(values) {
|
|
981
|
+
return [...new Set(values.map(normalizeRel))];
|
|
982
|
+
}
|
|
983
|
+
function isGeneratedRepair(unit) {
|
|
984
|
+
return unit.strategy === "generated-source-repair" || unit.strategies?.includes("generated-source-repair") === true;
|
|
985
|
+
}
|
|
986
|
+
function allowedPatchFiles(unit) {
|
|
987
|
+
return unique([...unit.files, ...isGeneratedRepair(unit) ? unit.verificationTargets ?? [] : []]);
|
|
988
|
+
}
|
|
989
|
+
function installArgs(pm) {
|
|
990
|
+
switch (pm) {
|
|
991
|
+
case "pnpm": return [
|
|
992
|
+
"install",
|
|
993
|
+
"--frozen-lockfile",
|
|
994
|
+
"--prefer-offline"
|
|
995
|
+
];
|
|
996
|
+
case "yarn": return [
|
|
997
|
+
"install",
|
|
998
|
+
"--frozen-lockfile",
|
|
999
|
+
"--prefer-offline"
|
|
1000
|
+
];
|
|
1001
|
+
case "bun": return ["install", "--frozen-lockfile"];
|
|
1002
|
+
case "npm": return ["ci", "--prefer-offline"];
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
var GitWorkerSandbox = class {
|
|
1006
|
+
prepared = false;
|
|
1007
|
+
constructor(cwd$1, deps) {
|
|
1008
|
+
this.cwd = cwd$1;
|
|
1009
|
+
this.deps = deps;
|
|
1010
|
+
}
|
|
1011
|
+
async reset() {
|
|
1012
|
+
const git = createGit(this.cwd);
|
|
1013
|
+
await git.raw([
|
|
1014
|
+
"reset",
|
|
1015
|
+
"--hard",
|
|
1016
|
+
this.deps.snapshotSha
|
|
1017
|
+
]);
|
|
1018
|
+
await git.raw([
|
|
1019
|
+
"clean",
|
|
1020
|
+
"-ffdx",
|
|
1021
|
+
...cleanExcludes.flatMap((pattern) => ["-e", pattern])
|
|
1022
|
+
]);
|
|
1023
|
+
}
|
|
1024
|
+
async prepare() {
|
|
1025
|
+
if (this.prepared || this.deps.prepareDependencies === false) return;
|
|
1026
|
+
const args = installArgs(this.deps.packageManager);
|
|
1027
|
+
const result = await this.deps.exec(this.deps.packageManager, args, {
|
|
1028
|
+
cwd: this.cwd,
|
|
1029
|
+
reject: false,
|
|
1030
|
+
timeout: 10 * 6e4
|
|
1031
|
+
});
|
|
1032
|
+
if ((result.exitCode ?? 1) !== 0) throw new SandboxSetupError(`sandbox dependency install failed: ${result.stderr || result.stdout || `exit ${result.exitCode ?? 1}`}`);
|
|
1033
|
+
this.prepared = true;
|
|
1034
|
+
}
|
|
1035
|
+
async collectPatch(unit) {
|
|
1036
|
+
const git = createGit(this.cwd);
|
|
1037
|
+
const tracked = lines(await git.raw([
|
|
1038
|
+
"diff",
|
|
1039
|
+
"--name-only",
|
|
1040
|
+
this.deps.snapshotSha
|
|
1041
|
+
]));
|
|
1042
|
+
const untracked = lines(await git.raw([
|
|
1043
|
+
"ls-files",
|
|
1044
|
+
"--others",
|
|
1045
|
+
"--exclude-standard"
|
|
1046
|
+
]));
|
|
1047
|
+
const changedFiles = unique([...tracked, ...untracked]).sort();
|
|
1048
|
+
const allowed = new Set(allowedPatchFiles(unit));
|
|
1049
|
+
const unowned = changedFiles.filter((file) => !allowed.has(file));
|
|
1050
|
+
if (unowned.length > 0) return {
|
|
1051
|
+
ok: false,
|
|
1052
|
+
reason: "unowned-patch",
|
|
1053
|
+
detail: `Worker modified unowned files: ${unowned.join(", ")}`,
|
|
1054
|
+
changedFiles
|
|
1055
|
+
};
|
|
1056
|
+
const allowedFiles = [...allowed].sort();
|
|
1057
|
+
if (allowedFiles.length === 0 || changedFiles.length === 0) return {
|
|
1058
|
+
ok: true,
|
|
1059
|
+
patch: "",
|
|
1060
|
+
changedFiles
|
|
1061
|
+
};
|
|
1062
|
+
if (untracked.length > 0) await git.raw([
|
|
1063
|
+
"add",
|
|
1064
|
+
"-N",
|
|
1065
|
+
"--",
|
|
1066
|
+
...untracked.filter((file) => allowed.has(file))
|
|
1067
|
+
]);
|
|
1068
|
+
const patch = await git.raw([
|
|
1069
|
+
"diff",
|
|
1070
|
+
"--binary",
|
|
1071
|
+
this.deps.snapshotSha,
|
|
1072
|
+
"--",
|
|
1073
|
+
...allowedFiles
|
|
1074
|
+
]);
|
|
1075
|
+
return {
|
|
1076
|
+
ok: true,
|
|
1077
|
+
patch,
|
|
1078
|
+
changedFiles
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
async dispose() {
|
|
1082
|
+
const git = createGit(this.deps.mainRoot);
|
|
1083
|
+
try {
|
|
1084
|
+
await git.raw([
|
|
1085
|
+
"worktree",
|
|
1086
|
+
"remove",
|
|
1087
|
+
"--force",
|
|
1088
|
+
this.cwd
|
|
1089
|
+
]);
|
|
1090
|
+
} finally {
|
|
1091
|
+
rmSync(this.cwd, {
|
|
1092
|
+
recursive: true,
|
|
1093
|
+
force: true
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
};
|
|
1098
|
+
var WorkerSandboxPool = class {
|
|
1099
|
+
queue;
|
|
1100
|
+
applyQueue = new PQueue({ concurrency: 1 });
|
|
1101
|
+
idle = [];
|
|
1102
|
+
sandboxes = new Set();
|
|
1103
|
+
counter = 0;
|
|
1104
|
+
disposed = false;
|
|
1105
|
+
exec;
|
|
1106
|
+
constructor(deps) {
|
|
1107
|
+
this.deps = deps;
|
|
1108
|
+
this.queue = new PQueue({ concurrency: deps.maxSandboxes });
|
|
1109
|
+
this.exec = deps.exec ?? execa;
|
|
1110
|
+
}
|
|
1111
|
+
async withSandbox(run) {
|
|
1112
|
+
return this.queue.add(async () => {
|
|
1113
|
+
let sandbox;
|
|
1114
|
+
try {
|
|
1115
|
+
sandbox = await this.acquire();
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
throw new SandboxSetupError(error instanceof Error ? error.message : String(error));
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
await sandbox.reset();
|
|
1121
|
+
await sandbox.prepare();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
this.idle.push(sandbox);
|
|
1124
|
+
throw error instanceof SandboxSetupError ? error : new SandboxSetupError(error instanceof Error ? error.message : String(error));
|
|
1125
|
+
}
|
|
1126
|
+
try {
|
|
1127
|
+
return await run(sandbox);
|
|
1128
|
+
} finally {
|
|
1129
|
+
this.idle.push(sandbox);
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
async applyPatchToMain(patch) {
|
|
1134
|
+
return this.applyQueue.add(async () => {
|
|
1135
|
+
if (patch.trim() === "") return { ok: true };
|
|
1136
|
+
const check = await this.exec("git", [
|
|
1137
|
+
"apply",
|
|
1138
|
+
"--check",
|
|
1139
|
+
"--3way"
|
|
1140
|
+
], {
|
|
1141
|
+
cwd: this.deps.mainRoot,
|
|
1142
|
+
input: patch,
|
|
1143
|
+
reject: false
|
|
1144
|
+
});
|
|
1145
|
+
if ((check.exitCode ?? 1) !== 0) return {
|
|
1146
|
+
ok: false,
|
|
1147
|
+
reason: "patch-conflict",
|
|
1148
|
+
detail: check.stderr || check.stdout || "git apply --check --3way failed"
|
|
1149
|
+
};
|
|
1150
|
+
const applied = await this.exec("git", ["apply", "--3way"], {
|
|
1151
|
+
cwd: this.deps.mainRoot,
|
|
1152
|
+
input: patch,
|
|
1153
|
+
reject: false
|
|
1154
|
+
});
|
|
1155
|
+
if ((applied.exitCode ?? 1) !== 0) return {
|
|
1156
|
+
ok: false,
|
|
1157
|
+
reason: "patch-conflict",
|
|
1158
|
+
detail: applied.stderr || applied.stdout || "git apply --3way failed"
|
|
1159
|
+
};
|
|
1160
|
+
return { ok: true };
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
async dispose() {
|
|
1164
|
+
if (this.disposed) return;
|
|
1165
|
+
this.disposed = true;
|
|
1166
|
+
await this.queue.onIdle();
|
|
1167
|
+
await Promise.all([...this.sandboxes].map((sandbox) => sandbox.dispose()));
|
|
1168
|
+
this.idle.length = 0;
|
|
1169
|
+
this.sandboxes.clear();
|
|
1170
|
+
}
|
|
1171
|
+
async acquire() {
|
|
1172
|
+
const existing = this.idle.pop();
|
|
1173
|
+
if (existing) return existing;
|
|
1174
|
+
const parent = this.deps.tempRoot ?? tmpdir();
|
|
1175
|
+
mkdirSync(parent, { recursive: true });
|
|
1176
|
+
const path = `${parent}/tend-worker-${process.pid}-${this.counter++}`;
|
|
1177
|
+
const mainGit = createGit(this.deps.mainRoot);
|
|
1178
|
+
await mainGit.raw([
|
|
1179
|
+
"worktree",
|
|
1180
|
+
"add",
|
|
1181
|
+
"--detach",
|
|
1182
|
+
path,
|
|
1183
|
+
this.deps.snapshotSha
|
|
1184
|
+
]);
|
|
1185
|
+
const sandbox = new GitWorkerSandbox(path, {
|
|
1186
|
+
...this.deps,
|
|
1187
|
+
exec: this.exec
|
|
1188
|
+
});
|
|
1189
|
+
this.sandboxes.add(sandbox);
|
|
1190
|
+
return sandbox;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
function mapOwnerRoot(mainRoot, mainOwnerRoot, sandboxRoot) {
|
|
1194
|
+
const rel = normalizeRel(relative(mainRoot, mainOwnerRoot));
|
|
1195
|
+
return rel === "" ? sandboxRoot : `${sandboxRoot}/${rel}`;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
907
1198
|
//#endregion
|
|
908
1199
|
//#region src/output/env.ts
|
|
909
1200
|
/** Truthy in the env-var sense: present and not an explicit off value. */
|
|
@@ -939,6 +1230,28 @@ function detectOutputEnv(input = {}) {
|
|
|
939
1230
|
};
|
|
940
1231
|
}
|
|
941
1232
|
|
|
1233
|
+
//#endregion
|
|
1234
|
+
//#region src/fixing/progress.ts
|
|
1235
|
+
const LABELS = {
|
|
1236
|
+
"ai-edit": "AI edit",
|
|
1237
|
+
"ai-no-edit-retry": "AI retry",
|
|
1238
|
+
"anti-suppression": "suppression check",
|
|
1239
|
+
typecheck: "typecheck",
|
|
1240
|
+
build: "build",
|
|
1241
|
+
"related-tests": "related tests",
|
|
1242
|
+
"test-repair": "test repair",
|
|
1243
|
+
rescan: "rescan",
|
|
1244
|
+
"regression-check": "regression check",
|
|
1245
|
+
"regression-repair": "regression repair",
|
|
1246
|
+
"patch-apply": "patch apply",
|
|
1247
|
+
"patch-conflict": "patch conflict",
|
|
1248
|
+
"sandbox-setup": "sandbox setup",
|
|
1249
|
+
"final-integration": "final integration"
|
|
1250
|
+
};
|
|
1251
|
+
function fixStageLabel(stage) {
|
|
1252
|
+
return LABELS[stage];
|
|
1253
|
+
}
|
|
1254
|
+
|
|
942
1255
|
//#endregion
|
|
943
1256
|
//#region src/output/base-reporter.ts
|
|
944
1257
|
var BaseReporter = class {
|
|
@@ -987,6 +1300,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
987
1300
|
audits = new Channel();
|
|
988
1301
|
phases = new Channel();
|
|
989
1302
|
fixTicks = new Channel();
|
|
1303
|
+
loopCompletions = new Channel();
|
|
990
1304
|
closed = false;
|
|
991
1305
|
resolveClosed;
|
|
992
1306
|
closedSignal = new Promise((resolve$1) => {
|
|
@@ -1002,7 +1316,11 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1002
1316
|
currentFile;
|
|
1003
1317
|
currentConcurrency;
|
|
1004
1318
|
rules = new Map();
|
|
1319
|
+
stages = new Map();
|
|
1320
|
+
scannerStates = new Map();
|
|
1321
|
+
currentScanLoop;
|
|
1005
1322
|
header;
|
|
1323
|
+
scanHeader;
|
|
1006
1324
|
labelWidth = 0;
|
|
1007
1325
|
constructor(deps) {
|
|
1008
1326
|
super(deps);
|
|
@@ -1018,6 +1336,26 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1018
1336
|
scanned: event.scanned
|
|
1019
1337
|
});
|
|
1020
1338
|
break;
|
|
1339
|
+
case "scan-start":
|
|
1340
|
+
this.currentScanLoop = event.loop;
|
|
1341
|
+
this.scannerStates.clear();
|
|
1342
|
+
this.scanStarts.push(event.loop);
|
|
1343
|
+
this.refreshScanHeader();
|
|
1344
|
+
break;
|
|
1345
|
+
case "scanner-start":
|
|
1346
|
+
if (event.loop !== this.currentScanLoop) break;
|
|
1347
|
+
this.scannerStates.set(event.tool, { status: "running" });
|
|
1348
|
+
this.refreshScanHeader();
|
|
1349
|
+
break;
|
|
1350
|
+
case "scanner-result":
|
|
1351
|
+
if (event.loop !== this.currentScanLoop) break;
|
|
1352
|
+
this.scannerStates.set(event.tool, {
|
|
1353
|
+
status: event.status,
|
|
1354
|
+
findings: event.findings,
|
|
1355
|
+
reason: event.reason
|
|
1356
|
+
});
|
|
1357
|
+
this.refreshScanHeader();
|
|
1358
|
+
break;
|
|
1021
1359
|
case "loop-start":
|
|
1022
1360
|
this.currentLoop = event.loop;
|
|
1023
1361
|
this.fixTotal = event.files.length;
|
|
@@ -1029,6 +1367,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1029
1367
|
this.currentFile = void 0;
|
|
1030
1368
|
this.currentConcurrency = event.concurrency;
|
|
1031
1369
|
this.rules.clear();
|
|
1370
|
+
this.stages.clear();
|
|
1032
1371
|
this.labelWidth = Math.max(0, ...event.files.map((f) => basename(f).length));
|
|
1033
1372
|
this.phases.push({
|
|
1034
1373
|
kind: "fix",
|
|
@@ -1041,12 +1380,19 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1041
1380
|
break;
|
|
1042
1381
|
case "file-start":
|
|
1043
1382
|
this.started += 1;
|
|
1383
|
+
this.fixTotal = Math.max(this.fixTotal, this.started);
|
|
1044
1384
|
this.currentFile = event.file;
|
|
1045
1385
|
if (event.rule) this.rules.set(event.file, event.rule);
|
|
1046
1386
|
this.refreshHeader();
|
|
1047
1387
|
break;
|
|
1388
|
+
case "file-stage":
|
|
1389
|
+
this.currentFile = event.file;
|
|
1390
|
+
this.stages.set(event.file, event.stage);
|
|
1391
|
+
this.refreshHeader();
|
|
1392
|
+
break;
|
|
1048
1393
|
case "file-result":
|
|
1049
1394
|
this.finished += 1;
|
|
1395
|
+
this.fixTotal = Math.max(this.fixTotal, this.started, this.finished);
|
|
1050
1396
|
if (event.outcome === "fixed") this.fixed += 1;
|
|
1051
1397
|
else if (event.outcome === "reverted") this.reverted += 1;
|
|
1052
1398
|
else this.notAttempted += 1;
|
|
@@ -1054,15 +1400,15 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1054
1400
|
this.refreshHeader();
|
|
1055
1401
|
this.fixTicks.push();
|
|
1056
1402
|
break;
|
|
1403
|
+
case "loop-complete":
|
|
1404
|
+
this.loopCompletions.push(event.loop);
|
|
1405
|
+
this.refreshHeader();
|
|
1406
|
+
break;
|
|
1057
1407
|
case "done":
|
|
1058
1408
|
this.phases.push({ kind: "done" });
|
|
1059
1409
|
break;
|
|
1060
|
-
case "scan-start":
|
|
1061
|
-
this.scanStarts.push(event.loop);
|
|
1062
|
-
break;
|
|
1063
1410
|
case "snapshot":
|
|
1064
|
-
case "detected":
|
|
1065
|
-
case "loop-complete": break;
|
|
1411
|
+
case "detected": break;
|
|
1066
1412
|
}
|
|
1067
1413
|
}
|
|
1068
1414
|
close() {
|
|
@@ -1090,6 +1436,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1090
1436
|
const list = new Listr([{
|
|
1091
1437
|
title: this.theme.dim("scanning…"),
|
|
1092
1438
|
task: async (_ctx, task) => {
|
|
1439
|
+
this.scanHeader = task;
|
|
1093
1440
|
const loop = await this.race(this.scanStarts.take());
|
|
1094
1441
|
if (loop === CLOSED) {
|
|
1095
1442
|
live = false;
|
|
@@ -1102,6 +1449,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1102
1449
|
return;
|
|
1103
1450
|
}
|
|
1104
1451
|
task.title = this.scannedTitle(audit);
|
|
1452
|
+
this.scanHeader = void 0;
|
|
1105
1453
|
}
|
|
1106
1454
|
}], this.listrOptions());
|
|
1107
1455
|
await list.run();
|
|
@@ -1116,10 +1464,11 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1116
1464
|
this.currentLoop = info.loop;
|
|
1117
1465
|
this.currentConcurrency = info.concurrency;
|
|
1118
1466
|
task.title = this.headerTitle();
|
|
1119
|
-
while (
|
|
1120
|
-
const
|
|
1121
|
-
if (
|
|
1467
|
+
while (true) {
|
|
1468
|
+
const tickOrComplete = await this.race(Promise.race([this.fixTicks.take().then(() => "tick"), this.loopCompletions.take().then(() => "complete")]));
|
|
1469
|
+
if (tickOrComplete === CLOSED) return;
|
|
1122
1470
|
task.title = this.headerTitle();
|
|
1471
|
+
if (tickOrComplete === "complete") break;
|
|
1123
1472
|
}
|
|
1124
1473
|
}
|
|
1125
1474
|
}], this.listrOptions());
|
|
@@ -1158,7 +1507,17 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1158
1507
|
return meta;
|
|
1159
1508
|
}
|
|
1160
1509
|
scanTitle(loop) {
|
|
1161
|
-
|
|
1510
|
+
const detail = this.scannerDetail();
|
|
1511
|
+
return this.theme.dim(loop === 1 ? `initial audit: scanning…${detail}` : `re-audit after fix pass ${loop - 1}: scanning…${detail}`);
|
|
1512
|
+
}
|
|
1513
|
+
scannerDetail() {
|
|
1514
|
+
const entries = [...this.scannerStates.entries()];
|
|
1515
|
+
if (entries.length === 0) return "";
|
|
1516
|
+
const running = entries.filter(([, info]) => info.status === "running");
|
|
1517
|
+
const done = entries.length - running.length;
|
|
1518
|
+
if (running.length === 0) return ` ${this.theme.glyph.bullet} scanners ${done}/${entries.length} done`;
|
|
1519
|
+
const runningTools = running.map(([tool]) => tool).join(", ");
|
|
1520
|
+
return ` ${this.theme.glyph.bullet} running ${runningTools} ${this.theme.glyph.bullet} ${done}/${entries.length} done`;
|
|
1162
1521
|
}
|
|
1163
1522
|
headerTitle() {
|
|
1164
1523
|
const running = Math.max(0, this.started - this.finished);
|
|
@@ -1173,12 +1532,17 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1173
1532
|
refreshHeader() {
|
|
1174
1533
|
if (this.header) this.header.title = this.headerTitle();
|
|
1175
1534
|
}
|
|
1535
|
+
refreshScanHeader() {
|
|
1536
|
+
if (this.scanHeader && this.currentScanLoop !== void 0) this.scanHeader.title = this.scanTitle(this.currentScanLoop);
|
|
1537
|
+
}
|
|
1176
1538
|
fileLabel(file) {
|
|
1177
1539
|
return basename(file).padEnd(this.labelWidth);
|
|
1178
1540
|
}
|
|
1179
1541
|
fileTitle(file) {
|
|
1180
1542
|
const rule = this.rules.get(file);
|
|
1181
|
-
const
|
|
1543
|
+
const stage = this.stages.get(file);
|
|
1544
|
+
const detail = [rule, stage ? fixStageLabel(stage) : void 0].filter(Boolean).join(" · ");
|
|
1545
|
+
const suffix = detail ? ` ${this.theme.dim(detail)}` : "";
|
|
1182
1546
|
return `${this.fileLabel(file)}${suffix}`;
|
|
1183
1547
|
}
|
|
1184
1548
|
};
|
|
@@ -1200,6 +1564,15 @@ var PlainReporter = class extends BaseReporter {
|
|
|
1200
1564
|
case "scan-start":
|
|
1201
1565
|
this.write(event.loop === 1 ? "initial audit: scanning…" : `re-audit after fix pass ${event.loop - 1}: scanning…`);
|
|
1202
1566
|
break;
|
|
1567
|
+
case "scanner-start":
|
|
1568
|
+
this.write(`scanner ${event.tool}: running`);
|
|
1569
|
+
break;
|
|
1570
|
+
case "scanner-result": {
|
|
1571
|
+
const count = event.status === "ran" ? ` ${event.findings} findings` : "";
|
|
1572
|
+
const reason = event.reason ? ` — ${event.reason}` : "";
|
|
1573
|
+
this.write(`scanner ${event.tool}: ${event.status}${count}${reason}`);
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1203
1576
|
case "audit": {
|
|
1204
1577
|
const scope = event.scanned != null ? `${event.scanned} files eligible for fixes` : "whole repo";
|
|
1205
1578
|
const phase = event.loop === 1 ? "initial audit" : `re-audit after fix pass ${event.loop - 1}`;
|
|
@@ -1214,6 +1587,9 @@ var PlainReporter = class extends BaseReporter {
|
|
|
1214
1587
|
else if (event.outcome === "reverted") this.write(`${glyph.reverted} reverted ${event.file} — ${reasonLabel(event.reason)}`);
|
|
1215
1588
|
else this.write(`${glyph.left} not attempted ${event.file}`);
|
|
1216
1589
|
break;
|
|
1590
|
+
case "file-stage":
|
|
1591
|
+
this.write(`progress ${event.file}: ${fixStageLabel(event.stage)}${event.detail ? ` (${event.detail})` : ""}`);
|
|
1592
|
+
break;
|
|
1217
1593
|
case "snapshot":
|
|
1218
1594
|
case "detected":
|
|
1219
1595
|
case "file-start":
|
|
@@ -1264,8 +1640,8 @@ function loadReport() {
|
|
|
1264
1640
|
* executes in). Files are re-based onto `root` so `vitest related` / `jest
|
|
1265
1641
|
* --findRelatedTests` resolve them inside the owning package, not the repo root.
|
|
1266
1642
|
*/
|
|
1267
|
-
async function runTests(runner, files, root) {
|
|
1268
|
-
const targets = toOwnerRelative(files,
|
|
1643
|
+
async function runTests(runner, files, root, repoRoot = cwd) {
|
|
1644
|
+
const targets = toOwnerRelative(files, repoRoot, root);
|
|
1269
1645
|
const args = runner === "vitest" ? [
|
|
1270
1646
|
"vitest",
|
|
1271
1647
|
"related",
|
|
@@ -1295,79 +1671,181 @@ async function runTests(runner, files, root) {
|
|
|
1295
1671
|
return [];
|
|
1296
1672
|
}
|
|
1297
1673
|
}
|
|
1298
|
-
async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd) {
|
|
1299
|
-
const typescript = detectTypeScript(ownerRoot);
|
|
1300
|
-
const runner = detectTestRunner(ownerRoot) ?? null;
|
|
1674
|
+
async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, bus, detected, sandboxPool) {
|
|
1675
|
+
const typescript = detected?.typescript ?? detectTypeScript(ownerRoot);
|
|
1676
|
+
const runner = detected?.runner ?? detectTestRunner(ownerRoot) ?? null;
|
|
1301
1677
|
const buildArgs = detectBuildCommand(ownerRoot);
|
|
1302
1678
|
const pm = detectPackageManager(ownerRoot);
|
|
1303
|
-
const baseline = new Set(runner && baselineTargets.length > 0 ? (await runTests(runner, baselineTargets, ownerRoot)).filter((t) => t.status === "pass").map((t) => t.name) : []);
|
|
1304
|
-
const
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
"
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const exitCode = r.exitCode ?? (r.timedOut ? 143 : r.failed ? 1 : 0);
|
|
1322
|
-
return {
|
|
1323
|
-
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
1324
|
-
exitCode
|
|
1325
|
-
};
|
|
1326
|
-
} });
|
|
1327
|
-
const gateDeps = {
|
|
1328
|
-
cwd,
|
|
1329
|
-
typescript,
|
|
1330
|
-
runTsc: async () => {
|
|
1331
|
-
const r = await execa("npx", ["tsc", "--noEmit"], {
|
|
1332
|
-
cwd: ownerRoot,
|
|
1679
|
+
const baseline = new Set(runner && baselineTargets.length > 0 ? (await runTests(runner, baselineTargets, ownerRoot, cwd)).filter((t) => t.status === "pass").map((t) => t.name) : []);
|
|
1680
|
+
const makeGateDeps = (sandbox) => {
|
|
1681
|
+
const repoRoot = sandbox?.cwd ?? cwd;
|
|
1682
|
+
const gateOwnerRoot = sandbox ? mapOwnerRoot(cwd, ownerRoot, sandbox.cwd) : ownerRoot;
|
|
1683
|
+
const session = new ClaudeSession({ spawn: async (req) => {
|
|
1684
|
+
const r = await execa("claude", [
|
|
1685
|
+
"-p",
|
|
1686
|
+
req.prompt,
|
|
1687
|
+
"--model",
|
|
1688
|
+
config.model,
|
|
1689
|
+
...config.effort ? ["--effort", config.effort] : [],
|
|
1690
|
+
"--output-format",
|
|
1691
|
+
"stream-json",
|
|
1692
|
+
"--verbose",
|
|
1693
|
+
"--allowedTools",
|
|
1694
|
+
"Read,Write,Edit"
|
|
1695
|
+
], {
|
|
1696
|
+
cwd: repoRoot,
|
|
1333
1697
|
reject: false,
|
|
1334
|
-
timeout:
|
|
1698
|
+
timeout: CLAUDE_TIMEOUT_MS
|
|
1335
1699
|
});
|
|
1700
|
+
const exitCode = r.exitCode ?? (r.timedOut ? 143 : r.failed ? 1 : 0);
|
|
1336
1701
|
return {
|
|
1337
|
-
|
|
1338
|
-
|
|
1702
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
1703
|
+
exitCode
|
|
1339
1704
|
};
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1705
|
+
} });
|
|
1706
|
+
return {
|
|
1707
|
+
cwd: repoRoot,
|
|
1708
|
+
typescript,
|
|
1709
|
+
runTsc: async () => {
|
|
1710
|
+
const r = await execa("npx", ["tsc", "--noEmit"], {
|
|
1711
|
+
cwd: gateOwnerRoot,
|
|
1712
|
+
reject: false,
|
|
1713
|
+
timeout: TSC_TIMEOUT_MS
|
|
1714
|
+
});
|
|
1715
|
+
return {
|
|
1716
|
+
exitCode: r.exitCode ?? 1,
|
|
1717
|
+
output: `${r.stdout}\n${r.stderr}`
|
|
1718
|
+
};
|
|
1719
|
+
},
|
|
1720
|
+
runBuild: buildArgs ? async () => {
|
|
1721
|
+
const r = await execa(pm, buildArgs, {
|
|
1722
|
+
cwd: gateOwnerRoot,
|
|
1723
|
+
reject: false,
|
|
1724
|
+
timeout: BUILD_TIMEOUT_MS
|
|
1725
|
+
});
|
|
1726
|
+
return {
|
|
1727
|
+
exitCode: r.exitCode ?? 1,
|
|
1728
|
+
output: `${r.stdout}\n${r.stderr}`
|
|
1729
|
+
};
|
|
1730
|
+
} : void 0,
|
|
1731
|
+
hasTestRunner: Boolean(runner),
|
|
1732
|
+
runRelated: (files) => runner ? runTests(runner, files, gateOwnerRoot, repoRoot) : Promise.resolve([]),
|
|
1733
|
+
scanFindings: async (files, tools) => (await scanFiles({
|
|
1734
|
+
cwd: repoRoot,
|
|
1735
|
+
which: realWhich,
|
|
1736
|
+
spawn: realSpawn,
|
|
1737
|
+
timeoutMs: 12e4,
|
|
1738
|
+
tools
|
|
1739
|
+
}, files, 0)).findings,
|
|
1740
|
+
baseline,
|
|
1741
|
+
session
|
|
1742
|
+
};
|
|
1743
|
+
};
|
|
1744
|
+
const mainGateDeps = makeGateDeps();
|
|
1745
|
+
const acceptedFiles = new Set();
|
|
1746
|
+
const acceptedTools = new Set();
|
|
1747
|
+
const buildFixUnit = (sandbox) => makeFixUnit({
|
|
1748
|
+
...makeGateDeps(sandbox),
|
|
1749
|
+
maxRepairs: 3,
|
|
1750
|
+
onProgress: (event) => bus?.emit({
|
|
1751
|
+
type: "file-stage",
|
|
1752
|
+
...event
|
|
1753
|
+
})
|
|
1754
|
+
});
|
|
1755
|
+
const fixUnit = async (unit, loop) => {
|
|
1756
|
+
if (!sandboxPool) return buildFixUnit()(unit, loop);
|
|
1757
|
+
try {
|
|
1758
|
+
return await sandboxPool.withSandbox(async (sandbox) => {
|
|
1759
|
+
const outcome = await buildFixUnit(sandbox)(unit, loop);
|
|
1760
|
+
if (!outcome.kept) return outcome;
|
|
1761
|
+
bus?.emit({
|
|
1762
|
+
type: "file-stage",
|
|
1763
|
+
loop,
|
|
1764
|
+
file: unit.file,
|
|
1765
|
+
stage: "patch-apply"
|
|
1766
|
+
});
|
|
1767
|
+
const patch = await sandbox.collectPatch(unit);
|
|
1768
|
+
if (!patch.ok) return {
|
|
1769
|
+
kept: false,
|
|
1770
|
+
reason: "unowned-patch",
|
|
1771
|
+
detail: patch.detail,
|
|
1772
|
+
failureClass: "unowned-patch",
|
|
1773
|
+
usage: outcome.usage
|
|
1774
|
+
};
|
|
1775
|
+
const applied = await sandboxPool.applyPatchToMain(patch.patch);
|
|
1776
|
+
if (!applied.ok) {
|
|
1777
|
+
bus?.emit({
|
|
1778
|
+
type: "file-stage",
|
|
1779
|
+
loop,
|
|
1780
|
+
file: unit.file,
|
|
1781
|
+
stage: "patch-conflict"
|
|
1782
|
+
});
|
|
1783
|
+
return {
|
|
1784
|
+
kept: false,
|
|
1785
|
+
reason: "patch-conflict",
|
|
1786
|
+
detail: applied.detail,
|
|
1787
|
+
failureClass: "patch-conflict",
|
|
1788
|
+
usage: outcome.usage
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
for (const file of patch.changedFiles) acceptedFiles.add(file);
|
|
1792
|
+
for (const finding of unit.findings) acceptedTools.add(finding.tool);
|
|
1793
|
+
return outcome;
|
|
1346
1794
|
});
|
|
1795
|
+
} catch (error) {
|
|
1796
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1797
|
+
const setupFailed = error instanceof SandboxSetupError;
|
|
1347
1798
|
return {
|
|
1348
|
-
|
|
1349
|
-
|
|
1799
|
+
kept: false,
|
|
1800
|
+
reason: setupFailed ? "sandbox-setup-failed" : "session-error",
|
|
1801
|
+
detail,
|
|
1802
|
+
failureClass: setupFailed ? "sandbox-setup-failed" : "model-tool-failure",
|
|
1803
|
+
usage: zeroUsage()
|
|
1350
1804
|
};
|
|
1351
|
-
}
|
|
1352
|
-
hasTestRunner: Boolean(runner),
|
|
1353
|
-
runRelated: (files) => runner ? runTests(runner, files, ownerRoot) : Promise.resolve([]),
|
|
1354
|
-
scanFindings: async (files) => (await scanFiles({
|
|
1355
|
-
cwd,
|
|
1356
|
-
which: realWhich,
|
|
1357
|
-
spawn: realSpawn,
|
|
1358
|
-
timeoutMs: 12e4
|
|
1359
|
-
}, files, 0)).findings,
|
|
1360
|
-
baseline
|
|
1805
|
+
}
|
|
1361
1806
|
};
|
|
1362
1807
|
return {
|
|
1363
1808
|
typescript,
|
|
1364
1809
|
runner,
|
|
1365
|
-
fixUnit
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1810
|
+
fixUnit,
|
|
1811
|
+
deterministicFixUnit: makeDeterministicFixUnit(mainGateDeps),
|
|
1812
|
+
finalIntegration: async () => {
|
|
1813
|
+
const files = [...acceptedFiles].sort();
|
|
1814
|
+
if (files.length === 0) return {
|
|
1815
|
+
ok: true,
|
|
1816
|
+
files
|
|
1817
|
+
};
|
|
1818
|
+
if (typescript) {
|
|
1819
|
+
const tc = await mainGateDeps.runTsc();
|
|
1820
|
+
if (tc.exitCode !== 0) return {
|
|
1821
|
+
ok: false,
|
|
1822
|
+
files,
|
|
1823
|
+
detail: `final integration typecheck failed: ${tc.output}`
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
if (runner) {
|
|
1827
|
+
const tests = await mainGateDeps.runRelated(files);
|
|
1828
|
+
const failed = tests.filter((test) => test.status === "fail");
|
|
1829
|
+
if (failed.length > 0) return {
|
|
1830
|
+
ok: false,
|
|
1831
|
+
files,
|
|
1832
|
+
detail: `final integration related tests failed: ${failed.map((test) => test.name).join(", ")}`
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
const tools = [...acceptedTools];
|
|
1836
|
+
if (tools.length > 0) {
|
|
1837
|
+
const findings = await mainGateDeps.scanFindings(files, tools);
|
|
1838
|
+
if (findings.length > 0) return {
|
|
1839
|
+
ok: false,
|
|
1840
|
+
files,
|
|
1841
|
+
detail: `final integration scanner rescan found ${findings.length} finding${findings.length === 1 ? "" : "s"}`
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
ok: true,
|
|
1846
|
+
files
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1371
1849
|
};
|
|
1372
1850
|
}
|
|
1373
1851
|
function describeScopeNote(all, paths, scope) {
|
|
@@ -1389,6 +1867,8 @@ async function runRun(opts) {
|
|
|
1389
1867
|
write: out
|
|
1390
1868
|
});
|
|
1391
1869
|
reporter.start();
|
|
1870
|
+
const bus = new EventBus();
|
|
1871
|
+
bus.on((e) => reporter.onEvent(e));
|
|
1392
1872
|
const git = createGit(cwd);
|
|
1393
1873
|
await assertGitRepo(git);
|
|
1394
1874
|
if (opts.effort && !EFFORT_LEVELS.includes(opts.effort)) {
|
|
@@ -1427,16 +1907,27 @@ async function runRun(opts) {
|
|
|
1427
1907
|
} else scope = await changedVsHead(git);
|
|
1428
1908
|
const baselineTargets = scope ?? ["."];
|
|
1429
1909
|
const ownerRoot = scope ? resolveOwnerRoot(cwd, scope) : cwd;
|
|
1430
|
-
const
|
|
1910
|
+
const typescript = detectTypeScript(ownerRoot);
|
|
1911
|
+
const runner = detectTestRunner(ownerRoot) ?? null;
|
|
1431
1912
|
const pm = detectPackageManager(cwd);
|
|
1432
1913
|
reporter.note(`${pm} · ${typescript ? "TypeScript" : "JavaScript"} · ${runner ?? "no test runner"} · ${modelLabel}`);
|
|
1433
1914
|
const scopeNote = describeScopeNote(opts.all, paths, scope);
|
|
1434
1915
|
reporter.note(`${scopeNote} · ${plural(available.length, "scanner")}`);
|
|
1435
|
-
|
|
1436
|
-
|
|
1916
|
+
if (runner && baselineTargets.length > 0) reporter.note(`baseline: ${runner} related ${describeScopeNote(opts.all, paths, scope)} (one-time)`);
|
|
1917
|
+
const sandboxPool = new WorkerSandboxPool({
|
|
1918
|
+
mainRoot: snapshot.repoRoot(),
|
|
1919
|
+
snapshotSha: snapshot.commitSha(),
|
|
1920
|
+
maxSandboxes: config.maxSessions,
|
|
1921
|
+
packageManager: pm
|
|
1922
|
+
});
|
|
1923
|
+
const { fixUnit, deterministicFixUnit, finalIntegration } = await makeProductionFixUnit(config, baselineTargets, ownerRoot, bus, {
|
|
1924
|
+
typescript,
|
|
1925
|
+
runner
|
|
1926
|
+
}, sandboxPool);
|
|
1437
1927
|
const start = Date.now();
|
|
1438
1928
|
const drawing = reporter.run();
|
|
1439
1929
|
let result;
|
|
1930
|
+
let finalIntegrationResult;
|
|
1440
1931
|
try {
|
|
1441
1932
|
result = await orchestrate({
|
|
1442
1933
|
cwd,
|
|
@@ -1445,7 +1936,8 @@ async function runRun(opts) {
|
|
|
1445
1936
|
which: realWhich,
|
|
1446
1937
|
spawn: realSpawn,
|
|
1447
1938
|
scope,
|
|
1448
|
-
timeoutMs: 12e4
|
|
1939
|
+
timeoutMs: 12e4,
|
|
1940
|
+
bus
|
|
1449
1941
|
}),
|
|
1450
1942
|
fixUnit,
|
|
1451
1943
|
deterministicFixUnit,
|
|
@@ -1453,7 +1945,10 @@ async function runRun(opts) {
|
|
|
1453
1945
|
inScope: scope ? (fs) => filterToChanged(fs, scope) : void 0,
|
|
1454
1946
|
bus
|
|
1455
1947
|
});
|
|
1948
|
+
finalIntegrationResult = await finalIntegration();
|
|
1949
|
+
if (!finalIntegrationResult.ok) result.exitStatus = 1;
|
|
1456
1950
|
} finally {
|
|
1951
|
+
await sandboxPool.dispose();
|
|
1457
1952
|
reporter.close();
|
|
1458
1953
|
}
|
|
1459
1954
|
await drawing;
|
|
@@ -1473,7 +1968,8 @@ async function runRun(opts) {
|
|
|
1473
1968
|
exclude: config.fix.exclude,
|
|
1474
1969
|
includeGenerated: config.fix.includeGenerated,
|
|
1475
1970
|
includeFixtures: config.fix.includeFixtures
|
|
1476
|
-
}
|
|
1971
|
+
},
|
|
1972
|
+
finalIntegration: finalIntegrationResult
|
|
1477
1973
|
});
|
|
1478
1974
|
persist(REPORT_PATH, report);
|
|
1479
1975
|
out("");
|
|
@@ -1496,6 +1992,7 @@ async function runRetry(id) {
|
|
|
1496
1992
|
}
|
|
1497
1993
|
const config = await loadConfig(cwd);
|
|
1498
1994
|
let snapshotSaved = false;
|
|
1995
|
+
let retrySandboxPool;
|
|
1499
1996
|
const result = await retryCommand(id, {
|
|
1500
1997
|
report,
|
|
1501
1998
|
baseBudget: config.perIssueBudget,
|
|
@@ -1517,12 +2014,20 @@ async function runRetry(id) {
|
|
|
1517
2014
|
if (!snapshotSaved) {
|
|
1518
2015
|
const snapshot = await Snapshot.capture(git, cwd);
|
|
1519
2016
|
persist(SNAPSHOT_PATH, snapshot.toJSON());
|
|
2017
|
+
retrySandboxPool = new WorkerSandboxPool({
|
|
2018
|
+
mainRoot: snapshot.repoRoot(),
|
|
2019
|
+
snapshotSha: snapshot.commitSha(),
|
|
2020
|
+
maxSandboxes: config.maxSessions,
|
|
2021
|
+
packageManager: detectPackageManager(cwd)
|
|
2022
|
+
});
|
|
1520
2023
|
snapshotSaved = true;
|
|
1521
2024
|
}
|
|
1522
2025
|
const ownerRoot = resolveOwnerRoot(cwd, unit.files);
|
|
1523
|
-
const { fixUnit } = await makeProductionFixUnit(config, unit.files, ownerRoot);
|
|
2026
|
+
const { fixUnit } = await makeProductionFixUnit(config, unit.files, ownerRoot, void 0, void 0, retrySandboxPool);
|
|
1524
2027
|
return fixUnit(unit, 1);
|
|
1525
2028
|
}
|
|
2029
|
+
}).finally(async () => {
|
|
2030
|
+
await retrySandboxPool?.dispose();
|
|
1526
2031
|
});
|
|
1527
2032
|
if ("error" in result) {
|
|
1528
2033
|
err(`✖ ${result.error}`);
|