tend-cli 0.6.1 → 0.8.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 +643 -90
- package/dist/{config-Dz4byqjU.js → config-BeA71px2.js} +91 -19
- package/dist/index.d.ts +125 -39
- package/dist/index.js +1 -1
- package/package.json +1 -1
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-BeA71px2.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,288 @@ function makeFixUnit(deps) {
|
|
|
904
957
|
};
|
|
905
958
|
}
|
|
906
959
|
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/fixing/thinking-budget.ts
|
|
962
|
+
/** Thinking disabled — mechanical fixes don't need reasoning tokens. */
|
|
963
|
+
const THINKING_OFF = 0;
|
|
964
|
+
/**
|
|
965
|
+
* Upper bound on extended-thinking tokens per fix session. Unbounded thinking
|
|
966
|
+
* measured ~7000 tokens per finding regardless of difficulty; capping it keeps
|
|
967
|
+
* reasoning fixes correct (gate stays green) at a fraction of the latency.
|
|
968
|
+
*/
|
|
969
|
+
const THINKING_BUDGET_CAP = 4096;
|
|
970
|
+
function isMechanical(finding) {
|
|
971
|
+
return finding.category === "dead-code" || finding.autofixable === true;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Decide the extended-thinking token budget for one finding's fix session.
|
|
975
|
+
* Mechanical findings (dead-code removal, autofixable rules) get thinking off;
|
|
976
|
+
* everything else — reasoning findings and any unrecognized category — gets the
|
|
977
|
+
* bounded cap (the safe default: never spend more than the cap, never starve a
|
|
978
|
+
* finding that might need reasoning). A configured `thinkingBudget` overrides
|
|
979
|
+
* the policy outright, with 0 meaning thinking off.
|
|
980
|
+
*/
|
|
981
|
+
function thinkingBudgetFor(finding, config) {
|
|
982
|
+
if (config?.thinkingBudget !== void 0) return config.thinkingBudget;
|
|
983
|
+
return isMechanical(finding) ? THINKING_OFF : THINKING_BUDGET_CAP;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Budget for a whole work unit (one file's findings). Takes the most-conservative
|
|
987
|
+
* (largest) per-finding budget so a reasoning finding is never starved of thinking
|
|
988
|
+
* just because it shares the file with a mechanical one. An empty unit and any
|
|
989
|
+
* unrecognized category fall back to the cap. A configured budget overrides all.
|
|
990
|
+
*/
|
|
991
|
+
function thinkingBudgetForUnit(findings, config) {
|
|
992
|
+
if (config?.thinkingBudget !== void 0) return config.thinkingBudget;
|
|
993
|
+
if (findings.length === 0) return THINKING_BUDGET_CAP;
|
|
994
|
+
return Math.max(...findings.map((finding) => thinkingBudgetFor(finding)));
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Delivery to the `claude -p` session boundary: the env overlay that pins the
|
|
998
|
+
* session's extended-thinking budget. Spread onto the child process env.
|
|
999
|
+
*/
|
|
1000
|
+
function thinkingEnv(findings, config) {
|
|
1001
|
+
return { MAX_THINKING_TOKENS: String(thinkingBudgetForUnit(findings, config)) };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
//#endregion
|
|
1005
|
+
//#region src/fixing/worker-sandbox.ts
|
|
1006
|
+
var SandboxSetupError = class extends Error {
|
|
1007
|
+
constructor(message) {
|
|
1008
|
+
super(message);
|
|
1009
|
+
this.name = "SandboxSetupError";
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
const cleanExcludes = [
|
|
1013
|
+
"node_modules",
|
|
1014
|
+
"node_modules/**",
|
|
1015
|
+
"**/node_modules",
|
|
1016
|
+
"**/node_modules/**"
|
|
1017
|
+
];
|
|
1018
|
+
function normalizeRel(path) {
|
|
1019
|
+
return path.replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
1020
|
+
}
|
|
1021
|
+
function lines(raw) {
|
|
1022
|
+
return raw.split("\n").map((line) => normalizeRel(line.trim())).filter(Boolean);
|
|
1023
|
+
}
|
|
1024
|
+
function unique(values) {
|
|
1025
|
+
return [...new Set(values.map(normalizeRel))];
|
|
1026
|
+
}
|
|
1027
|
+
function isGeneratedRepair(unit) {
|
|
1028
|
+
return unit.strategy === "generated-source-repair" || unit.strategies?.includes("generated-source-repair") === true;
|
|
1029
|
+
}
|
|
1030
|
+
function allowedPatchFiles(unit) {
|
|
1031
|
+
return unique([...unit.files, ...isGeneratedRepair(unit) ? unit.verificationTargets ?? [] : []]);
|
|
1032
|
+
}
|
|
1033
|
+
function installArgs(pm) {
|
|
1034
|
+
switch (pm) {
|
|
1035
|
+
case "pnpm": return [
|
|
1036
|
+
"install",
|
|
1037
|
+
"--frozen-lockfile",
|
|
1038
|
+
"--prefer-offline"
|
|
1039
|
+
];
|
|
1040
|
+
case "yarn": return [
|
|
1041
|
+
"install",
|
|
1042
|
+
"--frozen-lockfile",
|
|
1043
|
+
"--prefer-offline"
|
|
1044
|
+
];
|
|
1045
|
+
case "bun": return ["install", "--frozen-lockfile"];
|
|
1046
|
+
case "npm": return ["ci", "--prefer-offline"];
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
var GitWorkerSandbox = class {
|
|
1050
|
+
prepared = false;
|
|
1051
|
+
constructor(cwd$1, deps) {
|
|
1052
|
+
this.cwd = cwd$1;
|
|
1053
|
+
this.deps = deps;
|
|
1054
|
+
}
|
|
1055
|
+
async reset() {
|
|
1056
|
+
const git = createGit(this.cwd);
|
|
1057
|
+
await git.raw([
|
|
1058
|
+
"reset",
|
|
1059
|
+
"--hard",
|
|
1060
|
+
this.deps.snapshotSha
|
|
1061
|
+
]);
|
|
1062
|
+
await git.raw([
|
|
1063
|
+
"clean",
|
|
1064
|
+
"-ffdx",
|
|
1065
|
+
...cleanExcludes.flatMap((pattern) => ["-e", pattern])
|
|
1066
|
+
]);
|
|
1067
|
+
}
|
|
1068
|
+
async prepare() {
|
|
1069
|
+
if (this.prepared || this.deps.prepareDependencies === false) return;
|
|
1070
|
+
const args = installArgs(this.deps.packageManager);
|
|
1071
|
+
const result = await this.deps.exec(this.deps.packageManager, args, {
|
|
1072
|
+
cwd: this.cwd,
|
|
1073
|
+
reject: false,
|
|
1074
|
+
timeout: 10 * 6e4
|
|
1075
|
+
});
|
|
1076
|
+
if ((result.exitCode ?? 1) !== 0) throw new SandboxSetupError(`sandbox dependency install failed: ${result.stderr || result.stdout || `exit ${result.exitCode ?? 1}`}`);
|
|
1077
|
+
this.prepared = true;
|
|
1078
|
+
}
|
|
1079
|
+
async collectPatch(unit) {
|
|
1080
|
+
const git = createGit(this.cwd);
|
|
1081
|
+
const tracked = lines(await git.raw([
|
|
1082
|
+
"diff",
|
|
1083
|
+
"--name-only",
|
|
1084
|
+
this.deps.snapshotSha
|
|
1085
|
+
]));
|
|
1086
|
+
const untracked = lines(await git.raw([
|
|
1087
|
+
"ls-files",
|
|
1088
|
+
"--others",
|
|
1089
|
+
"--exclude-standard"
|
|
1090
|
+
]));
|
|
1091
|
+
const changedFiles = unique([...tracked, ...untracked]).sort();
|
|
1092
|
+
const allowed = new Set(allowedPatchFiles(unit));
|
|
1093
|
+
const unowned = changedFiles.filter((file) => !allowed.has(file));
|
|
1094
|
+
if (unowned.length > 0) return {
|
|
1095
|
+
ok: false,
|
|
1096
|
+
reason: "unowned-patch",
|
|
1097
|
+
detail: `Worker modified unowned files: ${unowned.join(", ")}`,
|
|
1098
|
+
changedFiles
|
|
1099
|
+
};
|
|
1100
|
+
const allowedFiles = [...allowed].sort();
|
|
1101
|
+
if (allowedFiles.length === 0 || changedFiles.length === 0) return {
|
|
1102
|
+
ok: true,
|
|
1103
|
+
patch: "",
|
|
1104
|
+
changedFiles
|
|
1105
|
+
};
|
|
1106
|
+
if (untracked.length > 0) await git.raw([
|
|
1107
|
+
"add",
|
|
1108
|
+
"-N",
|
|
1109
|
+
"--",
|
|
1110
|
+
...untracked.filter((file) => allowed.has(file))
|
|
1111
|
+
]);
|
|
1112
|
+
const patch = await git.raw([
|
|
1113
|
+
"diff",
|
|
1114
|
+
"--binary",
|
|
1115
|
+
this.deps.snapshotSha,
|
|
1116
|
+
"--",
|
|
1117
|
+
...allowedFiles
|
|
1118
|
+
]);
|
|
1119
|
+
return {
|
|
1120
|
+
ok: true,
|
|
1121
|
+
patch,
|
|
1122
|
+
changedFiles
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
async dispose() {
|
|
1126
|
+
const git = createGit(this.deps.mainRoot);
|
|
1127
|
+
try {
|
|
1128
|
+
await git.raw([
|
|
1129
|
+
"worktree",
|
|
1130
|
+
"remove",
|
|
1131
|
+
"--force",
|
|
1132
|
+
this.cwd
|
|
1133
|
+
]);
|
|
1134
|
+
} finally {
|
|
1135
|
+
rmSync(this.cwd, {
|
|
1136
|
+
recursive: true,
|
|
1137
|
+
force: true
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
var WorkerSandboxPool = class {
|
|
1143
|
+
queue;
|
|
1144
|
+
applyQueue = new PQueue({ concurrency: 1 });
|
|
1145
|
+
idle = [];
|
|
1146
|
+
sandboxes = new Set();
|
|
1147
|
+
counter = 0;
|
|
1148
|
+
disposed = false;
|
|
1149
|
+
exec;
|
|
1150
|
+
constructor(deps) {
|
|
1151
|
+
this.deps = deps;
|
|
1152
|
+
this.queue = new PQueue({ concurrency: deps.maxSandboxes });
|
|
1153
|
+
this.exec = deps.exec ?? execa;
|
|
1154
|
+
}
|
|
1155
|
+
async withSandbox(run) {
|
|
1156
|
+
return this.queue.add(async () => {
|
|
1157
|
+
let sandbox;
|
|
1158
|
+
try {
|
|
1159
|
+
sandbox = await this.acquire();
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
throw new SandboxSetupError(error instanceof Error ? error.message : String(error));
|
|
1162
|
+
}
|
|
1163
|
+
try {
|
|
1164
|
+
await sandbox.reset();
|
|
1165
|
+
await sandbox.prepare();
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
this.idle.push(sandbox);
|
|
1168
|
+
throw error instanceof SandboxSetupError ? error : new SandboxSetupError(error instanceof Error ? error.message : String(error));
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
return await run(sandbox);
|
|
1172
|
+
} finally {
|
|
1173
|
+
this.idle.push(sandbox);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
async applyPatchToMain(patch) {
|
|
1178
|
+
return this.applyQueue.add(async () => {
|
|
1179
|
+
if (patch.trim() === "") return { ok: true };
|
|
1180
|
+
const check = await this.exec("git", [
|
|
1181
|
+
"apply",
|
|
1182
|
+
"--check",
|
|
1183
|
+
"--3way"
|
|
1184
|
+
], {
|
|
1185
|
+
cwd: this.deps.mainRoot,
|
|
1186
|
+
input: patch,
|
|
1187
|
+
reject: false
|
|
1188
|
+
});
|
|
1189
|
+
if ((check.exitCode ?? 1) !== 0) return {
|
|
1190
|
+
ok: false,
|
|
1191
|
+
reason: "patch-conflict",
|
|
1192
|
+
detail: check.stderr || check.stdout || "git apply --check --3way failed"
|
|
1193
|
+
};
|
|
1194
|
+
const applied = await this.exec("git", ["apply", "--3way"], {
|
|
1195
|
+
cwd: this.deps.mainRoot,
|
|
1196
|
+
input: patch,
|
|
1197
|
+
reject: false
|
|
1198
|
+
});
|
|
1199
|
+
if ((applied.exitCode ?? 1) !== 0) return {
|
|
1200
|
+
ok: false,
|
|
1201
|
+
reason: "patch-conflict",
|
|
1202
|
+
detail: applied.stderr || applied.stdout || "git apply --3way failed"
|
|
1203
|
+
};
|
|
1204
|
+
return { ok: true };
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
async dispose() {
|
|
1208
|
+
if (this.disposed) return;
|
|
1209
|
+
this.disposed = true;
|
|
1210
|
+
await this.queue.onIdle();
|
|
1211
|
+
await Promise.all([...this.sandboxes].map((sandbox) => sandbox.dispose()));
|
|
1212
|
+
this.idle.length = 0;
|
|
1213
|
+
this.sandboxes.clear();
|
|
1214
|
+
}
|
|
1215
|
+
async acquire() {
|
|
1216
|
+
const existing = this.idle.pop();
|
|
1217
|
+
if (existing) return existing;
|
|
1218
|
+
const parent = this.deps.tempRoot ?? tmpdir();
|
|
1219
|
+
mkdirSync(parent, { recursive: true });
|
|
1220
|
+
const path = `${parent}/tend-worker-${process.pid}-${this.counter++}`;
|
|
1221
|
+
const mainGit = createGit(this.deps.mainRoot);
|
|
1222
|
+
await mainGit.raw([
|
|
1223
|
+
"worktree",
|
|
1224
|
+
"add",
|
|
1225
|
+
"--detach",
|
|
1226
|
+
path,
|
|
1227
|
+
this.deps.snapshotSha
|
|
1228
|
+
]);
|
|
1229
|
+
const sandbox = new GitWorkerSandbox(path, {
|
|
1230
|
+
...this.deps,
|
|
1231
|
+
exec: this.exec
|
|
1232
|
+
});
|
|
1233
|
+
this.sandboxes.add(sandbox);
|
|
1234
|
+
return sandbox;
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
function mapOwnerRoot(mainRoot, mainOwnerRoot, sandboxRoot) {
|
|
1238
|
+
const rel = normalizeRel(relative(mainRoot, mainOwnerRoot));
|
|
1239
|
+
return rel === "" ? sandboxRoot : `${sandboxRoot}/${rel}`;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
907
1242
|
//#endregion
|
|
908
1243
|
//#region src/output/env.ts
|
|
909
1244
|
/** Truthy in the env-var sense: present and not an explicit off value. */
|
|
@@ -939,6 +1274,28 @@ function detectOutputEnv(input = {}) {
|
|
|
939
1274
|
};
|
|
940
1275
|
}
|
|
941
1276
|
|
|
1277
|
+
//#endregion
|
|
1278
|
+
//#region src/fixing/progress.ts
|
|
1279
|
+
const LABELS = {
|
|
1280
|
+
"ai-edit": "AI edit",
|
|
1281
|
+
"ai-no-edit-retry": "AI retry",
|
|
1282
|
+
"anti-suppression": "suppression check",
|
|
1283
|
+
typecheck: "typecheck",
|
|
1284
|
+
build: "build",
|
|
1285
|
+
"related-tests": "related tests",
|
|
1286
|
+
"test-repair": "test repair",
|
|
1287
|
+
rescan: "rescan",
|
|
1288
|
+
"regression-check": "regression check",
|
|
1289
|
+
"regression-repair": "regression repair",
|
|
1290
|
+
"patch-apply": "patch apply",
|
|
1291
|
+
"patch-conflict": "patch conflict",
|
|
1292
|
+
"sandbox-setup": "sandbox setup",
|
|
1293
|
+
"final-integration": "final integration"
|
|
1294
|
+
};
|
|
1295
|
+
function fixStageLabel(stage) {
|
|
1296
|
+
return LABELS[stage];
|
|
1297
|
+
}
|
|
1298
|
+
|
|
942
1299
|
//#endregion
|
|
943
1300
|
//#region src/output/base-reporter.ts
|
|
944
1301
|
var BaseReporter = class {
|
|
@@ -987,6 +1344,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
987
1344
|
audits = new Channel();
|
|
988
1345
|
phases = new Channel();
|
|
989
1346
|
fixTicks = new Channel();
|
|
1347
|
+
loopCompletions = new Channel();
|
|
990
1348
|
closed = false;
|
|
991
1349
|
resolveClosed;
|
|
992
1350
|
closedSignal = new Promise((resolve$1) => {
|
|
@@ -1002,7 +1360,11 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1002
1360
|
currentFile;
|
|
1003
1361
|
currentConcurrency;
|
|
1004
1362
|
rules = new Map();
|
|
1363
|
+
stages = new Map();
|
|
1364
|
+
scannerStates = new Map();
|
|
1365
|
+
currentScanLoop;
|
|
1005
1366
|
header;
|
|
1367
|
+
scanHeader;
|
|
1006
1368
|
labelWidth = 0;
|
|
1007
1369
|
constructor(deps) {
|
|
1008
1370
|
super(deps);
|
|
@@ -1018,6 +1380,26 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1018
1380
|
scanned: event.scanned
|
|
1019
1381
|
});
|
|
1020
1382
|
break;
|
|
1383
|
+
case "scan-start":
|
|
1384
|
+
this.currentScanLoop = event.loop;
|
|
1385
|
+
this.scannerStates.clear();
|
|
1386
|
+
this.scanStarts.push(event.loop);
|
|
1387
|
+
this.refreshScanHeader();
|
|
1388
|
+
break;
|
|
1389
|
+
case "scanner-start":
|
|
1390
|
+
if (event.loop !== this.currentScanLoop) break;
|
|
1391
|
+
this.scannerStates.set(event.tool, { status: "running" });
|
|
1392
|
+
this.refreshScanHeader();
|
|
1393
|
+
break;
|
|
1394
|
+
case "scanner-result":
|
|
1395
|
+
if (event.loop !== this.currentScanLoop) break;
|
|
1396
|
+
this.scannerStates.set(event.tool, {
|
|
1397
|
+
status: event.status,
|
|
1398
|
+
findings: event.findings,
|
|
1399
|
+
reason: event.reason
|
|
1400
|
+
});
|
|
1401
|
+
this.refreshScanHeader();
|
|
1402
|
+
break;
|
|
1021
1403
|
case "loop-start":
|
|
1022
1404
|
this.currentLoop = event.loop;
|
|
1023
1405
|
this.fixTotal = event.files.length;
|
|
@@ -1029,6 +1411,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1029
1411
|
this.currentFile = void 0;
|
|
1030
1412
|
this.currentConcurrency = event.concurrency;
|
|
1031
1413
|
this.rules.clear();
|
|
1414
|
+
this.stages.clear();
|
|
1032
1415
|
this.labelWidth = Math.max(0, ...event.files.map((f) => basename(f).length));
|
|
1033
1416
|
this.phases.push({
|
|
1034
1417
|
kind: "fix",
|
|
@@ -1041,12 +1424,19 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1041
1424
|
break;
|
|
1042
1425
|
case "file-start":
|
|
1043
1426
|
this.started += 1;
|
|
1427
|
+
this.fixTotal = Math.max(this.fixTotal, this.started);
|
|
1044
1428
|
this.currentFile = event.file;
|
|
1045
1429
|
if (event.rule) this.rules.set(event.file, event.rule);
|
|
1046
1430
|
this.refreshHeader();
|
|
1047
1431
|
break;
|
|
1432
|
+
case "file-stage":
|
|
1433
|
+
this.currentFile = event.file;
|
|
1434
|
+
this.stages.set(event.file, event.stage);
|
|
1435
|
+
this.refreshHeader();
|
|
1436
|
+
break;
|
|
1048
1437
|
case "file-result":
|
|
1049
1438
|
this.finished += 1;
|
|
1439
|
+
this.fixTotal = Math.max(this.fixTotal, this.started, this.finished);
|
|
1050
1440
|
if (event.outcome === "fixed") this.fixed += 1;
|
|
1051
1441
|
else if (event.outcome === "reverted") this.reverted += 1;
|
|
1052
1442
|
else this.notAttempted += 1;
|
|
@@ -1054,15 +1444,15 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1054
1444
|
this.refreshHeader();
|
|
1055
1445
|
this.fixTicks.push();
|
|
1056
1446
|
break;
|
|
1447
|
+
case "loop-complete":
|
|
1448
|
+
this.loopCompletions.push(event.loop);
|
|
1449
|
+
this.refreshHeader();
|
|
1450
|
+
break;
|
|
1057
1451
|
case "done":
|
|
1058
1452
|
this.phases.push({ kind: "done" });
|
|
1059
1453
|
break;
|
|
1060
|
-
case "scan-start":
|
|
1061
|
-
this.scanStarts.push(event.loop);
|
|
1062
|
-
break;
|
|
1063
1454
|
case "snapshot":
|
|
1064
|
-
case "detected":
|
|
1065
|
-
case "loop-complete": break;
|
|
1455
|
+
case "detected": break;
|
|
1066
1456
|
}
|
|
1067
1457
|
}
|
|
1068
1458
|
close() {
|
|
@@ -1090,6 +1480,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1090
1480
|
const list = new Listr([{
|
|
1091
1481
|
title: this.theme.dim("scanning…"),
|
|
1092
1482
|
task: async (_ctx, task) => {
|
|
1483
|
+
this.scanHeader = task;
|
|
1093
1484
|
const loop = await this.race(this.scanStarts.take());
|
|
1094
1485
|
if (loop === CLOSED) {
|
|
1095
1486
|
live = false;
|
|
@@ -1102,6 +1493,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1102
1493
|
return;
|
|
1103
1494
|
}
|
|
1104
1495
|
task.title = this.scannedTitle(audit);
|
|
1496
|
+
this.scanHeader = void 0;
|
|
1105
1497
|
}
|
|
1106
1498
|
}], this.listrOptions());
|
|
1107
1499
|
await list.run();
|
|
@@ -1116,10 +1508,11 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1116
1508
|
this.currentLoop = info.loop;
|
|
1117
1509
|
this.currentConcurrency = info.concurrency;
|
|
1118
1510
|
task.title = this.headerTitle();
|
|
1119
|
-
while (
|
|
1120
|
-
const
|
|
1121
|
-
if (
|
|
1511
|
+
while (true) {
|
|
1512
|
+
const tickOrComplete = await this.race(Promise.race([this.fixTicks.take().then(() => "tick"), this.loopCompletions.take().then(() => "complete")]));
|
|
1513
|
+
if (tickOrComplete === CLOSED) return;
|
|
1122
1514
|
task.title = this.headerTitle();
|
|
1515
|
+
if (tickOrComplete === "complete") break;
|
|
1123
1516
|
}
|
|
1124
1517
|
}
|
|
1125
1518
|
}], this.listrOptions());
|
|
@@ -1158,7 +1551,17 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1158
1551
|
return meta;
|
|
1159
1552
|
}
|
|
1160
1553
|
scanTitle(loop) {
|
|
1161
|
-
|
|
1554
|
+
const detail = this.scannerDetail();
|
|
1555
|
+
return this.theme.dim(loop === 1 ? `initial audit: scanning…${detail}` : `re-audit after fix pass ${loop - 1}: scanning…${detail}`);
|
|
1556
|
+
}
|
|
1557
|
+
scannerDetail() {
|
|
1558
|
+
const entries = [...this.scannerStates.entries()];
|
|
1559
|
+
if (entries.length === 0) return "";
|
|
1560
|
+
const running = entries.filter(([, info]) => info.status === "running");
|
|
1561
|
+
const done = entries.length - running.length;
|
|
1562
|
+
if (running.length === 0) return ` ${this.theme.glyph.bullet} scanners ${done}/${entries.length} done`;
|
|
1563
|
+
const runningTools = running.map(([tool]) => tool).join(", ");
|
|
1564
|
+
return ` ${this.theme.glyph.bullet} running ${runningTools} ${this.theme.glyph.bullet} ${done}/${entries.length} done`;
|
|
1162
1565
|
}
|
|
1163
1566
|
headerTitle() {
|
|
1164
1567
|
const running = Math.max(0, this.started - this.finished);
|
|
@@ -1173,12 +1576,17 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1173
1576
|
refreshHeader() {
|
|
1174
1577
|
if (this.header) this.header.title = this.headerTitle();
|
|
1175
1578
|
}
|
|
1579
|
+
refreshScanHeader() {
|
|
1580
|
+
if (this.scanHeader && this.currentScanLoop !== void 0) this.scanHeader.title = this.scanTitle(this.currentScanLoop);
|
|
1581
|
+
}
|
|
1176
1582
|
fileLabel(file) {
|
|
1177
1583
|
return basename(file).padEnd(this.labelWidth);
|
|
1178
1584
|
}
|
|
1179
1585
|
fileTitle(file) {
|
|
1180
1586
|
const rule = this.rules.get(file);
|
|
1181
|
-
const
|
|
1587
|
+
const stage = this.stages.get(file);
|
|
1588
|
+
const detail = [rule, stage ? fixStageLabel(stage) : void 0].filter(Boolean).join(" · ");
|
|
1589
|
+
const suffix = detail ? ` ${this.theme.dim(detail)}` : "";
|
|
1182
1590
|
return `${this.fileLabel(file)}${suffix}`;
|
|
1183
1591
|
}
|
|
1184
1592
|
};
|
|
@@ -1200,6 +1608,15 @@ var PlainReporter = class extends BaseReporter {
|
|
|
1200
1608
|
case "scan-start":
|
|
1201
1609
|
this.write(event.loop === 1 ? "initial audit: scanning…" : `re-audit after fix pass ${event.loop - 1}: scanning…`);
|
|
1202
1610
|
break;
|
|
1611
|
+
case "scanner-start":
|
|
1612
|
+
this.write(`scanner ${event.tool}: running`);
|
|
1613
|
+
break;
|
|
1614
|
+
case "scanner-result": {
|
|
1615
|
+
const count = event.status === "ran" ? ` ${event.findings} findings` : "";
|
|
1616
|
+
const reason = event.reason ? ` — ${event.reason}` : "";
|
|
1617
|
+
this.write(`scanner ${event.tool}: ${event.status}${count}${reason}`);
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1203
1620
|
case "audit": {
|
|
1204
1621
|
const scope = event.scanned != null ? `${event.scanned} files eligible for fixes` : "whole repo";
|
|
1205
1622
|
const phase = event.loop === 1 ? "initial audit" : `re-audit after fix pass ${event.loop - 1}`;
|
|
@@ -1214,6 +1631,9 @@ var PlainReporter = class extends BaseReporter {
|
|
|
1214
1631
|
else if (event.outcome === "reverted") this.write(`${glyph.reverted} reverted ${event.file} — ${reasonLabel(event.reason)}`);
|
|
1215
1632
|
else this.write(`${glyph.left} not attempted ${event.file}`);
|
|
1216
1633
|
break;
|
|
1634
|
+
case "file-stage":
|
|
1635
|
+
this.write(`progress ${event.file}: ${fixStageLabel(event.stage)}${event.detail ? ` (${event.detail})` : ""}`);
|
|
1636
|
+
break;
|
|
1217
1637
|
case "snapshot":
|
|
1218
1638
|
case "detected":
|
|
1219
1639
|
case "file-start":
|
|
@@ -1264,8 +1684,8 @@ function loadReport() {
|
|
|
1264
1684
|
* executes in). Files are re-based onto `root` so `vitest related` / `jest
|
|
1265
1685
|
* --findRelatedTests` resolve them inside the owning package, not the repo root.
|
|
1266
1686
|
*/
|
|
1267
|
-
async function runTests(runner, files, root) {
|
|
1268
|
-
const targets = toOwnerRelative(files,
|
|
1687
|
+
async function runTests(runner, files, root, repoRoot = cwd) {
|
|
1688
|
+
const targets = toOwnerRelative(files, repoRoot, root);
|
|
1269
1689
|
const args = runner === "vitest" ? [
|
|
1270
1690
|
"vitest",
|
|
1271
1691
|
"related",
|
|
@@ -1295,79 +1715,185 @@ async function runTests(runner, files, root) {
|
|
|
1295
1715
|
return [];
|
|
1296
1716
|
}
|
|
1297
1717
|
}
|
|
1298
|
-
async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd) {
|
|
1299
|
-
const typescript = detectTypeScript(ownerRoot);
|
|
1300
|
-
const runner = detectTestRunner(ownerRoot) ?? null;
|
|
1718
|
+
async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, bus, detected, sandboxPool) {
|
|
1719
|
+
const typescript = detected?.typescript ?? detectTypeScript(ownerRoot);
|
|
1720
|
+
const runner = detected?.runner ?? detectTestRunner(ownerRoot) ?? null;
|
|
1301
1721
|
const buildArgs = detectBuildCommand(ownerRoot);
|
|
1302
1722
|
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,
|
|
1723
|
+
const baseline = new Set(runner && baselineTargets.length > 0 ? (await runTests(runner, baselineTargets, ownerRoot, cwd)).filter((t) => t.status === "pass").map((t) => t.name) : []);
|
|
1724
|
+
const makeGateDeps = (sandbox) => {
|
|
1725
|
+
const repoRoot = sandbox?.cwd ?? cwd;
|
|
1726
|
+
const gateOwnerRoot = sandbox ? mapOwnerRoot(cwd, ownerRoot, sandbox.cwd) : ownerRoot;
|
|
1727
|
+
const session = new ClaudeSession({ spawn: async (req) => {
|
|
1728
|
+
const r = await execa("claude", [
|
|
1729
|
+
"-p",
|
|
1730
|
+
req.prompt,
|
|
1731
|
+
"--model",
|
|
1732
|
+
config.model,
|
|
1733
|
+
...config.effort ? ["--effort", config.effort] : [],
|
|
1734
|
+
"--output-format",
|
|
1735
|
+
"stream-json",
|
|
1736
|
+
"--verbose",
|
|
1737
|
+
"--allowedTools",
|
|
1738
|
+
"Read,Write,Edit"
|
|
1739
|
+
], {
|
|
1740
|
+
cwd: repoRoot,
|
|
1333
1741
|
reject: false,
|
|
1334
|
-
timeout:
|
|
1742
|
+
timeout: CLAUDE_TIMEOUT_MS,
|
|
1743
|
+
env: {
|
|
1744
|
+
...process.env,
|
|
1745
|
+
...thinkingEnv(req.findings, config)
|
|
1746
|
+
}
|
|
1335
1747
|
});
|
|
1748
|
+
const exitCode = r.exitCode ?? (r.timedOut ? 143 : r.failed ? 1 : 0);
|
|
1336
1749
|
return {
|
|
1337
|
-
|
|
1338
|
-
|
|
1750
|
+
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
1751
|
+
exitCode
|
|
1339
1752
|
};
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1753
|
+
} });
|
|
1754
|
+
return {
|
|
1755
|
+
cwd: repoRoot,
|
|
1756
|
+
typescript,
|
|
1757
|
+
runTsc: async () => {
|
|
1758
|
+
const r = await execa("npx", ["tsc", "--noEmit"], {
|
|
1759
|
+
cwd: gateOwnerRoot,
|
|
1760
|
+
reject: false,
|
|
1761
|
+
timeout: TSC_TIMEOUT_MS
|
|
1762
|
+
});
|
|
1763
|
+
return {
|
|
1764
|
+
exitCode: r.exitCode ?? 1,
|
|
1765
|
+
output: `${r.stdout}\n${r.stderr}`
|
|
1766
|
+
};
|
|
1767
|
+
},
|
|
1768
|
+
runBuild: buildArgs ? async () => {
|
|
1769
|
+
const r = await execa(pm, buildArgs, {
|
|
1770
|
+
cwd: gateOwnerRoot,
|
|
1771
|
+
reject: false,
|
|
1772
|
+
timeout: BUILD_TIMEOUT_MS
|
|
1773
|
+
});
|
|
1774
|
+
return {
|
|
1775
|
+
exitCode: r.exitCode ?? 1,
|
|
1776
|
+
output: `${r.stdout}\n${r.stderr}`
|
|
1777
|
+
};
|
|
1778
|
+
} : void 0,
|
|
1779
|
+
hasTestRunner: Boolean(runner),
|
|
1780
|
+
runRelated: (files) => runner ? runTests(runner, files, gateOwnerRoot, repoRoot) : Promise.resolve([]),
|
|
1781
|
+
scanFindings: async (files, tools) => (await scanFiles({
|
|
1782
|
+
cwd: repoRoot,
|
|
1783
|
+
which: realWhich,
|
|
1784
|
+
spawn: realSpawn,
|
|
1785
|
+
timeoutMs: 12e4,
|
|
1786
|
+
tools
|
|
1787
|
+
}, files, 0)).findings,
|
|
1788
|
+
baseline,
|
|
1789
|
+
session
|
|
1790
|
+
};
|
|
1791
|
+
};
|
|
1792
|
+
const mainGateDeps = makeGateDeps();
|
|
1793
|
+
const acceptedFiles = new Set();
|
|
1794
|
+
const acceptedTools = new Set();
|
|
1795
|
+
const buildFixUnit = (sandbox) => makeFixUnit({
|
|
1796
|
+
...makeGateDeps(sandbox),
|
|
1797
|
+
maxRepairs: 3,
|
|
1798
|
+
onProgress: (event) => bus?.emit({
|
|
1799
|
+
type: "file-stage",
|
|
1800
|
+
...event
|
|
1801
|
+
})
|
|
1802
|
+
});
|
|
1803
|
+
const fixUnit = async (unit, loop) => {
|
|
1804
|
+
if (!sandboxPool) return buildFixUnit()(unit, loop);
|
|
1805
|
+
try {
|
|
1806
|
+
return await sandboxPool.withSandbox(async (sandbox) => {
|
|
1807
|
+
const outcome = await buildFixUnit(sandbox)(unit, loop);
|
|
1808
|
+
if (!outcome.kept) return outcome;
|
|
1809
|
+
bus?.emit({
|
|
1810
|
+
type: "file-stage",
|
|
1811
|
+
loop,
|
|
1812
|
+
file: unit.file,
|
|
1813
|
+
stage: "patch-apply"
|
|
1814
|
+
});
|
|
1815
|
+
const patch = await sandbox.collectPatch(unit);
|
|
1816
|
+
if (!patch.ok) return {
|
|
1817
|
+
kept: false,
|
|
1818
|
+
reason: "unowned-patch",
|
|
1819
|
+
detail: patch.detail,
|
|
1820
|
+
failureClass: "unowned-patch",
|
|
1821
|
+
usage: outcome.usage
|
|
1822
|
+
};
|
|
1823
|
+
const applied = await sandboxPool.applyPatchToMain(patch.patch);
|
|
1824
|
+
if (!applied.ok) {
|
|
1825
|
+
bus?.emit({
|
|
1826
|
+
type: "file-stage",
|
|
1827
|
+
loop,
|
|
1828
|
+
file: unit.file,
|
|
1829
|
+
stage: "patch-conflict"
|
|
1830
|
+
});
|
|
1831
|
+
return {
|
|
1832
|
+
kept: false,
|
|
1833
|
+
reason: "patch-conflict",
|
|
1834
|
+
detail: applied.detail,
|
|
1835
|
+
failureClass: "patch-conflict",
|
|
1836
|
+
usage: outcome.usage
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
for (const file of patch.changedFiles) acceptedFiles.add(file);
|
|
1840
|
+
for (const finding of unit.findings) acceptedTools.add(finding.tool);
|
|
1841
|
+
return outcome;
|
|
1346
1842
|
});
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1845
|
+
const setupFailed = error instanceof SandboxSetupError;
|
|
1347
1846
|
return {
|
|
1348
|
-
|
|
1349
|
-
|
|
1847
|
+
kept: false,
|
|
1848
|
+
reason: setupFailed ? "sandbox-setup-failed" : "session-error",
|
|
1849
|
+
detail,
|
|
1850
|
+
failureClass: setupFailed ? "sandbox-setup-failed" : "model-tool-failure",
|
|
1851
|
+
usage: zeroUsage()
|
|
1350
1852
|
};
|
|
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
|
|
1853
|
+
}
|
|
1361
1854
|
};
|
|
1362
1855
|
return {
|
|
1363
1856
|
typescript,
|
|
1364
1857
|
runner,
|
|
1365
|
-
fixUnit
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1858
|
+
fixUnit,
|
|
1859
|
+
deterministicFixUnit: makeDeterministicFixUnit(mainGateDeps),
|
|
1860
|
+
finalIntegration: async () => {
|
|
1861
|
+
const files = [...acceptedFiles].sort();
|
|
1862
|
+
if (files.length === 0) return {
|
|
1863
|
+
ok: true,
|
|
1864
|
+
files
|
|
1865
|
+
};
|
|
1866
|
+
if (typescript) {
|
|
1867
|
+
const tc = await mainGateDeps.runTsc();
|
|
1868
|
+
if (tc.exitCode !== 0) return {
|
|
1869
|
+
ok: false,
|
|
1870
|
+
files,
|
|
1871
|
+
detail: `final integration typecheck failed: ${tc.output}`
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
if (runner) {
|
|
1875
|
+
const tests = await mainGateDeps.runRelated(files);
|
|
1876
|
+
const failed = tests.filter((test) => test.status === "fail");
|
|
1877
|
+
if (failed.length > 0) return {
|
|
1878
|
+
ok: false,
|
|
1879
|
+
files,
|
|
1880
|
+
detail: `final integration related tests failed: ${failed.map((test) => test.name).join(", ")}`
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const tools = [...acceptedTools];
|
|
1884
|
+
if (tools.length > 0) {
|
|
1885
|
+
const findings = await mainGateDeps.scanFindings(files, tools);
|
|
1886
|
+
if (findings.length > 0) return {
|
|
1887
|
+
ok: false,
|
|
1888
|
+
files,
|
|
1889
|
+
detail: `final integration scanner rescan found ${findings.length} finding${findings.length === 1 ? "" : "s"}`
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
ok: true,
|
|
1894
|
+
files
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1371
1897
|
};
|
|
1372
1898
|
}
|
|
1373
1899
|
function describeScopeNote(all, paths, scope) {
|
|
@@ -1389,6 +1915,8 @@ async function runRun(opts) {
|
|
|
1389
1915
|
write: out
|
|
1390
1916
|
});
|
|
1391
1917
|
reporter.start();
|
|
1918
|
+
const bus = new EventBus();
|
|
1919
|
+
bus.on((e) => reporter.onEvent(e));
|
|
1392
1920
|
const git = createGit(cwd);
|
|
1393
1921
|
await assertGitRepo(git);
|
|
1394
1922
|
if (opts.effort && !EFFORT_LEVELS.includes(opts.effort)) {
|
|
@@ -1427,16 +1955,27 @@ async function runRun(opts) {
|
|
|
1427
1955
|
} else scope = await changedVsHead(git);
|
|
1428
1956
|
const baselineTargets = scope ?? ["."];
|
|
1429
1957
|
const ownerRoot = scope ? resolveOwnerRoot(cwd, scope) : cwd;
|
|
1430
|
-
const
|
|
1958
|
+
const typescript = detectTypeScript(ownerRoot);
|
|
1959
|
+
const runner = detectTestRunner(ownerRoot) ?? null;
|
|
1431
1960
|
const pm = detectPackageManager(cwd);
|
|
1432
1961
|
reporter.note(`${pm} · ${typescript ? "TypeScript" : "JavaScript"} · ${runner ?? "no test runner"} · ${modelLabel}`);
|
|
1433
1962
|
const scopeNote = describeScopeNote(opts.all, paths, scope);
|
|
1434
1963
|
reporter.note(`${scopeNote} · ${plural(available.length, "scanner")}`);
|
|
1435
|
-
|
|
1436
|
-
|
|
1964
|
+
if (runner && baselineTargets.length > 0) reporter.note(`baseline: ${runner} related ${describeScopeNote(opts.all, paths, scope)} (one-time)`);
|
|
1965
|
+
const sandboxPool = new WorkerSandboxPool({
|
|
1966
|
+
mainRoot: snapshot.repoRoot(),
|
|
1967
|
+
snapshotSha: snapshot.commitSha(),
|
|
1968
|
+
maxSandboxes: config.maxSessions,
|
|
1969
|
+
packageManager: pm
|
|
1970
|
+
});
|
|
1971
|
+
const { fixUnit, deterministicFixUnit, finalIntegration } = await makeProductionFixUnit(config, baselineTargets, ownerRoot, bus, {
|
|
1972
|
+
typescript,
|
|
1973
|
+
runner
|
|
1974
|
+
}, sandboxPool);
|
|
1437
1975
|
const start = Date.now();
|
|
1438
1976
|
const drawing = reporter.run();
|
|
1439
1977
|
let result;
|
|
1978
|
+
let finalIntegrationResult;
|
|
1440
1979
|
try {
|
|
1441
1980
|
result = await orchestrate({
|
|
1442
1981
|
cwd,
|
|
@@ -1445,7 +1984,8 @@ async function runRun(opts) {
|
|
|
1445
1984
|
which: realWhich,
|
|
1446
1985
|
spawn: realSpawn,
|
|
1447
1986
|
scope,
|
|
1448
|
-
timeoutMs: 12e4
|
|
1987
|
+
timeoutMs: 12e4,
|
|
1988
|
+
bus
|
|
1449
1989
|
}),
|
|
1450
1990
|
fixUnit,
|
|
1451
1991
|
deterministicFixUnit,
|
|
@@ -1453,7 +1993,10 @@ async function runRun(opts) {
|
|
|
1453
1993
|
inScope: scope ? (fs) => filterToChanged(fs, scope) : void 0,
|
|
1454
1994
|
bus
|
|
1455
1995
|
});
|
|
1996
|
+
finalIntegrationResult = await finalIntegration();
|
|
1997
|
+
if (!finalIntegrationResult.ok) result.exitStatus = 1;
|
|
1456
1998
|
} finally {
|
|
1999
|
+
await sandboxPool.dispose();
|
|
1457
2000
|
reporter.close();
|
|
1458
2001
|
}
|
|
1459
2002
|
await drawing;
|
|
@@ -1473,7 +2016,8 @@ async function runRun(opts) {
|
|
|
1473
2016
|
exclude: config.fix.exclude,
|
|
1474
2017
|
includeGenerated: config.fix.includeGenerated,
|
|
1475
2018
|
includeFixtures: config.fix.includeFixtures
|
|
1476
|
-
}
|
|
2019
|
+
},
|
|
2020
|
+
finalIntegration: finalIntegrationResult
|
|
1477
2021
|
});
|
|
1478
2022
|
persist(REPORT_PATH, report);
|
|
1479
2023
|
out("");
|
|
@@ -1496,6 +2040,7 @@ async function runRetry(id) {
|
|
|
1496
2040
|
}
|
|
1497
2041
|
const config = await loadConfig(cwd);
|
|
1498
2042
|
let snapshotSaved = false;
|
|
2043
|
+
let retrySandboxPool;
|
|
1499
2044
|
const result = await retryCommand(id, {
|
|
1500
2045
|
report,
|
|
1501
2046
|
baseBudget: config.perIssueBudget,
|
|
@@ -1517,12 +2062,20 @@ async function runRetry(id) {
|
|
|
1517
2062
|
if (!snapshotSaved) {
|
|
1518
2063
|
const snapshot = await Snapshot.capture(git, cwd);
|
|
1519
2064
|
persist(SNAPSHOT_PATH, snapshot.toJSON());
|
|
2065
|
+
retrySandboxPool = new WorkerSandboxPool({
|
|
2066
|
+
mainRoot: snapshot.repoRoot(),
|
|
2067
|
+
snapshotSha: snapshot.commitSha(),
|
|
2068
|
+
maxSandboxes: config.maxSessions,
|
|
2069
|
+
packageManager: detectPackageManager(cwd)
|
|
2070
|
+
});
|
|
1520
2071
|
snapshotSaved = true;
|
|
1521
2072
|
}
|
|
1522
2073
|
const ownerRoot = resolveOwnerRoot(cwd, unit.files);
|
|
1523
|
-
const { fixUnit } = await makeProductionFixUnit(config, unit.files, ownerRoot);
|
|
2074
|
+
const { fixUnit } = await makeProductionFixUnit(config, unit.files, ownerRoot, void 0, void 0, retrySandboxPool);
|
|
1524
2075
|
return fixUnit(unit, 1);
|
|
1525
2076
|
}
|
|
2077
|
+
}).finally(async () => {
|
|
2078
|
+
await retrySandboxPool?.dispose();
|
|
1526
2079
|
});
|
|
1527
2080
|
if ("error" in result) {
|
|
1528
2081
|
err(`✖ ${result.error}`);
|