tend-cli 0.7.0 → 0.9.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 +337 -38
- package/dist/{config-DPlVYfKX.js → config-BPLYzcro.js} +14 -4
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/prompts/single-file-ai-edit.md +8 -0
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-
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
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-BPLYzcro.js";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { execa } from "execa";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import PQueue from "p-queue";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { tmpdir } from "node:os";
|
|
@@ -753,18 +754,28 @@ function templateForStrategy(strategy) {
|
|
|
753
754
|
function renderFileList(files) {
|
|
754
755
|
return files.map((file) => `- ${file}`).join("\n");
|
|
755
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* Render the editable files' current source as labelled fenced blocks, so the model can
|
|
759
|
+
* edit without a preliminary Read (Fix 6). Templates that don't reference `{{fileContents}}`
|
|
760
|
+
* are unaffected; an empty map renders a neutral note so the placeholder is never left raw.
|
|
761
|
+
*/
|
|
762
|
+
function renderFileContents(contents) {
|
|
763
|
+
if (contents.size === 0) return "(file content not supplied — read the file before editing)";
|
|
764
|
+
return [...contents].map(([file, source]) => `### ${file}\n\n\`\`\`\n${source}\n\`\`\``).join("\n\n");
|
|
765
|
+
}
|
|
756
766
|
function renderCommonTemplate(input) {
|
|
757
|
-
return replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(input.template, "{{strategyName}}", input.strategyName), "{{findings}}", input.findings), "{{editableFiles}}", renderFileList(input.editableFiles)), "{{verificationTargets}}", renderFileList(input.verificationTargets)).trim();
|
|
767
|
+
return replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(input.template, "{{strategyName}}", input.strategyName), "{{findings}}", input.findings), "{{editableFiles}}", renderFileList(input.editableFiles)), "{{verificationTargets}}", renderFileList(input.verificationTargets)), "{{fileContents}}", renderFileContents(input.fileContents ?? new Map())).trim();
|
|
758
768
|
}
|
|
759
|
-
/** Render the fix prompt for a unit's findings. */
|
|
760
|
-
function renderPrompt(unit) {
|
|
769
|
+
/** Render the fix prompt for a unit's findings. `fileContents` supplies on-disk source. */
|
|
770
|
+
function renderPrompt(unit, fileContents) {
|
|
761
771
|
const strategyName = promptStrategyFor(unit);
|
|
762
772
|
return renderCommonTemplate({
|
|
763
773
|
template: templateForStrategy(strategyName),
|
|
764
774
|
strategyName,
|
|
765
775
|
findings: renderFindingsJson(unit.findings),
|
|
766
776
|
editableFiles: unit.files,
|
|
767
|
-
verificationTargets: unit.verificationTargets ?? unit.files
|
|
777
|
+
verificationTargets: unit.verificationTargets ?? unit.files,
|
|
778
|
+
fileContents
|
|
768
779
|
});
|
|
769
780
|
}
|
|
770
781
|
function firstRelevantLines(output, max = 20) {
|
|
@@ -800,8 +811,8 @@ function renderRegressionRepairPrompt(input) {
|
|
|
800
811
|
});
|
|
801
812
|
return replaceAllLiteral(replaceAllLiteral(replaceAllLiteral(prompt, "{{rejectedDiff}}", firstRelevantLines(input.rejectedDiff, 80)), "{{newFindings}}", renderFindingsJson(input.newFindings)), "{{gateDetails}}", [`Reason: ${input.gateReason}`, firstRelevantLines(input.gateOutput)].join("\n")).trim();
|
|
802
813
|
}
|
|
803
|
-
function renderNoEditRetryPrompt(unit) {
|
|
804
|
-
return `${renderPrompt(unit)}
|
|
814
|
+
function renderNoEditRetryPrompt(unit, fileContents) {
|
|
815
|
+
return `${renderPrompt(unit, fileContents)}
|
|
805
816
|
|
|
806
817
|
The previous session completed without changing any owned file. Retry once with a smaller, concrete edit:
|
|
807
818
|
- Make the minimal behavior-preserving code change that clears the finding.
|
|
@@ -841,12 +852,19 @@ function makeFixUnit(deps) {
|
|
|
841
852
|
const before = snapshotUnitFiles(deps.cwd, snapshotFiles);
|
|
842
853
|
const restore = () => restoreSnapshot(deps.cwd, before);
|
|
843
854
|
const changedOnDisk = () => unitChanged(deps.cwd, unit.files, before);
|
|
855
|
+
const fileContents = new Map();
|
|
856
|
+
for (const file of unit.files) {
|
|
857
|
+
const source = before.get(file);
|
|
858
|
+
if (typeof source === "string") fileContents.set(file, source);
|
|
859
|
+
}
|
|
844
860
|
let usage = zeroUsage();
|
|
861
|
+
const activity = (stage) => (detail) => progress(stage, detail);
|
|
845
862
|
progress("ai-edit");
|
|
846
863
|
const res = await deps.session.run({
|
|
847
864
|
file: unit.file,
|
|
848
865
|
findings: unit.findings,
|
|
849
|
-
prompt: renderPrompt(unit)
|
|
866
|
+
prompt: renderPrompt(unit, fileContents),
|
|
867
|
+
onActivity: activity("ai-edit")
|
|
850
868
|
});
|
|
851
869
|
if (res.usage) usage = addUsage(usage, res.usage);
|
|
852
870
|
if (!res.ok) {
|
|
@@ -864,7 +882,8 @@ function makeFixUnit(deps) {
|
|
|
864
882
|
const retry = await deps.session.run({
|
|
865
883
|
file: unit.file,
|
|
866
884
|
findings: unit.findings,
|
|
867
|
-
prompt: renderNoEditRetryPrompt(unit)
|
|
885
|
+
prompt: renderNoEditRetryPrompt(unit, fileContents),
|
|
886
|
+
onActivity: activity("ai-no-edit-retry")
|
|
868
887
|
});
|
|
869
888
|
if (retry.usage) usage = addUsage(usage, retry.usage);
|
|
870
889
|
if (!retry.ok) {
|
|
@@ -906,7 +925,8 @@ function makeFixUnit(deps) {
|
|
|
906
925
|
newFindings: outcome$1.reason === "regression" ? await scanNewFindings() : [],
|
|
907
926
|
gateReason: outcome$1.reason,
|
|
908
927
|
gateOutput: outcome$1.detail ?? ""
|
|
909
|
-
})
|
|
928
|
+
}),
|
|
929
|
+
onActivity: activity("regression-repair")
|
|
910
930
|
});
|
|
911
931
|
if (repair.usage) usage = addUsage(usage, repair.usage);
|
|
912
932
|
if (!repair.ok) {
|
|
@@ -931,7 +951,8 @@ function makeFixUnit(deps) {
|
|
|
931
951
|
newFindings: [],
|
|
932
952
|
gateReason: "broke-test",
|
|
933
953
|
gateOutput: `Fix left previously-green test(s) red:\n${regressed.map((test) => test.name).join("\n")}`
|
|
934
|
-
})
|
|
954
|
+
}),
|
|
955
|
+
onActivity: activity("test-repair")
|
|
935
956
|
});
|
|
936
957
|
if (repair.usage) usage = addUsage(usage, repair.usage);
|
|
937
958
|
if (!repair.ok) repairFailureDetail = `Repair session failed: ${repair.error}`;
|
|
@@ -957,14 +978,83 @@ function makeFixUnit(deps) {
|
|
|
957
978
|
};
|
|
958
979
|
}
|
|
959
980
|
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/fixing/thinking-budget.ts
|
|
983
|
+
/** Thinking disabled — mechanical fixes don't need reasoning tokens. */
|
|
984
|
+
const THINKING_OFF = 0;
|
|
985
|
+
/**
|
|
986
|
+
* Upper bound on extended-thinking tokens per fix session. Unbounded thinking
|
|
987
|
+
* measured ~7000 tokens per finding regardless of difficulty; capping it keeps
|
|
988
|
+
* reasoning fixes correct (gate stays green) at a fraction of the latency.
|
|
989
|
+
*/
|
|
990
|
+
const THINKING_BUDGET_CAP = 4096;
|
|
991
|
+
function isMechanical(finding) {
|
|
992
|
+
return finding.category === "dead-code" || finding.autofixable === true;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Decide the extended-thinking token budget for one finding's fix session.
|
|
996
|
+
* Mechanical findings (dead-code removal, autofixable rules) get thinking off;
|
|
997
|
+
* everything else — reasoning findings and any unrecognized category — gets the
|
|
998
|
+
* bounded cap (the safe default: never spend more than the cap, never starve a
|
|
999
|
+
* finding that might need reasoning). A configured `thinkingBudget` overrides
|
|
1000
|
+
* the policy outright, with 0 meaning thinking off.
|
|
1001
|
+
*/
|
|
1002
|
+
function thinkingBudgetFor(finding, config) {
|
|
1003
|
+
if (config?.thinkingBudget !== void 0) return config.thinkingBudget;
|
|
1004
|
+
return isMechanical(finding) ? THINKING_OFF : THINKING_BUDGET_CAP;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Budget for a whole work unit (one file's findings). Takes the most-conservative
|
|
1008
|
+
* (largest) per-finding budget so a reasoning finding is never starved of thinking
|
|
1009
|
+
* just because it shares the file with a mechanical one. An empty unit and any
|
|
1010
|
+
* unrecognized category fall back to the cap. A configured budget overrides all.
|
|
1011
|
+
*/
|
|
1012
|
+
function thinkingBudgetForUnit(findings, config) {
|
|
1013
|
+
if (config?.thinkingBudget !== void 0) return config.thinkingBudget;
|
|
1014
|
+
if (findings.length === 0) return THINKING_BUDGET_CAP;
|
|
1015
|
+
return Math.max(...findings.map((finding) => thinkingBudgetFor(finding)));
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Delivery to the `claude -p` session boundary: the env overlay that pins the
|
|
1019
|
+
* session's extended-thinking budget. Spread onto the child process env.
|
|
1020
|
+
*/
|
|
1021
|
+
function thinkingEnv(findings, config) {
|
|
1022
|
+
return { MAX_THINKING_TOKENS: String(thinkingBudgetForUnit(findings, config)) };
|
|
1023
|
+
}
|
|
1024
|
+
|
|
960
1025
|
//#endregion
|
|
961
1026
|
//#region src/fixing/worker-sandbox.ts
|
|
1027
|
+
/** Prefix every tend sandbox worktree directory carries, so we can recognize our own. */
|
|
1028
|
+
const WORKTREE_PREFIX = "tend-worker-";
|
|
962
1029
|
var SandboxSetupError = class extends Error {
|
|
963
1030
|
constructor(message) {
|
|
964
1031
|
super(message);
|
|
965
1032
|
this.name = "SandboxSetupError";
|
|
966
1033
|
}
|
|
967
1034
|
};
|
|
1035
|
+
/**
|
|
1036
|
+
* Files whose presence in a unit means the install graph may have changed, so a sandbox
|
|
1037
|
+
* must reinstall rather than reuse the main checkout's node_modules. Matched by basename,
|
|
1038
|
+
* so nested-package manifests (e.g. `packages/app/package.json`) count too.
|
|
1039
|
+
*/
|
|
1040
|
+
const DEPENDENCY_MANIFESTS = new Set([
|
|
1041
|
+
"package.json",
|
|
1042
|
+
"package-lock.json",
|
|
1043
|
+
"npm-shrinkwrap.json",
|
|
1044
|
+
"pnpm-lock.yaml",
|
|
1045
|
+
"pnpm-workspace.yaml",
|
|
1046
|
+
"yarn.lock",
|
|
1047
|
+
"bun.lockb",
|
|
1048
|
+
"bun.lock"
|
|
1049
|
+
]);
|
|
1050
|
+
/**
|
|
1051
|
+
* The reuse decision (Fix 4): a sandbox can reuse the main repo's installed deps unless
|
|
1052
|
+
* the unit edits a dependency manifest, in which case it must reinstall. Package-manager-
|
|
1053
|
+
* agnostic — driven entirely by which files the unit may change.
|
|
1054
|
+
*/
|
|
1055
|
+
function shouldReinstall(unit) {
|
|
1056
|
+
return allowedPatchFiles(unit).some((file) => DEPENDENCY_MANIFESTS.has(basename(normalizeRel(file))));
|
|
1057
|
+
}
|
|
968
1058
|
const cleanExcludes = [
|
|
969
1059
|
"node_modules",
|
|
970
1060
|
"node_modules/**",
|
|
@@ -1003,7 +1093,7 @@ function installArgs(pm) {
|
|
|
1003
1093
|
}
|
|
1004
1094
|
}
|
|
1005
1095
|
var GitWorkerSandbox = class {
|
|
1006
|
-
|
|
1096
|
+
preparedMode = "none";
|
|
1007
1097
|
constructor(cwd$1, deps) {
|
|
1008
1098
|
this.cwd = cwd$1;
|
|
1009
1099
|
this.deps = deps;
|
|
@@ -1021,8 +1111,37 @@ var GitWorkerSandbox = class {
|
|
|
1021
1111
|
...cleanExcludes.flatMap((pattern) => ["-e", pattern])
|
|
1022
1112
|
]);
|
|
1023
1113
|
}
|
|
1024
|
-
async prepare() {
|
|
1025
|
-
if (this.
|
|
1114
|
+
async prepare(unit) {
|
|
1115
|
+
if (this.deps.prepareDependencies === false) return;
|
|
1116
|
+
if (shouldReinstall(unit)) {
|
|
1117
|
+
if (this.preparedMode === "install") return;
|
|
1118
|
+
this.removeNodeModulesLink();
|
|
1119
|
+
await this.install();
|
|
1120
|
+
this.preparedMode = "install";
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
if (this.preparedMode !== "none") return;
|
|
1124
|
+
if (this.tryReuseMainDeps()) {
|
|
1125
|
+
this.preparedMode = "reuse";
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
await this.install();
|
|
1129
|
+
this.preparedMode = "install";
|
|
1130
|
+
}
|
|
1131
|
+
/** Symlink the main repo's node_modules into this worktree. Returns false if main has none. */
|
|
1132
|
+
tryReuseMainDeps() {
|
|
1133
|
+
const mainModules = join(this.deps.mainRoot, "node_modules");
|
|
1134
|
+
if (!existsSync(mainModules)) return false;
|
|
1135
|
+
const link = join(this.cwd, "node_modules");
|
|
1136
|
+
if (existsSync(link)) return true;
|
|
1137
|
+
symlinkSync(mainModules, link, "junction");
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
removeNodeModulesLink() {
|
|
1141
|
+
const link = join(this.cwd, "node_modules");
|
|
1142
|
+
rmSync(link, { force: true });
|
|
1143
|
+
}
|
|
1144
|
+
async install() {
|
|
1026
1145
|
const args = installArgs(this.deps.packageManager);
|
|
1027
1146
|
const result = await this.deps.exec(this.deps.packageManager, args, {
|
|
1028
1147
|
cwd: this.cwd,
|
|
@@ -1030,7 +1149,6 @@ var GitWorkerSandbox = class {
|
|
|
1030
1149
|
timeout: 10 * 6e4
|
|
1031
1150
|
});
|
|
1032
1151
|
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
1152
|
}
|
|
1035
1153
|
async collectPatch(unit) {
|
|
1036
1154
|
const git = createGit(this.cwd);
|
|
@@ -1101,14 +1219,14 @@ var WorkerSandboxPool = class {
|
|
|
1101
1219
|
idle = [];
|
|
1102
1220
|
sandboxes = new Set();
|
|
1103
1221
|
counter = 0;
|
|
1104
|
-
|
|
1222
|
+
disposing;
|
|
1105
1223
|
exec;
|
|
1106
1224
|
constructor(deps) {
|
|
1107
1225
|
this.deps = deps;
|
|
1108
1226
|
this.queue = new PQueue({ concurrency: deps.maxSandboxes });
|
|
1109
1227
|
this.exec = deps.exec ?? execa;
|
|
1110
1228
|
}
|
|
1111
|
-
async withSandbox(run) {
|
|
1229
|
+
async withSandbox(unit, run) {
|
|
1112
1230
|
return this.queue.add(async () => {
|
|
1113
1231
|
let sandbox;
|
|
1114
1232
|
try {
|
|
@@ -1118,7 +1236,7 @@ var WorkerSandboxPool = class {
|
|
|
1118
1236
|
}
|
|
1119
1237
|
try {
|
|
1120
1238
|
await sandbox.reset();
|
|
1121
|
-
await sandbox.prepare();
|
|
1239
|
+
await sandbox.prepare(unit);
|
|
1122
1240
|
} catch (error) {
|
|
1123
1241
|
this.idle.push(sandbox);
|
|
1124
1242
|
throw error instanceof SandboxSetupError ? error : new SandboxSetupError(error instanceof Error ? error.message : String(error));
|
|
@@ -1160,9 +1278,16 @@ var WorkerSandboxPool = class {
|
|
|
1160
1278
|
return { ok: true };
|
|
1161
1279
|
});
|
|
1162
1280
|
}
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1281
|
+
/**
|
|
1282
|
+
* Tear down every sandbox (remove worktrees). Idempotent and concurrency-safe: the SIGINT
|
|
1283
|
+
* handler and the run's finally path may both call this at once, so all callers share — and
|
|
1284
|
+
* await — the same in-flight teardown. Without this, a second caller returning early would let
|
|
1285
|
+
* the signal handler's process.exit() race ahead and leak a worktree.
|
|
1286
|
+
*/
|
|
1287
|
+
dispose() {
|
|
1288
|
+
return this.disposing ??= this.runDispose();
|
|
1289
|
+
}
|
|
1290
|
+
async runDispose() {
|
|
1166
1291
|
await this.queue.onIdle();
|
|
1167
1292
|
await Promise.all([...this.sandboxes].map((sandbox) => sandbox.dispose()));
|
|
1168
1293
|
this.idle.length = 0;
|
|
@@ -1173,7 +1298,7 @@ var WorkerSandboxPool = class {
|
|
|
1173
1298
|
if (existing) return existing;
|
|
1174
1299
|
const parent = this.deps.tempRoot ?? tmpdir();
|
|
1175
1300
|
mkdirSync(parent, { recursive: true });
|
|
1176
|
-
const path = `${parent}
|
|
1301
|
+
const path = `${parent}/${WORKTREE_PREFIX}${process.pid}-${this.counter++}`;
|
|
1177
1302
|
const mainGit = createGit(this.deps.mainRoot);
|
|
1178
1303
|
await mainGit.raw([
|
|
1179
1304
|
"worktree",
|
|
@@ -1190,11 +1315,168 @@ var WorkerSandboxPool = class {
|
|
|
1190
1315
|
return sandbox;
|
|
1191
1316
|
}
|
|
1192
1317
|
};
|
|
1318
|
+
/** Paths of every worktree currently registered for the repo at `mainRoot`. */
|
|
1319
|
+
async function registeredWorktrees(mainRoot) {
|
|
1320
|
+
const raw = await createGit(mainRoot).raw([
|
|
1321
|
+
"worktree",
|
|
1322
|
+
"list",
|
|
1323
|
+
"--porcelain"
|
|
1324
|
+
]);
|
|
1325
|
+
return raw.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("worktree ")).map((line) => line.slice(9).trim());
|
|
1326
|
+
}
|
|
1327
|
+
/**
|
|
1328
|
+
* Self-heal after a crashed or cancelled prior run: force-remove any leftover `tend-worker-*`
|
|
1329
|
+
* worktrees still registered for `mainRoot`, then `git worktree prune` to clear stale admin
|
|
1330
|
+
* records. Best-effort and idempotent — never throws (a run must start even if cleanup hiccups).
|
|
1331
|
+
*/
|
|
1332
|
+
async function pruneStaleWorktrees(mainRoot) {
|
|
1333
|
+
const git = createGit(mainRoot);
|
|
1334
|
+
try {
|
|
1335
|
+
for (const path of await registeredWorktrees(mainRoot)) {
|
|
1336
|
+
if (!basename(path).startsWith(WORKTREE_PREFIX)) continue;
|
|
1337
|
+
try {
|
|
1338
|
+
await git.raw([
|
|
1339
|
+
"worktree",
|
|
1340
|
+
"remove",
|
|
1341
|
+
"--force",
|
|
1342
|
+
path
|
|
1343
|
+
]);
|
|
1344
|
+
} catch {}
|
|
1345
|
+
rmSync(path, {
|
|
1346
|
+
recursive: true,
|
|
1347
|
+
force: true
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
await git.raw(["worktree", "prune"]);
|
|
1351
|
+
} catch {}
|
|
1352
|
+
}
|
|
1193
1353
|
function mapOwnerRoot(mainRoot, mainOwnerRoot, sandboxRoot) {
|
|
1194
1354
|
const rel = normalizeRel(relative(mainRoot, mainOwnerRoot));
|
|
1195
1355
|
return rel === "" ? sandboxRoot : `${sandboxRoot}/${rel}`;
|
|
1196
1356
|
}
|
|
1197
1357
|
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region src/process/signals.ts
|
|
1360
|
+
const SIGNALS = ["SIGINT", "SIGTERM"];
|
|
1361
|
+
/**
|
|
1362
|
+
* Run `handler` once when the process is asked to terminate (Ctrl-C / SIGTERM), so a run can
|
|
1363
|
+
* tear down its sandboxes before exiting instead of leaking worktrees. Each signal fires the
|
|
1364
|
+
* handler at most once (the run then exits). Returns an unregister function for the normal
|
|
1365
|
+
* completion path. The emitter is injectable so tests don't have to raise real signals.
|
|
1366
|
+
*/
|
|
1367
|
+
function onTerminationSignals(handler, emitter = process) {
|
|
1368
|
+
const wrappers = new Map();
|
|
1369
|
+
for (const signal of SIGNALS) {
|
|
1370
|
+
const wrapper = () => handler(signal);
|
|
1371
|
+
emitter.once(signal, wrapper);
|
|
1372
|
+
wrappers.set(signal, wrapper);
|
|
1373
|
+
}
|
|
1374
|
+
return () => {
|
|
1375
|
+
for (const [signal, wrapper] of wrappers) emitter.removeListener(signal, wrapper);
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
//#endregion
|
|
1380
|
+
//#region src/session/stream-activity.ts
|
|
1381
|
+
/** A short human label for one stream event's activity, or null for non-activity events. */
|
|
1382
|
+
function activityLabels(event) {
|
|
1383
|
+
if (event.type !== "assistant") return [];
|
|
1384
|
+
const labels = [];
|
|
1385
|
+
for (const block of event.message?.content ?? []) if (block.type === "tool_use" && typeof block.name === "string") {
|
|
1386
|
+
const path = block.input?.["file_path"];
|
|
1387
|
+
labels.push(typeof path === "string" ? `${block.name} ${path}` : block.name);
|
|
1388
|
+
} else if (block.type === "text") labels.push("thinking");
|
|
1389
|
+
return labels;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Incremental consumer for Claude Code `--output-format stream-json` stdout. Buffers
|
|
1393
|
+
* partial lines across chunks and reports a short activity label per assistant event
|
|
1394
|
+
* (tool uses with their target file, text turns as "thinking"). Malformed or truncated
|
|
1395
|
+
* lines are skipped — this is progress decoration only and must never throw; the
|
|
1396
|
+
* authoritative outcome is still judged from the disk after the session ends.
|
|
1397
|
+
*/
|
|
1398
|
+
function createStreamActivityScanner(onActivity) {
|
|
1399
|
+
let buffer = "";
|
|
1400
|
+
let last;
|
|
1401
|
+
const consume = (line) => {
|
|
1402
|
+
const trimmed = line.trim();
|
|
1403
|
+
if (!trimmed) return;
|
|
1404
|
+
let event;
|
|
1405
|
+
try {
|
|
1406
|
+
event = JSON.parse(trimmed);
|
|
1407
|
+
} catch {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
for (const label of activityLabels(event)) {
|
|
1411
|
+
if (label === last) continue;
|
|
1412
|
+
last = label;
|
|
1413
|
+
onActivity(label);
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
return {
|
|
1417
|
+
push(chunk) {
|
|
1418
|
+
buffer += chunk;
|
|
1419
|
+
const lines$1 = buffer.split("\n");
|
|
1420
|
+
buffer = lines$1.pop() ?? "";
|
|
1421
|
+
for (const line of lines$1) consume(line);
|
|
1422
|
+
},
|
|
1423
|
+
end() {
|
|
1424
|
+
consume(buffer);
|
|
1425
|
+
buffer = "";
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
//#endregion
|
|
1431
|
+
//#region src/fixing/typecheck-cache.ts
|
|
1432
|
+
const TSC_TIMEOUT_MS$1 = 5 * 6e4;
|
|
1433
|
+
/**
|
|
1434
|
+
* tsc arguments for an incremental, cached `--noEmit` typecheck. **Only** caching/speed
|
|
1435
|
+
* flags are added (`--incremental`, `--tsBuildInfoFile`) — never a correctness or
|
|
1436
|
+
* tsconfig-semantic flag (`--skipLibCheck`, `--strict`, `--target`, …). The owning
|
|
1437
|
+
* package's tsconfig is still resolved from the cwd, so its semantics are unchanged.
|
|
1438
|
+
*/
|
|
1439
|
+
function incrementalTscArgs(cacheFile) {
|
|
1440
|
+
return [
|
|
1441
|
+
"tsc",
|
|
1442
|
+
"--noEmit",
|
|
1443
|
+
"--incremental",
|
|
1444
|
+
"--tsBuildInfoFile",
|
|
1445
|
+
cacheFile
|
|
1446
|
+
];
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* A stable, tend-owned build-info cache path for the package rooted at `ownerRoot`.
|
|
1450
|
+
*
|
|
1451
|
+
* The caller roots `cacheDir` at the **main** repo's `.tend/cache` — outside any sandbox
|
|
1452
|
+
* worktree — so the file survives `reset()`/`git clean` between iterations and is reused.
|
|
1453
|
+
* The filename encodes the owner's repo-relative path (slug + hash) so monorepo packages
|
|
1454
|
+
* get distinct caches that don't collide.
|
|
1455
|
+
*/
|
|
1456
|
+
function tscCacheFile(cacheDir, mainRoot, ownerRoot) {
|
|
1457
|
+
const rel = relative(mainRoot, ownerRoot).replaceAll("\\", "/") || ".";
|
|
1458
|
+
const slug = rel === "." ? "root" : rel.replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "pkg";
|
|
1459
|
+
const hash = createHash("sha1").update(rel).digest("hex").slice(0, 8);
|
|
1460
|
+
return join(cacheDir, `${slug}-${hash}.tsbuildinfo`);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Run `tsc --noEmit` with an incremental cache. Ensures the cache directory exists, then
|
|
1464
|
+
* runs tsc. tsc itself tolerates a missing or corrupt build-info file (it rebuilds from
|
|
1465
|
+
* scratch and overwrites), so correctness is unaffected — only cold runs are slower.
|
|
1466
|
+
*/
|
|
1467
|
+
async function runIncrementalTsc(deps) {
|
|
1468
|
+
mkdirSync(dirname(deps.cacheFile), { recursive: true });
|
|
1469
|
+
const r = await deps.exec("npx", incrementalTscArgs(deps.cacheFile), {
|
|
1470
|
+
cwd: deps.cwd,
|
|
1471
|
+
reject: false,
|
|
1472
|
+
timeout: deps.timeoutMs ?? TSC_TIMEOUT_MS$1
|
|
1473
|
+
});
|
|
1474
|
+
return {
|
|
1475
|
+
exitCode: r.exitCode ?? 1,
|
|
1476
|
+
output: `${r.stdout ?? ""}\n${r.stderr ?? ""}`
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1198
1480
|
//#endregion
|
|
1199
1481
|
//#region src/output/env.ts
|
|
1200
1482
|
/** Truthy in the env-var sense: present and not an explicit off value. */
|
|
@@ -1317,6 +1599,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1317
1599
|
currentConcurrency;
|
|
1318
1600
|
rules = new Map();
|
|
1319
1601
|
stages = new Map();
|
|
1602
|
+
stageDetails = new Map();
|
|
1320
1603
|
scannerStates = new Map();
|
|
1321
1604
|
currentScanLoop;
|
|
1322
1605
|
header;
|
|
@@ -1368,6 +1651,7 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1368
1651
|
this.currentConcurrency = event.concurrency;
|
|
1369
1652
|
this.rules.clear();
|
|
1370
1653
|
this.stages.clear();
|
|
1654
|
+
this.stageDetails.clear();
|
|
1371
1655
|
this.labelWidth = Math.max(0, ...event.files.map((f) => basename(f).length));
|
|
1372
1656
|
this.phases.push({
|
|
1373
1657
|
kind: "fix",
|
|
@@ -1388,6 +1672,8 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1388
1672
|
case "file-stage":
|
|
1389
1673
|
this.currentFile = event.file;
|
|
1390
1674
|
this.stages.set(event.file, event.stage);
|
|
1675
|
+
if (event.detail) this.stageDetails.set(event.file, event.detail);
|
|
1676
|
+
else this.stageDetails.delete(event.file);
|
|
1391
1677
|
this.refreshHeader();
|
|
1392
1678
|
break;
|
|
1393
1679
|
case "file-result":
|
|
@@ -1541,7 +1827,11 @@ var LiveReporter = class extends BaseReporter {
|
|
|
1541
1827
|
fileTitle(file) {
|
|
1542
1828
|
const rule = this.rules.get(file);
|
|
1543
1829
|
const stage = this.stages.get(file);
|
|
1544
|
-
const detail = [
|
|
1830
|
+
const detail = [
|
|
1831
|
+
rule,
|
|
1832
|
+
stage ? fixStageLabel(stage) : void 0,
|
|
1833
|
+
this.stageDetails.get(file)
|
|
1834
|
+
].filter(Boolean).join(" · ");
|
|
1545
1835
|
const suffix = detail ? ` ${this.theme.dim(detail)}` : "";
|
|
1546
1836
|
return `${this.fileLabel(file)}${suffix}`;
|
|
1547
1837
|
}
|
|
@@ -1616,6 +1906,7 @@ const cwd = process.cwd();
|
|
|
1616
1906
|
const TEND_DIR = join(cwd, ".tend");
|
|
1617
1907
|
const SNAPSHOT_PATH = join(TEND_DIR, "snapshot.json");
|
|
1618
1908
|
const REPORT_PATH = join(TEND_DIR, "report.json");
|
|
1909
|
+
const TEND_CACHE_DIR = join(TEND_DIR, "cache");
|
|
1619
1910
|
const CLAUDE_TIMEOUT_MS = 10 * 6e4;
|
|
1620
1911
|
const BUILD_TIMEOUT_MS = 5 * 6e4;
|
|
1621
1912
|
const TSC_TIMEOUT_MS = 5 * 6e4;
|
|
@@ -1681,7 +1972,7 @@ async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, b
|
|
|
1681
1972
|
const repoRoot = sandbox?.cwd ?? cwd;
|
|
1682
1973
|
const gateOwnerRoot = sandbox ? mapOwnerRoot(cwd, ownerRoot, sandbox.cwd) : ownerRoot;
|
|
1683
1974
|
const session = new ClaudeSession({ spawn: async (req) => {
|
|
1684
|
-
const
|
|
1975
|
+
const child = execa("claude", [
|
|
1685
1976
|
"-p",
|
|
1686
1977
|
req.prompt,
|
|
1687
1978
|
"--model",
|
|
@@ -1695,8 +1986,16 @@ async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, b
|
|
|
1695
1986
|
], {
|
|
1696
1987
|
cwd: repoRoot,
|
|
1697
1988
|
reject: false,
|
|
1698
|
-
timeout: CLAUDE_TIMEOUT_MS
|
|
1989
|
+
timeout: CLAUDE_TIMEOUT_MS,
|
|
1990
|
+
env: {
|
|
1991
|
+
...process.env,
|
|
1992
|
+
...thinkingEnv(req.findings, config)
|
|
1993
|
+
}
|
|
1699
1994
|
});
|
|
1995
|
+
const scanner = createStreamActivityScanner((activity) => req.onActivity?.(activity));
|
|
1996
|
+
child.stdout?.on("data", (chunk) => scanner.push(chunk.toString()));
|
|
1997
|
+
child.stdout?.on("end", () => scanner.end());
|
|
1998
|
+
const r = await child;
|
|
1700
1999
|
const exitCode = r.exitCode ?? (r.timedOut ? 143 : r.failed ? 1 : 0);
|
|
1701
2000
|
return {
|
|
1702
2001
|
stdout: typeof r.stdout === "string" ? r.stdout : "",
|
|
@@ -1706,17 +2005,12 @@ async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, b
|
|
|
1706
2005
|
return {
|
|
1707
2006
|
cwd: repoRoot,
|
|
1708
2007
|
typescript,
|
|
1709
|
-
runTsc: async () => {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
return {
|
|
1716
|
-
exitCode: r.exitCode ?? 1,
|
|
1717
|
-
output: `${r.stdout}\n${r.stderr}`
|
|
1718
|
-
};
|
|
1719
|
-
},
|
|
2008
|
+
runTsc: async () => runIncrementalTsc({
|
|
2009
|
+
exec: execa,
|
|
2010
|
+
cwd: gateOwnerRoot,
|
|
2011
|
+
cacheFile: tscCacheFile(TEND_CACHE_DIR, cwd, ownerRoot),
|
|
2012
|
+
timeoutMs: TSC_TIMEOUT_MS
|
|
2013
|
+
}),
|
|
1720
2014
|
runBuild: buildArgs ? async () => {
|
|
1721
2015
|
const r = await execa(pm, buildArgs, {
|
|
1722
2016
|
cwd: gateOwnerRoot,
|
|
@@ -1755,7 +2049,7 @@ async function makeProductionFixUnit(config, baselineTargets, ownerRoot = cwd, b
|
|
|
1755
2049
|
const fixUnit = async (unit, loop) => {
|
|
1756
2050
|
if (!sandboxPool) return buildFixUnit()(unit, loop);
|
|
1757
2051
|
try {
|
|
1758
|
-
return await sandboxPool.withSandbox(async (sandbox) => {
|
|
2052
|
+
return await sandboxPool.withSandbox(unit, async (sandbox) => {
|
|
1759
2053
|
const outcome = await buildFixUnit(sandbox)(unit, loop);
|
|
1760
2054
|
if (!outcome.kept) return outcome;
|
|
1761
2055
|
bus?.emit({
|
|
@@ -1914,6 +2208,7 @@ async function runRun(opts) {
|
|
|
1914
2208
|
const scopeNote = describeScopeNote(opts.all, paths, scope);
|
|
1915
2209
|
reporter.note(`${scopeNote} · ${plural(available.length, "scanner")}`);
|
|
1916
2210
|
if (runner && baselineTargets.length > 0) reporter.note(`baseline: ${runner} related ${describeScopeNote(opts.all, paths, scope)} (one-time)`);
|
|
2211
|
+
await pruneStaleWorktrees(snapshot.repoRoot());
|
|
1917
2212
|
const sandboxPool = new WorkerSandboxPool({
|
|
1918
2213
|
mainRoot: snapshot.repoRoot(),
|
|
1919
2214
|
snapshotSha: snapshot.commitSha(),
|
|
@@ -1924,6 +2219,9 @@ async function runRun(opts) {
|
|
|
1924
2219
|
typescript,
|
|
1925
2220
|
runner
|
|
1926
2221
|
}, sandboxPool);
|
|
2222
|
+
const stopSignals = onTerminationSignals((signal) => {
|
|
2223
|
+
sandboxPool.dispose().finally(() => process.exit(signal === "SIGINT" ? 130 : 143));
|
|
2224
|
+
});
|
|
1927
2225
|
const start = Date.now();
|
|
1928
2226
|
const drawing = reporter.run();
|
|
1929
2227
|
let result;
|
|
@@ -1948,6 +2246,7 @@ async function runRun(opts) {
|
|
|
1948
2246
|
finalIntegrationResult = await finalIntegration();
|
|
1949
2247
|
if (!finalIntegrationResult.ok) result.exitStatus = 1;
|
|
1950
2248
|
} finally {
|
|
2249
|
+
stopSignals();
|
|
1951
2250
|
await sandboxPool.dispose();
|
|
1952
2251
|
reporter.close();
|
|
1953
2252
|
}
|
|
@@ -1350,10 +1350,11 @@ async function currentFiles(root) {
|
|
|
1350
1350
|
* so the on-disk record is a 40-char id rather than a copy of every file.
|
|
1351
1351
|
*/
|
|
1352
1352
|
var Snapshot = class Snapshot {
|
|
1353
|
-
constructor(cwd, root, sha) {
|
|
1353
|
+
constructor(cwd, root, sha, indexTree = null) {
|
|
1354
1354
|
this.cwd = cwd;
|
|
1355
1355
|
this.root = root;
|
|
1356
1356
|
this.sha = sha;
|
|
1357
|
+
this.indexTree = indexTree;
|
|
1357
1358
|
}
|
|
1358
1359
|
static async capture(_git, cwd) {
|
|
1359
1360
|
const git = createGit(cwd);
|
|
@@ -1362,6 +1363,12 @@ var Snapshot = class Snapshot {
|
|
|
1362
1363
|
ensureTendIgnored(gitDir);
|
|
1363
1364
|
const rg = createGit(root);
|
|
1364
1365
|
const tree = await writeWorkingTree(root);
|
|
1366
|
+
let indexTree = null;
|
|
1367
|
+
try {
|
|
1368
|
+
indexTree = (await rg.raw(["write-tree"])).trim();
|
|
1369
|
+
} catch {
|
|
1370
|
+
indexTree = null;
|
|
1371
|
+
}
|
|
1365
1372
|
let parent = null;
|
|
1366
1373
|
try {
|
|
1367
1374
|
parent = (await rg.revparse(["HEAD"])).trim();
|
|
@@ -1387,14 +1394,15 @@ var Snapshot = class Snapshot {
|
|
|
1387
1394
|
SNAP_REF,
|
|
1388
1395
|
sha
|
|
1389
1396
|
]);
|
|
1390
|
-
return new Snapshot(cwd, root, sha);
|
|
1397
|
+
return new Snapshot(cwd, root, sha, indexTree);
|
|
1391
1398
|
}
|
|
1392
1399
|
/** Serialize to a tiny object for `.tend/snapshot.json` (powers `undo` across invocations). */
|
|
1393
1400
|
toJSON() {
|
|
1394
1401
|
return {
|
|
1395
1402
|
cwd: this.cwd,
|
|
1396
1403
|
root: this.root,
|
|
1397
|
-
sha: this.sha
|
|
1404
|
+
sha: this.sha,
|
|
1405
|
+
indexTree: this.indexTree
|
|
1398
1406
|
};
|
|
1399
1407
|
}
|
|
1400
1408
|
commitSha() {
|
|
@@ -1404,7 +1412,7 @@ var Snapshot = class Snapshot {
|
|
|
1404
1412
|
return this.root;
|
|
1405
1413
|
}
|
|
1406
1414
|
static fromJSON(data) {
|
|
1407
|
-
return new Snapshot(data.cwd, data.root, data.sha);
|
|
1415
|
+
return new Snapshot(data.cwd, data.root, data.sha, data.indexTree ?? null);
|
|
1408
1416
|
}
|
|
1409
1417
|
/** Files whose contents differ from the snapshot, or that are new/deleted since it (sorted). */
|
|
1410
1418
|
async changedSince(_git) {
|
|
@@ -1446,6 +1454,7 @@ var Snapshot = class Snapshot {
|
|
|
1446
1454
|
this.sha
|
|
1447
1455
|
])));
|
|
1448
1456
|
for (const rel of await currentFiles(this.root)) if (!inSnapshot.has(rel)) rmSync(join(this.root, rel), { force: true });
|
|
1457
|
+
if (this.indexTree) await rg.raw(["read-tree", this.indexTree]);
|
|
1449
1458
|
}
|
|
1450
1459
|
};
|
|
1451
1460
|
|
|
@@ -3636,6 +3645,7 @@ const ConfigSchema = z.object({
|
|
|
3636
3645
|
includeTests: z.boolean().default(false),
|
|
3637
3646
|
model: z.string().default("sonnet"),
|
|
3638
3647
|
effort: z.enum(EFFORT_LEVELS).optional(),
|
|
3648
|
+
thinkingBudget: z.number().int().nonnegative().optional(),
|
|
3639
3649
|
fix: FixScopeConfigSchema,
|
|
3640
3650
|
tools: z.record(z.string(), ToolConfigSchema).default({})
|
|
3641
3651
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -236,6 +236,12 @@ type SessionRequest = {
|
|
|
236
236
|
findings: Finding[];
|
|
237
237
|
/** The fully-rendered prompt for the AI. */
|
|
238
238
|
prompt: string;
|
|
239
|
+
/**
|
|
240
|
+
* Live progress hook: called with a short label (e.g. "Edit src/a.ts") as activity
|
|
241
|
+
* streams from the running session. Decoration only — outcomes are still judged
|
|
242
|
+
* from the disk after the session ends.
|
|
243
|
+
*/
|
|
244
|
+
onActivity?: (activity: string) => void;
|
|
239
245
|
};
|
|
240
246
|
type FailureClass = "tool-timeout" | "rate-limit" | "model-tool-failure" | "sandbox-setup-failed" | "patch-conflict" | "unowned-patch" | "final-integration-failed" | "no-edit" | "no-op" | "regression" | "typecheck" | "broke-test" | "suppression" | "needs-lockfile-update";
|
|
241
247
|
/**
|
|
@@ -1800,6 +1806,7 @@ declare class Snapshot {
|
|
|
1800
1806
|
private readonly cwd;
|
|
1801
1807
|
private readonly root;
|
|
1802
1808
|
private readonly sha;
|
|
1809
|
+
private readonly indexTree;
|
|
1803
1810
|
private constructor();
|
|
1804
1811
|
static capture(_git: SimpleGit, cwd: string): Promise<Snapshot>;
|
|
1805
1812
|
/** Serialize to a tiny object for `.tend/snapshot.json` (powers `undo` across invocations). */
|
|
@@ -1807,6 +1814,7 @@ declare class Snapshot {
|
|
|
1807
1814
|
cwd: string;
|
|
1808
1815
|
root: string;
|
|
1809
1816
|
sha: string;
|
|
1817
|
+
indexTree: string | null;
|
|
1810
1818
|
};
|
|
1811
1819
|
commitSha(): string;
|
|
1812
1820
|
repoRoot(): string;
|
|
@@ -1814,6 +1822,7 @@ declare class Snapshot {
|
|
|
1814
1822
|
cwd: string;
|
|
1815
1823
|
root: string;
|
|
1816
1824
|
sha: string;
|
|
1825
|
+
indexTree?: string | null;
|
|
1817
1826
|
}): Snapshot;
|
|
1818
1827
|
/** Files whose contents differ from the snapshot, or that are new/deleted since it (sorted). */
|
|
1819
1828
|
changedSince(_git: SimpleGit): Promise<string[]>;
|
|
@@ -1863,6 +1872,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1863
1872
|
model: z.ZodDefault<z.ZodString>;
|
|
1864
1873
|
/** Reasoning effort for fixes; unset → claude's own default. */
|
|
1865
1874
|
effort: z.ZodOptional<z.ZodEnum<["low", "medium", "high", "xhigh", "max"]>>;
|
|
1875
|
+
/** Extended-thinking token budget per fix session; unset → per-finding policy decides. */
|
|
1876
|
+
thinkingBudget: z.ZodOptional<z.ZodNumber>;
|
|
1866
1877
|
/** Report/fix scope policy. Reports stay broad; fixes default away from generated/tooling paths. */
|
|
1867
1878
|
fix: z.ZodDefault<z.ZodObject<{
|
|
1868
1879
|
include: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -1909,6 +1920,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1909
1920
|
}>;
|
|
1910
1921
|
test?: string | undefined;
|
|
1911
1922
|
effort?: "low" | "medium" | "high" | "xhigh" | "max" | undefined;
|
|
1923
|
+
thinkingBudget?: number | undefined;
|
|
1912
1924
|
}, {
|
|
1913
1925
|
maxSessions?: number | undefined;
|
|
1914
1926
|
maxLoops?: number | undefined;
|
|
@@ -1918,6 +1930,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
1918
1930
|
includeTests?: boolean | undefined;
|
|
1919
1931
|
model?: string | undefined;
|
|
1920
1932
|
effort?: "low" | "medium" | "high" | "xhigh" | "max" | undefined;
|
|
1933
|
+
thinkingBudget?: number | undefined;
|
|
1921
1934
|
fix?: {
|
|
1922
1935
|
include?: string[] | undefined;
|
|
1923
1936
|
exclude?: string[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClaudeSession, ConfigSchema, EventBus, FindingSchema, FindingStore, REPAIR_STRATEGIES, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, applyRepairPlanToFinding, assertGitRepo, buildProgram, changedFiles, changedVsHead, detectPackageManager, dispatch, filterToChanged, fingerprint, groupRemaining, isAiDispatchStrategy, isAvailable, loadConfig, makeDeterministicFixUnit, makeDeterministicFixer, normalize, orchestrate, planRepair, planWork, planWorkFromRepairs, renderSummary, retryCommand, revertFile, route, runScanner, scopeFindings, showCommand, trackForTool, zeroUsage } from "./config-
|
|
1
|
+
import { ClaudeSession, ConfigSchema, EventBus, FindingSchema, FindingStore, REPAIR_STRATEGIES, ReportBuilder, ReportSchema, Snapshot, addUsage, applyCliOverrides, applyRepairPlanToFinding, assertGitRepo, buildProgram, changedFiles, changedVsHead, detectPackageManager, dispatch, filterToChanged, fingerprint, groupRemaining, isAiDispatchStrategy, isAvailable, loadConfig, makeDeterministicFixUnit, makeDeterministicFixer, normalize, orchestrate, planRepair, planWork, planWorkFromRepairs, renderSummary, retryCommand, revertFile, route, runScanner, scopeFindings, showCommand, trackForTool, zeroUsage } from "./config-BPLYzcro.js";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -20,6 +20,14 @@ Only edit these repo-relative files:
|
|
|
20
20
|
Do not edit any other file. If the correct fix requires another file, leave the
|
|
21
21
|
files unchanged.
|
|
22
22
|
|
|
23
|
+
## Current file content
|
|
24
|
+
|
|
25
|
+
The current on-disk content of the editable file(s) follows, so you can edit
|
|
26
|
+
without reading first. Treat it as the source of truth; if it differs from what
|
|
27
|
+
you expect, re-read before editing.
|
|
28
|
+
|
|
29
|
+
{{fileContents}}
|
|
30
|
+
|
|
23
31
|
## Verification targets
|
|
24
32
|
|
|
25
33
|
The gate will verify these repo-relative files:
|