agent-gauntlet 0.10.0 → 0.11.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/README.md +25 -23
- package/dist/index.js +9226 -0
- package/dist/index.js.map +65 -0
- package/dist/scripts/status.js +280 -0
- package/dist/scripts/status.js.map +10 -0
- package/package.json +22 -8
- package/src/built-in-reviews/code-quality.md +0 -25
- package/src/built-in-reviews/index.ts +0 -28
- package/src/bun-plugins.d.ts +0 -4
- package/src/cli-adapters/claude.ts +0 -327
- package/src/cli-adapters/codex.ts +0 -290
- package/src/cli-adapters/cursor.ts +0 -128
- package/src/cli-adapters/gemini.ts +0 -510
- package/src/cli-adapters/github-copilot.ts +0 -141
- package/src/cli-adapters/index.ts +0 -250
- package/src/cli-adapters/thinking-budget.ts +0 -23
- package/src/commands/check.ts +0 -311
- package/src/commands/ci/index.ts +0 -15
- package/src/commands/ci/init.ts +0 -96
- package/src/commands/ci/list-jobs.ts +0 -90
- package/src/commands/clean.ts +0 -54
- package/src/commands/detect.ts +0 -173
- package/src/commands/health.ts +0 -169
- package/src/commands/help.ts +0 -34
- package/src/commands/index.ts +0 -13
- package/src/commands/init.ts +0 -1878
- package/src/commands/list.ts +0 -33
- package/src/commands/review.ts +0 -311
- package/src/commands/run.ts +0 -29
- package/src/commands/shared.ts +0 -267
- package/src/commands/stop-hook.ts +0 -567
- package/src/commands/validate.ts +0 -20
- package/src/commands/wait-ci.ts +0 -518
- package/src/config/ci-loader.ts +0 -33
- package/src/config/ci-schema.ts +0 -28
- package/src/config/global.ts +0 -87
- package/src/config/loader.ts +0 -301
- package/src/config/schema.ts +0 -165
- package/src/config/stop-hook-config.ts +0 -130
- package/src/config/types.ts +0 -65
- package/src/config/validator.ts +0 -592
- package/src/core/change-detector.ts +0 -137
- package/src/core/diff-stats.ts +0 -442
- package/src/core/entry-point.ts +0 -190
- package/src/core/job.ts +0 -96
- package/src/core/run-executor.ts +0 -621
- package/src/core/runner.ts +0 -290
- package/src/gates/check.ts +0 -118
- package/src/gates/resolve-check-command.ts +0 -21
- package/src/gates/result.ts +0 -54
- package/src/gates/review.ts +0 -1333
- package/src/hooks/adapters/claude-stop-hook.ts +0 -99
- package/src/hooks/adapters/cursor-stop-hook.ts +0 -122
- package/src/hooks/adapters/types.ts +0 -94
- package/src/hooks/stop-hook-handler.ts +0 -748
- package/src/index.ts +0 -47
- package/src/output/app-logger.ts +0 -214
- package/src/output/console-log.ts +0 -168
- package/src/output/console.ts +0 -359
- package/src/output/logger.ts +0 -126
- package/src/output/sinks/console-sink.ts +0 -59
- package/src/output/sinks/file-sink.ts +0 -110
- package/src/scripts/status.ts +0 -433
- package/src/templates/workflow.yml +0 -79
- package/src/types/gauntlet-status.ts +0 -79
- package/src/utils/debug-log.ts +0 -392
- package/src/utils/diff-parser.ts +0 -103
- package/src/utils/execution-state.ts +0 -472
- package/src/utils/log-parser.ts +0 -696
- package/src/utils/sanitizer.ts +0 -3
- package/src/utils/session-ref.ts +0 -91
package/src/commands/list.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { loadConfig } from "../config/loader.js";
|
|
4
|
-
|
|
5
|
-
export function registerListCommand(program: Command): void {
|
|
6
|
-
program
|
|
7
|
-
.command("list")
|
|
8
|
-
.description("List configured gates")
|
|
9
|
-
.action(async () => {
|
|
10
|
-
try {
|
|
11
|
-
const config = await loadConfig();
|
|
12
|
-
console.log(chalk.bold("Check Gates:"));
|
|
13
|
-
Object.values(config.checks).forEach((c) => {
|
|
14
|
-
console.log(` - ${c.name}`);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
console.log(chalk.bold("\nReview Gates:"));
|
|
18
|
-
Object.values(config.reviews).forEach((r) => {
|
|
19
|
-
console.log(` - ${r.name} (Tools: ${r.cli_preference?.join(", ")})`);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
console.log(chalk.bold("\nEntry Points:"));
|
|
23
|
-
config.project.entry_points.forEach((ep) => {
|
|
24
|
-
console.log(` - ${ep.path}`);
|
|
25
|
-
if (ep.checks) console.log(` Checks: ${ep.checks.join(", ")}`);
|
|
26
|
-
if (ep.reviews) console.log(` Reviews: ${ep.reviews.join(", ")}`);
|
|
27
|
-
});
|
|
28
|
-
} catch (error: unknown) {
|
|
29
|
-
const err = error as { message?: string };
|
|
30
|
-
console.error(chalk.red("Error:"), err.message);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
package/src/commands/review.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { loadGlobalConfig } from "../config/global.js";
|
|
4
|
-
import { loadConfig } from "../config/loader.js";
|
|
5
|
-
import { ChangeDetector } from "../core/change-detector.js";
|
|
6
|
-
import { EntryPointExpander } from "../core/entry-point.js";
|
|
7
|
-
import { JobGenerator } from "../core/job.js";
|
|
8
|
-
import { Runner } from "../core/runner.js";
|
|
9
|
-
import { ConsoleReporter } from "../output/console.js";
|
|
10
|
-
import {
|
|
11
|
-
type ConsoleLogHandle,
|
|
12
|
-
startConsoleLog,
|
|
13
|
-
} from "../output/console-log.js";
|
|
14
|
-
import { Logger } from "../output/logger.js";
|
|
15
|
-
import {
|
|
16
|
-
getDebugLogger,
|
|
17
|
-
initDebugLogger,
|
|
18
|
-
mergeDebugLogConfig,
|
|
19
|
-
} from "../utils/debug-log.js";
|
|
20
|
-
import {
|
|
21
|
-
readExecutionState,
|
|
22
|
-
resolveFixBase,
|
|
23
|
-
writeExecutionState,
|
|
24
|
-
} from "../utils/execution-state.js";
|
|
25
|
-
import {
|
|
26
|
-
findPreviousFailures,
|
|
27
|
-
type PassedSlot,
|
|
28
|
-
type PreviousViolation,
|
|
29
|
-
} from "../utils/log-parser.js";
|
|
30
|
-
import {
|
|
31
|
-
acquireLock,
|
|
32
|
-
cleanLogs,
|
|
33
|
-
hasExistingLogs,
|
|
34
|
-
performAutoClean,
|
|
35
|
-
releaseLock,
|
|
36
|
-
shouldAutoClean,
|
|
37
|
-
} from "./shared.js";
|
|
38
|
-
|
|
39
|
-
export function registerReviewCommand(program: Command): void {
|
|
40
|
-
program
|
|
41
|
-
.command("review")
|
|
42
|
-
.description("Run only applicable reviews for detected changes")
|
|
43
|
-
.option(
|
|
44
|
-
"-b, --base-branch <branch>",
|
|
45
|
-
"Override base branch for change detection",
|
|
46
|
-
)
|
|
47
|
-
.option("-g, --gate <name>", "Run specific review gate only")
|
|
48
|
-
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
49
|
-
.option(
|
|
50
|
-
"-u, --uncommitted",
|
|
51
|
-
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
52
|
-
)
|
|
53
|
-
.action(async (options) => {
|
|
54
|
-
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
55
|
-
let lockAcquired = false;
|
|
56
|
-
let restoreConsole: ConsoleLogHandle | undefined;
|
|
57
|
-
try {
|
|
58
|
-
config = await loadConfig();
|
|
59
|
-
|
|
60
|
-
// Initialize debug logger
|
|
61
|
-
const globalConfig = await loadGlobalConfig();
|
|
62
|
-
const debugLogConfig = mergeDebugLogConfig(
|
|
63
|
-
config.project.debug_log,
|
|
64
|
-
globalConfig.debug_log,
|
|
65
|
-
);
|
|
66
|
-
initDebugLogger(config.project.log_dir, debugLogConfig);
|
|
67
|
-
|
|
68
|
-
// Log the command invocation
|
|
69
|
-
const debugLogger = getDebugLogger();
|
|
70
|
-
const args = [
|
|
71
|
-
options.baseBranch ? `-b ${options.baseBranch}` : "",
|
|
72
|
-
options.gate ? `-g ${options.gate}` : "",
|
|
73
|
-
options.commit ? `-c ${options.commit}` : "",
|
|
74
|
-
options.uncommitted ? "-u" : "",
|
|
75
|
-
].filter(Boolean);
|
|
76
|
-
await debugLogger?.logCommand("review", args);
|
|
77
|
-
|
|
78
|
-
// Determine effective base branch first (needed for auto-clean)
|
|
79
|
-
const effectiveBaseBranch =
|
|
80
|
-
options.baseBranch ||
|
|
81
|
-
(process.env.GITHUB_BASE_REF &&
|
|
82
|
-
(process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
|
|
83
|
-
? process.env.GITHUB_BASE_REF
|
|
84
|
-
: null) ||
|
|
85
|
-
config.project.base_branch;
|
|
86
|
-
|
|
87
|
-
// Auto-clean on context change (branch changed, commit merged)
|
|
88
|
-
const autoCleanResult = await shouldAutoClean(
|
|
89
|
-
config.project.log_dir,
|
|
90
|
-
effectiveBaseBranch,
|
|
91
|
-
);
|
|
92
|
-
if (autoCleanResult.clean) {
|
|
93
|
-
console.log(
|
|
94
|
-
chalk.dim(`Auto-cleaning logs (${autoCleanResult.reason})...`),
|
|
95
|
-
);
|
|
96
|
-
await debugLogger?.logClean(
|
|
97
|
-
"auto",
|
|
98
|
-
autoCleanResult.reason || "unknown",
|
|
99
|
-
);
|
|
100
|
-
await performAutoClean(config.project.log_dir, autoCleanResult);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Detect rerun mode after auto-clean (clean may have removed logs)
|
|
104
|
-
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
105
|
-
const isRerun = logsExist && !options.commit;
|
|
106
|
-
|
|
107
|
-
// Acquire lock BEFORE starting console log (prevents orphaned log files)
|
|
108
|
-
await acquireLock(config.project.log_dir);
|
|
109
|
-
lockAcquired = true;
|
|
110
|
-
|
|
111
|
-
// Initialize Logger early to get unified run number for console log
|
|
112
|
-
const logger = new Logger(config.project.log_dir);
|
|
113
|
-
await logger.init();
|
|
114
|
-
const runNumber = logger.getRunNumber();
|
|
115
|
-
|
|
116
|
-
restoreConsole = await startConsoleLog(
|
|
117
|
-
config.project.log_dir,
|
|
118
|
-
runNumber,
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
let failuresMap:
|
|
122
|
-
| Map<string, Map<string, PreviousViolation[]>>
|
|
123
|
-
| undefined;
|
|
124
|
-
let changeOptions:
|
|
125
|
-
| { commit?: string; uncommitted?: boolean; fixBase?: string }
|
|
126
|
-
| undefined;
|
|
127
|
-
|
|
128
|
-
let passedSlotsMap: Map<string, Map<number, PassedSlot>> | undefined;
|
|
129
|
-
|
|
130
|
-
if (isRerun) {
|
|
131
|
-
console.log(
|
|
132
|
-
chalk.dim(
|
|
133
|
-
"Existing logs detected — running in verification mode...",
|
|
134
|
-
),
|
|
135
|
-
);
|
|
136
|
-
const { failures: previousFailures, passedSlots } =
|
|
137
|
-
await findPreviousFailures(
|
|
138
|
-
config.project.log_dir,
|
|
139
|
-
options.gate,
|
|
140
|
-
true,
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
failuresMap = new Map();
|
|
144
|
-
for (const gateFailure of previousFailures) {
|
|
145
|
-
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
146
|
-
for (const af of gateFailure.adapterFailures) {
|
|
147
|
-
const key = af.reviewIndex
|
|
148
|
-
? String(af.reviewIndex)
|
|
149
|
-
: af.adapterName;
|
|
150
|
-
adapterMap.set(key, af.violations);
|
|
151
|
-
}
|
|
152
|
-
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
passedSlotsMap = passedSlots;
|
|
156
|
-
|
|
157
|
-
if (previousFailures.length > 0) {
|
|
158
|
-
const totalViolations = previousFailures.reduce(
|
|
159
|
-
(sum, gf) =>
|
|
160
|
-
sum +
|
|
161
|
-
gf.adapterFailures.reduce(
|
|
162
|
-
(s, af) => s + af.violations.length,
|
|
163
|
-
0,
|
|
164
|
-
),
|
|
165
|
-
0,
|
|
166
|
-
);
|
|
167
|
-
console.log(
|
|
168
|
-
chalk.yellow(
|
|
169
|
-
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
170
|
-
),
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
changeOptions = { uncommitted: true };
|
|
175
|
-
// Use working_tree_ref from execution state for rerun diff scoping
|
|
176
|
-
const executionState = await readExecutionState(
|
|
177
|
-
config.project.log_dir,
|
|
178
|
-
);
|
|
179
|
-
if (executionState?.working_tree_ref) {
|
|
180
|
-
changeOptions.fixBase = executionState.working_tree_ref;
|
|
181
|
-
}
|
|
182
|
-
} else if (!logsExist) {
|
|
183
|
-
// Post-clean run: check if execution state has a working_tree_ref to use as fixBase
|
|
184
|
-
const executionState = await readExecutionState(
|
|
185
|
-
config.project.log_dir,
|
|
186
|
-
);
|
|
187
|
-
if (executionState) {
|
|
188
|
-
const resolved = await resolveFixBase(
|
|
189
|
-
executionState,
|
|
190
|
-
effectiveBaseBranch,
|
|
191
|
-
);
|
|
192
|
-
if (resolved.warning) {
|
|
193
|
-
console.log(chalk.yellow(`Warning: ${resolved.warning}`));
|
|
194
|
-
}
|
|
195
|
-
if (resolved.fixBase) {
|
|
196
|
-
changeOptions = { fixBase: resolved.fixBase };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Allow explicit commit or uncommitted options to override fixBase
|
|
202
|
-
if (options.commit || options.uncommitted) {
|
|
203
|
-
changeOptions = {
|
|
204
|
-
commit: options.commit,
|
|
205
|
-
uncommitted: options.uncommitted,
|
|
206
|
-
fixBase: changeOptions?.fixBase,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const changeDetector = new ChangeDetector(
|
|
211
|
-
effectiveBaseBranch,
|
|
212
|
-
changeOptions || {
|
|
213
|
-
commit: options.commit,
|
|
214
|
-
uncommitted: options.uncommitted,
|
|
215
|
-
},
|
|
216
|
-
);
|
|
217
|
-
const expander = new EntryPointExpander();
|
|
218
|
-
const jobGen = new JobGenerator(config);
|
|
219
|
-
|
|
220
|
-
console.log(chalk.dim("Detecting changes..."));
|
|
221
|
-
const changes = await changeDetector.getChangedFiles();
|
|
222
|
-
|
|
223
|
-
if (changes.length === 0) {
|
|
224
|
-
console.log(chalk.green("No changes detected."));
|
|
225
|
-
await writeExecutionState(config.project.log_dir);
|
|
226
|
-
await releaseLock(config.project.log_dir);
|
|
227
|
-
restoreConsole?.restore();
|
|
228
|
-
process.exit(0);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
console.log(chalk.dim(`Found ${changes.length} changed files.`));
|
|
232
|
-
|
|
233
|
-
const entryPoints = await expander.expand(
|
|
234
|
-
config.project.entry_points,
|
|
235
|
-
changes,
|
|
236
|
-
);
|
|
237
|
-
let jobs = jobGen.generateJobs(entryPoints);
|
|
238
|
-
|
|
239
|
-
// Filter to only reviews
|
|
240
|
-
jobs = jobs.filter((j) => j.type === "review");
|
|
241
|
-
|
|
242
|
-
if (options.gate) {
|
|
243
|
-
jobs = jobs.filter((j) => j.name === options.gate);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (jobs.length === 0) {
|
|
247
|
-
console.log(chalk.yellow("No applicable reviews for these changes."));
|
|
248
|
-
await writeExecutionState(config.project.log_dir);
|
|
249
|
-
await releaseLock(config.project.log_dir);
|
|
250
|
-
restoreConsole?.restore();
|
|
251
|
-
process.exit(0);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
console.log(chalk.dim(`Running ${jobs.length} review(s)...`));
|
|
255
|
-
|
|
256
|
-
// Log run start
|
|
257
|
-
const runMode = isRerun ? "verification" : "full";
|
|
258
|
-
await debugLogger?.logRunStart(runMode, changes.length, jobs.length);
|
|
259
|
-
|
|
260
|
-
const reporter = new ConsoleReporter();
|
|
261
|
-
const runner = new Runner(
|
|
262
|
-
config,
|
|
263
|
-
logger,
|
|
264
|
-
reporter,
|
|
265
|
-
failuresMap,
|
|
266
|
-
changeOptions,
|
|
267
|
-
effectiveBaseBranch,
|
|
268
|
-
passedSlotsMap,
|
|
269
|
-
debugLogger ?? undefined,
|
|
270
|
-
isRerun,
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const success = await runner.run(jobs);
|
|
274
|
-
|
|
275
|
-
// Log run end
|
|
276
|
-
await debugLogger?.logRunEnd(
|
|
277
|
-
success ? "pass" : "fail",
|
|
278
|
-
0,
|
|
279
|
-
0,
|
|
280
|
-
0,
|
|
281
|
-
logger.getRunNumber(),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
// Write execution state before releasing lock (for interval checks)
|
|
285
|
-
// This now captures working_tree_ref which is used for rerun diff scoping
|
|
286
|
-
await writeExecutionState(config.project.log_dir);
|
|
287
|
-
|
|
288
|
-
if (success) {
|
|
289
|
-
await debugLogger?.logClean("auto", "all_passed");
|
|
290
|
-
await cleanLogs(config.project.log_dir);
|
|
291
|
-
}
|
|
292
|
-
await releaseLock(config.project.log_dir);
|
|
293
|
-
restoreConsole?.restore();
|
|
294
|
-
process.exit(success ? 0 : 1);
|
|
295
|
-
} catch (error: unknown) {
|
|
296
|
-
// Write execution state even on error (if lock was acquired)
|
|
297
|
-
if (config && lockAcquired) {
|
|
298
|
-
try {
|
|
299
|
-
await writeExecutionState(config.project.log_dir);
|
|
300
|
-
} catch {
|
|
301
|
-
// Ignore errors writing state during error handling
|
|
302
|
-
}
|
|
303
|
-
await releaseLock(config.project.log_dir);
|
|
304
|
-
}
|
|
305
|
-
const err = error as { message?: string };
|
|
306
|
-
console.error(chalk.red("Error:"), err.message);
|
|
307
|
-
restoreConsole?.restore();
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
}
|
package/src/commands/run.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { executeRun } from "../core/run-executor.js";
|
|
3
|
-
import { isSuccessStatus } from "../types/gauntlet-status.js";
|
|
4
|
-
|
|
5
|
-
export function registerRunCommand(program: Command): void {
|
|
6
|
-
program
|
|
7
|
-
.command("run")
|
|
8
|
-
.description("Run gates for detected changes")
|
|
9
|
-
.option(
|
|
10
|
-
"-b, --base-branch <branch>",
|
|
11
|
-
"Override base branch for change detection",
|
|
12
|
-
)
|
|
13
|
-
.option("-g, --gate <name>", "Run specific gate only")
|
|
14
|
-
.option("-c, --commit <sha>", "Use diff for a specific commit")
|
|
15
|
-
.option(
|
|
16
|
-
"-u, --uncommitted",
|
|
17
|
-
"Use diff for current uncommitted changes (staged and unstaged)",
|
|
18
|
-
)
|
|
19
|
-
.action(async (options) => {
|
|
20
|
-
const result = await executeRun({
|
|
21
|
-
baseBranch: options.baseBranch,
|
|
22
|
-
gate: options.gate,
|
|
23
|
-
commit: options.commit,
|
|
24
|
-
uncommitted: options.uncommitted,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
process.exit(isSuccessStatus(result.status) ? 0 : 1);
|
|
28
|
-
});
|
|
29
|
-
}
|
package/src/commands/shared.ts
DELETED
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
getDebugLogBackupFilename,
|
|
5
|
-
getDebugLogFilename,
|
|
6
|
-
} from "../utils/debug-log.js";
|
|
7
|
-
import {
|
|
8
|
-
deleteExecutionState,
|
|
9
|
-
getCurrentBranch,
|
|
10
|
-
getExecutionStateFilename,
|
|
11
|
-
isCommitInBranch,
|
|
12
|
-
readExecutionState,
|
|
13
|
-
} from "../utils/execution-state.js";
|
|
14
|
-
|
|
15
|
-
const LOCK_FILENAME = ".gauntlet-run.lock";
|
|
16
|
-
const SESSION_REF_FILENAME = ".session_ref";
|
|
17
|
-
|
|
18
|
-
export interface AutoCleanResult {
|
|
19
|
-
clean: boolean;
|
|
20
|
-
reason?: string;
|
|
21
|
-
resetState?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Check if logs should be auto-cleaned based on execution context changes.
|
|
26
|
-
* Returns { clean: true, reason, resetState } if context has changed.
|
|
27
|
-
* Returns { clean: false } if context is unchanged or state file doesn't exist.
|
|
28
|
-
* When resetState is true, the execution state should be deleted (not just logs).
|
|
29
|
-
*/
|
|
30
|
-
export async function shouldAutoClean(
|
|
31
|
-
logDir: string,
|
|
32
|
-
baseBranch: string,
|
|
33
|
-
): Promise<AutoCleanResult> {
|
|
34
|
-
const state = await readExecutionState(logDir);
|
|
35
|
-
|
|
36
|
-
// No state file = no auto-clean needed
|
|
37
|
-
if (!state) {
|
|
38
|
-
return { clean: false };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if branch changed
|
|
42
|
-
try {
|
|
43
|
-
const currentBranch = await getCurrentBranch();
|
|
44
|
-
if (currentBranch !== state.branch) {
|
|
45
|
-
return { clean: true, reason: "branch changed", resetState: true };
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
// If we can't get the current branch, don't auto-clean
|
|
49
|
-
return { clean: false };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Check if commit was merged into base branch
|
|
53
|
-
try {
|
|
54
|
-
const isMerged = await isCommitInBranch(state.commit, baseBranch);
|
|
55
|
-
if (isMerged) {
|
|
56
|
-
return { clean: true, reason: "commit merged", resetState: true };
|
|
57
|
-
}
|
|
58
|
-
} catch {
|
|
59
|
-
// If we can't check merge status, don't auto-clean
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { clean: false };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Perform auto-clean with state reset if needed.
|
|
67
|
-
*/
|
|
68
|
-
export async function performAutoClean(
|
|
69
|
-
logDir: string,
|
|
70
|
-
result: AutoCleanResult,
|
|
71
|
-
maxPreviousLogs = 3,
|
|
72
|
-
): Promise<void> {
|
|
73
|
-
await cleanLogs(logDir, maxPreviousLogs);
|
|
74
|
-
|
|
75
|
-
// Delete execution state if context changed (branch changed or commit merged)
|
|
76
|
-
if (result.resetState) {
|
|
77
|
-
await deleteExecutionState(logDir);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Get the lock filename constant.
|
|
83
|
-
* Useful for checking lock status from other modules.
|
|
84
|
-
*/
|
|
85
|
-
export function getLockFilename(): string {
|
|
86
|
-
return LOCK_FILENAME;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export async function exists(filePath: string): Promise<boolean> {
|
|
90
|
-
try {
|
|
91
|
-
await fs.stat(filePath);
|
|
92
|
-
return true;
|
|
93
|
-
} catch {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function acquireLock(logDir: string): Promise<void> {
|
|
99
|
-
await fs.mkdir(logDir, { recursive: true });
|
|
100
|
-
const lockPath = path.resolve(logDir, LOCK_FILENAME);
|
|
101
|
-
try {
|
|
102
|
-
await fs.writeFile(lockPath, String(process.pid), { flag: "wx" });
|
|
103
|
-
} catch (err: unknown) {
|
|
104
|
-
if (
|
|
105
|
-
typeof err === "object" &&
|
|
106
|
-
err !== null &&
|
|
107
|
-
"code" in err &&
|
|
108
|
-
(err as { code: string }).code === "EEXIST"
|
|
109
|
-
) {
|
|
110
|
-
console.error(
|
|
111
|
-
`Error: A gauntlet run is already in progress (lock file: ${lockPath}).`,
|
|
112
|
-
);
|
|
113
|
-
console.error(
|
|
114
|
-
"If no run is actually in progress, delete the lock file manually.",
|
|
115
|
-
);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export async function releaseLock(logDir: string): Promise<void> {
|
|
123
|
-
const lockPath = path.resolve(logDir, LOCK_FILENAME);
|
|
124
|
-
try {
|
|
125
|
-
await fs.rm(lockPath, { force: true });
|
|
126
|
-
} catch {
|
|
127
|
-
// no-op if missing
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function hasExistingLogs(logDir: string): Promise<boolean> {
|
|
132
|
-
try {
|
|
133
|
-
const entries = await fs.readdir(logDir);
|
|
134
|
-
return entries.some(
|
|
135
|
-
(f) =>
|
|
136
|
-
(f.endsWith(".log") || f.endsWith(".json")) &&
|
|
137
|
-
f !== "previous" &&
|
|
138
|
-
!f.startsWith("console.") &&
|
|
139
|
-
!f.startsWith("."),
|
|
140
|
-
);
|
|
141
|
-
} catch {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get the set of persistent files that should never be moved during clean.
|
|
148
|
-
*/
|
|
149
|
-
/**
|
|
150
|
-
* Marker file used by stop-hook to detect nested invocations.
|
|
151
|
-
* Must match STOP_HOOK_MARKER_FILE in stop-hook.ts.
|
|
152
|
-
*/
|
|
153
|
-
const STOP_HOOK_MARKER_FILE = ".stop-hook-active";
|
|
154
|
-
|
|
155
|
-
function getPersistentFiles(): Set<string> {
|
|
156
|
-
return new Set([
|
|
157
|
-
getExecutionStateFilename(),
|
|
158
|
-
getDebugLogFilename(),
|
|
159
|
-
getDebugLogBackupFilename(),
|
|
160
|
-
LOCK_FILENAME,
|
|
161
|
-
SESSION_REF_FILENAME, // Will be deleted, not moved
|
|
162
|
-
STOP_HOOK_MARKER_FILE, // Cleaned up by stop-hook finally block, not cleanLogs
|
|
163
|
-
]);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Check if there are current logs to archive.
|
|
168
|
-
* Returns true if there are .log or .json files in the log directory root.
|
|
169
|
-
* Excludes persistent files (.execution_state, .debug.log, etc.)
|
|
170
|
-
*/
|
|
171
|
-
async function hasCurrentLogs(logDir: string): Promise<boolean> {
|
|
172
|
-
try {
|
|
173
|
-
const files = await fs.readdir(logDir);
|
|
174
|
-
const persistentFiles = getPersistentFiles();
|
|
175
|
-
return files.some(
|
|
176
|
-
(f) =>
|
|
177
|
-
(f.endsWith(".log") || f.endsWith(".json")) &&
|
|
178
|
-
f !== "previous" &&
|
|
179
|
-
!persistentFiles.has(f),
|
|
180
|
-
);
|
|
181
|
-
} catch {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Get current log files (excludes previous dirs and persistent files). */
|
|
187
|
-
function getCurrentLogFiles(files: string[]): string[] {
|
|
188
|
-
const persistentFiles = getPersistentFiles();
|
|
189
|
-
return files.filter(
|
|
190
|
-
(file) => !file.startsWith("previous") && !persistentFiles.has(file),
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Delete current logs without archiving (maxPreviousLogs === 0). */
|
|
195
|
-
async function deleteCurrentLogs(logDir: string): Promise<void> {
|
|
196
|
-
const files = await fs.readdir(logDir);
|
|
197
|
-
await Promise.all(
|
|
198
|
-
getCurrentLogFiles(files).map((file) =>
|
|
199
|
-
fs.rm(path.join(logDir, file), { recursive: true, force: true }),
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/** Rotate existing previous/ directories to make room for a new archive. */
|
|
205
|
-
async function rotatePreviousDirs(
|
|
206
|
-
logDir: string,
|
|
207
|
-
maxPreviousLogs: number,
|
|
208
|
-
): Promise<void> {
|
|
209
|
-
const oldestSuffix = maxPreviousLogs - 1;
|
|
210
|
-
const oldestDir =
|
|
211
|
-
oldestSuffix === 0 ? "previous" : `previous.${oldestSuffix}`;
|
|
212
|
-
const oldestPath = path.join(logDir, oldestDir);
|
|
213
|
-
if (await exists(oldestPath)) {
|
|
214
|
-
await fs.rm(oldestPath, { recursive: true, force: true });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
for (let i = oldestSuffix - 1; i >= 0; i--) {
|
|
218
|
-
const fromName = i === 0 ? "previous" : `previous.${i}`;
|
|
219
|
-
const toName = `previous.${i + 1}`;
|
|
220
|
-
const fromPath = path.join(logDir, fromName);
|
|
221
|
-
const toPath = path.join(logDir, toName);
|
|
222
|
-
if (await exists(fromPath)) {
|
|
223
|
-
await fs.rename(fromPath, toPath);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export async function cleanLogs(
|
|
229
|
-
logDir: string,
|
|
230
|
-
maxPreviousLogs = 3,
|
|
231
|
-
): Promise<void> {
|
|
232
|
-
try {
|
|
233
|
-
if (!(await exists(logDir))) return;
|
|
234
|
-
if (!(await hasCurrentLogs(logDir))) return;
|
|
235
|
-
|
|
236
|
-
if (maxPreviousLogs === 0) {
|
|
237
|
-
await deleteCurrentLogs(logDir);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
await rotatePreviousDirs(logDir, maxPreviousLogs);
|
|
242
|
-
|
|
243
|
-
const previousDir = path.join(logDir, "previous");
|
|
244
|
-
await fs.mkdir(previousDir, { recursive: true });
|
|
245
|
-
|
|
246
|
-
const files = await fs.readdir(logDir);
|
|
247
|
-
await Promise.all(
|
|
248
|
-
getCurrentLogFiles(files).map((file) =>
|
|
249
|
-
fs.rename(path.join(logDir, file), path.join(previousDir, file)),
|
|
250
|
-
),
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
// Delete legacy .session_ref if it exists (migration cleanup)
|
|
254
|
-
try {
|
|
255
|
-
await fs.rm(path.join(logDir, SESSION_REF_FILENAME), { force: true });
|
|
256
|
-
} catch {
|
|
257
|
-
// Ignore errors
|
|
258
|
-
}
|
|
259
|
-
} catch (error) {
|
|
260
|
-
console.warn(
|
|
261
|
-
"Failed to clean logs in",
|
|
262
|
-
logDir,
|
|
263
|
-
":",
|
|
264
|
-
error instanceof Error ? error.message : error,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
}
|