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/core/run-executor.ts
DELETED
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
cleanLogs,
|
|
5
|
-
hasExistingLogs,
|
|
6
|
-
performAutoClean,
|
|
7
|
-
releaseLock,
|
|
8
|
-
shouldAutoClean,
|
|
9
|
-
} from "../commands/shared.js";
|
|
10
|
-
import { loadGlobalConfig } from "../config/global.js";
|
|
11
|
-
import { loadConfig } from "../config/loader.js";
|
|
12
|
-
import { resolveStopHookConfig } from "../config/stop-hook-config.js";
|
|
13
|
-
import {
|
|
14
|
-
getCategoryLogger,
|
|
15
|
-
initLogger,
|
|
16
|
-
isLoggerConfigured,
|
|
17
|
-
resetLogger,
|
|
18
|
-
} from "../output/app-logger.js";
|
|
19
|
-
import { ConsoleReporter } from "../output/console.js";
|
|
20
|
-
import {
|
|
21
|
-
type ConsoleLogHandle,
|
|
22
|
-
startConsoleLog,
|
|
23
|
-
} from "../output/console-log.js";
|
|
24
|
-
import { Logger } from "../output/logger.js";
|
|
25
|
-
import type { GauntletStatus, RunResult } from "../types/gauntlet-status.js";
|
|
26
|
-
import {
|
|
27
|
-
getDebugLogger,
|
|
28
|
-
initDebugLogger,
|
|
29
|
-
mergeDebugLogConfig,
|
|
30
|
-
} from "../utils/debug-log.js";
|
|
31
|
-
import {
|
|
32
|
-
readExecutionState,
|
|
33
|
-
resolveFixBase,
|
|
34
|
-
writeExecutionState,
|
|
35
|
-
} from "../utils/execution-state.js";
|
|
36
|
-
import {
|
|
37
|
-
findPreviousFailures,
|
|
38
|
-
type PassedSlot,
|
|
39
|
-
type PreviousViolation,
|
|
40
|
-
} from "../utils/log-parser.js";
|
|
41
|
-
import { ChangeDetector } from "./change-detector.js";
|
|
42
|
-
import { computeDiffStats } from "./diff-stats.js";
|
|
43
|
-
import { EntryPointExpander } from "./entry-point.js";
|
|
44
|
-
import { JobGenerator } from "./job.js";
|
|
45
|
-
import { Runner } from "./runner.js";
|
|
46
|
-
|
|
47
|
-
const LOCK_FILENAME = ".gauntlet-run.lock";
|
|
48
|
-
|
|
49
|
-
export interface ExecuteRunOptions {
|
|
50
|
-
baseBranch?: string;
|
|
51
|
-
gate?: string;
|
|
52
|
-
commit?: string;
|
|
53
|
-
uncommitted?: boolean;
|
|
54
|
-
/** Working directory for config loading (defaults to process.cwd()) */
|
|
55
|
-
cwd?: string;
|
|
56
|
-
/**
|
|
57
|
-
* When true, check if run interval has elapsed before proceeding.
|
|
58
|
-
* Only stop-hook uses this; CLI commands (run, check, review) always run immediately.
|
|
59
|
-
* If interval hasn't elapsed, returns { status: "interval_not_elapsed", ... }.
|
|
60
|
-
*/
|
|
61
|
-
checkInterval?: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Maximum age for a lock file before it's considered stale (10 minutes).
|
|
66
|
-
* Matches the stale marker threshold in stop-hook.ts.
|
|
67
|
-
*/
|
|
68
|
-
const STALE_LOCK_MS = 10 * 60 * 1000;
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Check if a process with the given PID is still alive.
|
|
72
|
-
*/
|
|
73
|
-
function isProcessAlive(pid: number): boolean {
|
|
74
|
-
try {
|
|
75
|
-
process.kill(pid, 0); // Signal 0 = check existence without killing
|
|
76
|
-
return true;
|
|
77
|
-
} catch (err: unknown) {
|
|
78
|
-
// EPERM means the process exists but we lack permission to signal it
|
|
79
|
-
if (
|
|
80
|
-
typeof err === "object" &&
|
|
81
|
-
err !== null &&
|
|
82
|
-
"code" in err &&
|
|
83
|
-
(err as { code: string }).code === "EPERM"
|
|
84
|
-
) {
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
// ESRCH or other errors mean the process doesn't exist
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Acquire the lock file. Returns true if successful, false if lock exists.
|
|
94
|
-
* Unlike acquireLock() in shared.ts, this doesn't call process.exit().
|
|
95
|
-
*
|
|
96
|
-
* If the lock file already exists, checks for staleness:
|
|
97
|
-
* - If the PID in the lock file is no longer alive, removes the lock and retries.
|
|
98
|
-
* - If the lock file is older than STALE_LOCK_MS, removes the lock and retries.
|
|
99
|
-
* This prevents zombie processes from holding locks indefinitely.
|
|
100
|
-
*/
|
|
101
|
-
async function tryAcquireLock(logDir: string): Promise<boolean> {
|
|
102
|
-
await fs.mkdir(logDir, { recursive: true });
|
|
103
|
-
const lockPath = path.resolve(logDir, LOCK_FILENAME);
|
|
104
|
-
try {
|
|
105
|
-
await fs.writeFile(lockPath, String(process.pid), { flag: "wx" });
|
|
106
|
-
return true;
|
|
107
|
-
} catch (err: unknown) {
|
|
108
|
-
if (
|
|
109
|
-
typeof err === "object" &&
|
|
110
|
-
err !== null &&
|
|
111
|
-
"code" in err &&
|
|
112
|
-
(err as { code: string }).code === "EEXIST"
|
|
113
|
-
) {
|
|
114
|
-
// Lock exists — check if the holding process is still alive
|
|
115
|
-
try {
|
|
116
|
-
const lockContent = await fs.readFile(lockPath, "utf-8");
|
|
117
|
-
const lockPid = parseInt(lockContent.trim(), 10);
|
|
118
|
-
const lockStat = await fs.stat(lockPath);
|
|
119
|
-
const lockAgeMs = Date.now() - lockStat.mtimeMs;
|
|
120
|
-
|
|
121
|
-
const pidValid = !Number.isNaN(lockPid);
|
|
122
|
-
const pidDead = pidValid && !isProcessAlive(lockPid);
|
|
123
|
-
// Only use time-based staleness when we can't determine the PID
|
|
124
|
-
// (e.g. lock file is empty or contains non-numeric content).
|
|
125
|
-
// If the PID is valid and alive, never steal the lock regardless of age.
|
|
126
|
-
const lockStale = !pidValid && lockAgeMs > STALE_LOCK_MS;
|
|
127
|
-
|
|
128
|
-
if (pidDead || lockStale) {
|
|
129
|
-
// Stale lock — remove and retry once
|
|
130
|
-
await fs.rm(lockPath, { force: true });
|
|
131
|
-
try {
|
|
132
|
-
await fs.writeFile(lockPath, String(process.pid), {
|
|
133
|
-
flag: "wx",
|
|
134
|
-
});
|
|
135
|
-
return true;
|
|
136
|
-
} catch {
|
|
137
|
-
// Another process beat us to it
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
// Can't read/stat lock file — treat as active lock
|
|
143
|
-
}
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Find the latest console.N.log file in the log directory.
|
|
152
|
-
*/
|
|
153
|
-
async function findLatestConsoleLog(logDir: string): Promise<string | null> {
|
|
154
|
-
try {
|
|
155
|
-
const files = await fs.readdir(logDir);
|
|
156
|
-
let maxNum = -1;
|
|
157
|
-
let latestFile: string | null = null;
|
|
158
|
-
|
|
159
|
-
for (const file of files) {
|
|
160
|
-
if (!file.startsWith("console.") || !file.endsWith(".log")) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
const middle = file.slice("console.".length, file.length - ".log".length);
|
|
164
|
-
if (/^\d+$/.test(middle)) {
|
|
165
|
-
const n = parseInt(middle, 10);
|
|
166
|
-
if (n > maxNum) {
|
|
167
|
-
maxNum = n;
|
|
168
|
-
latestFile = file;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return latestFile ? path.join(logDir, latestFile) : null;
|
|
174
|
-
} catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Check if the run interval has elapsed since the last gauntlet run.
|
|
181
|
-
* Returns true if gauntlet should run, false if interval hasn't elapsed.
|
|
182
|
-
*/
|
|
183
|
-
async function shouldRunBasedOnInterval(
|
|
184
|
-
logDir: string,
|
|
185
|
-
intervalMinutes: number,
|
|
186
|
-
): Promise<boolean> {
|
|
187
|
-
const state = await readExecutionState(logDir);
|
|
188
|
-
if (!state) {
|
|
189
|
-
// No execution state = always run
|
|
190
|
-
return true;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const lastRun = new Date(state.last_run_completed_at);
|
|
194
|
-
// Handle invalid date (corrupted state) - treat as needing to run
|
|
195
|
-
if (Number.isNaN(lastRun.getTime())) {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const now = new Date();
|
|
200
|
-
const elapsedMinutes = (now.getTime() - lastRun.getTime()) / (1000 * 60);
|
|
201
|
-
|
|
202
|
-
return elapsedMinutes >= intervalMinutes;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Get status message for a given status.
|
|
207
|
-
*/
|
|
208
|
-
const statusMessages: Record<GauntletStatus, string> = {
|
|
209
|
-
passed: "All gates passed.",
|
|
210
|
-
passed_with_warnings: "Passed with warnings — some issues were skipped.",
|
|
211
|
-
no_applicable_gates: "No applicable gates for these changes.",
|
|
212
|
-
no_changes: "No changes detected.",
|
|
213
|
-
failed: "Gates failed — issues must be fixed.",
|
|
214
|
-
retry_limit_exceeded:
|
|
215
|
-
"Retry limit exceeded — logs have been automatically archived.",
|
|
216
|
-
lock_conflict: "Another gauntlet run is already in progress.",
|
|
217
|
-
error: "Unexpected error occurred.",
|
|
218
|
-
no_config: "No .gauntlet/config.yml found.",
|
|
219
|
-
stop_hook_active: "Stop hook already active.",
|
|
220
|
-
interval_not_elapsed: "Run interval not elapsed.",
|
|
221
|
-
invalid_input: "Invalid input.",
|
|
222
|
-
stop_hook_disabled: "Stop hook is disabled via configuration.",
|
|
223
|
-
pr_push_required: "Gates passed — PR needs to be created/updated.",
|
|
224
|
-
ci_pending: "CI checks still running.",
|
|
225
|
-
ci_failed: "CI checks failed or review changes requested.",
|
|
226
|
-
ci_passed: "CI checks passed, no blocking reviews.",
|
|
227
|
-
ci_timeout: "CI wait attempts exhausted.",
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
function getStatusMessage(status: GauntletStatus): string {
|
|
231
|
-
return statusMessages[status] || "Unknown status";
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Get the run executor logger.
|
|
236
|
-
*/
|
|
237
|
-
function getRunLogger() {
|
|
238
|
-
return getCategoryLogger("run");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Execute the gauntlet run logic. Returns a structured RunResult.
|
|
243
|
-
* This function never calls process.exit() - the caller is responsible for that.
|
|
244
|
-
*/
|
|
245
|
-
export async function executeRun(
|
|
246
|
-
options: ExecuteRunOptions = {},
|
|
247
|
-
): Promise<RunResult> {
|
|
248
|
-
const { cwd } = options;
|
|
249
|
-
let config: Awaited<ReturnType<typeof loadConfig>> | undefined;
|
|
250
|
-
let lockAcquired = false;
|
|
251
|
-
let consoleLogHandle: ConsoleLogHandle | undefined;
|
|
252
|
-
let loggerInitializedHere = false;
|
|
253
|
-
const log = getRunLogger();
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
config = await loadConfig(cwd);
|
|
257
|
-
|
|
258
|
-
// Initialize app logger if not already configured (e.g., by stop-hook)
|
|
259
|
-
if (!isLoggerConfigured()) {
|
|
260
|
-
await initLogger({
|
|
261
|
-
mode: "interactive",
|
|
262
|
-
logDir: config.project.log_dir,
|
|
263
|
-
});
|
|
264
|
-
loggerInitializedHere = true;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Initialize debug logger
|
|
268
|
-
const globalConfig = await loadGlobalConfig();
|
|
269
|
-
const debugLogConfig = mergeDebugLogConfig(
|
|
270
|
-
config.project.debug_log,
|
|
271
|
-
globalConfig.debug_log,
|
|
272
|
-
);
|
|
273
|
-
initDebugLogger(config.project.log_dir, debugLogConfig);
|
|
274
|
-
|
|
275
|
-
// Log the command invocation
|
|
276
|
-
const debugLogger = getDebugLogger();
|
|
277
|
-
const args = [
|
|
278
|
-
options.baseBranch ? `-b ${options.baseBranch}` : "",
|
|
279
|
-
options.gate ? `-g ${options.gate}` : "",
|
|
280
|
-
options.commit ? `-c ${options.commit}` : "",
|
|
281
|
-
options.uncommitted ? "-u" : "",
|
|
282
|
-
options.checkInterval ? "--check-interval" : "",
|
|
283
|
-
].filter(Boolean);
|
|
284
|
-
await debugLogger?.logCommand("run", args);
|
|
285
|
-
|
|
286
|
-
// Interval check: only stop-hook passes checkInterval: true
|
|
287
|
-
// CLI commands (run, check, review) always run immediately
|
|
288
|
-
if (options.checkInterval) {
|
|
289
|
-
// Resolve stop hook config from env > project > global
|
|
290
|
-
const stopHookConfig = resolveStopHookConfig(
|
|
291
|
-
config.project.stop_hook,
|
|
292
|
-
globalConfig,
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
// Check if stop hook is disabled
|
|
296
|
-
if (!stopHookConfig.enabled) {
|
|
297
|
-
log.debug("Stop hook is disabled via configuration, skipping");
|
|
298
|
-
// Clean up logger if we initialized it
|
|
299
|
-
if (loggerInitializedHere) {
|
|
300
|
-
await resetLogger();
|
|
301
|
-
}
|
|
302
|
-
return {
|
|
303
|
-
status: "stop_hook_disabled",
|
|
304
|
-
message: getStatusMessage("stop_hook_disabled"),
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
309
|
-
// Only check interval if there are no existing logs (not in rerun mode)
|
|
310
|
-
// and interval > 0 (interval 0 means always run)
|
|
311
|
-
if (!logsExist && stopHookConfig.run_interval_minutes > 0) {
|
|
312
|
-
const intervalMinutes = stopHookConfig.run_interval_minutes;
|
|
313
|
-
const shouldRun = await shouldRunBasedOnInterval(
|
|
314
|
-
config.project.log_dir,
|
|
315
|
-
intervalMinutes,
|
|
316
|
-
);
|
|
317
|
-
if (!shouldRun) {
|
|
318
|
-
log.debug(
|
|
319
|
-
`Run interval (${intervalMinutes} min) not elapsed, skipping`,
|
|
320
|
-
);
|
|
321
|
-
// Clean up logger if we initialized it
|
|
322
|
-
if (loggerInitializedHere) {
|
|
323
|
-
await resetLogger();
|
|
324
|
-
}
|
|
325
|
-
return {
|
|
326
|
-
status: "interval_not_elapsed",
|
|
327
|
-
message: `Run interval (${intervalMinutes} min) not elapsed.`,
|
|
328
|
-
intervalMinutes,
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Determine effective base branch first (needed for auto-clean)
|
|
335
|
-
const effectiveBaseBranch =
|
|
336
|
-
options.baseBranch ||
|
|
337
|
-
(process.env.GITHUB_BASE_REF &&
|
|
338
|
-
(process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true")
|
|
339
|
-
? process.env.GITHUB_BASE_REF
|
|
340
|
-
: null) ||
|
|
341
|
-
config.project.base_branch;
|
|
342
|
-
|
|
343
|
-
// Auto-clean on context change (branch changed, commit merged)
|
|
344
|
-
const autoCleanResult = await shouldAutoClean(
|
|
345
|
-
config.project.log_dir,
|
|
346
|
-
effectiveBaseBranch,
|
|
347
|
-
);
|
|
348
|
-
if (autoCleanResult.clean) {
|
|
349
|
-
log.debug(`Auto-cleaning logs (${autoCleanResult.reason})...`);
|
|
350
|
-
await debugLogger?.logClean("auto", autoCleanResult.reason || "unknown");
|
|
351
|
-
await performAutoClean(
|
|
352
|
-
config.project.log_dir,
|
|
353
|
-
autoCleanResult,
|
|
354
|
-
config.project.max_previous_logs,
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Detect rerun mode after auto-clean (clean may have removed logs)
|
|
359
|
-
const logsExist = await hasExistingLogs(config.project.log_dir);
|
|
360
|
-
const isRerun = logsExist && !options.commit;
|
|
361
|
-
|
|
362
|
-
// Try to acquire lock (non-exiting version)
|
|
363
|
-
lockAcquired = await tryAcquireLock(config.project.log_dir);
|
|
364
|
-
if (!lockAcquired) {
|
|
365
|
-
// Clean up logger if we initialized it
|
|
366
|
-
if (loggerInitializedHere) {
|
|
367
|
-
await resetLogger();
|
|
368
|
-
}
|
|
369
|
-
return {
|
|
370
|
-
status: "lock_conflict",
|
|
371
|
-
message: getStatusMessage("lock_conflict"),
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Lock acquired — wrap in try/finally to guarantee release on all paths
|
|
376
|
-
try {
|
|
377
|
-
// Initialize Logger early to get unified run number for console log
|
|
378
|
-
const logger = new Logger(config.project.log_dir);
|
|
379
|
-
await logger.init();
|
|
380
|
-
const runNumber = logger.getRunNumber();
|
|
381
|
-
|
|
382
|
-
consoleLogHandle = await startConsoleLog(
|
|
383
|
-
config.project.log_dir,
|
|
384
|
-
runNumber,
|
|
385
|
-
);
|
|
386
|
-
|
|
387
|
-
let failuresMap:
|
|
388
|
-
| Map<string, Map<string, PreviousViolation[]>>
|
|
389
|
-
| undefined;
|
|
390
|
-
let changeOptions:
|
|
391
|
-
| { commit?: string; uncommitted?: boolean; fixBase?: string }
|
|
392
|
-
| undefined;
|
|
393
|
-
|
|
394
|
-
let passedSlotsMap: Map<string, Map<number, PassedSlot>> | undefined;
|
|
395
|
-
|
|
396
|
-
if (isRerun) {
|
|
397
|
-
log.debug("Existing logs detected — running in verification mode...");
|
|
398
|
-
const { failures: previousFailures, passedSlots } =
|
|
399
|
-
await findPreviousFailures(
|
|
400
|
-
config.project.log_dir,
|
|
401
|
-
options.gate,
|
|
402
|
-
true,
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
failuresMap = new Map();
|
|
406
|
-
for (const gateFailure of previousFailures) {
|
|
407
|
-
const adapterMap = new Map<string, PreviousViolation[]>();
|
|
408
|
-
for (const af of gateFailure.adapterFailures) {
|
|
409
|
-
const key = af.reviewIndex
|
|
410
|
-
? String(af.reviewIndex)
|
|
411
|
-
: af.adapterName;
|
|
412
|
-
adapterMap.set(key, af.violations);
|
|
413
|
-
}
|
|
414
|
-
failuresMap.set(gateFailure.jobId, adapterMap);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
passedSlotsMap = passedSlots;
|
|
418
|
-
|
|
419
|
-
if (previousFailures.length > 0) {
|
|
420
|
-
const totalViolations = previousFailures.reduce(
|
|
421
|
-
(sum, gf) =>
|
|
422
|
-
sum +
|
|
423
|
-
gf.adapterFailures.reduce((s, af) => s + af.violations.length, 0),
|
|
424
|
-
0,
|
|
425
|
-
);
|
|
426
|
-
log.warn(
|
|
427
|
-
`Found ${previousFailures.length} gate(s) with ${totalViolations} previous violation(s)`,
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
changeOptions = { uncommitted: true };
|
|
432
|
-
const executionState = await readExecutionState(config.project.log_dir);
|
|
433
|
-
if (executionState?.working_tree_ref) {
|
|
434
|
-
changeOptions.fixBase = executionState.working_tree_ref;
|
|
435
|
-
}
|
|
436
|
-
} else if (!logsExist) {
|
|
437
|
-
const executionState = await readExecutionState(config.project.log_dir);
|
|
438
|
-
if (executionState) {
|
|
439
|
-
const resolved = await resolveFixBase(
|
|
440
|
-
executionState,
|
|
441
|
-
effectiveBaseBranch,
|
|
442
|
-
);
|
|
443
|
-
if (resolved.warning) {
|
|
444
|
-
log.warn(`Warning: ${resolved.warning}`);
|
|
445
|
-
}
|
|
446
|
-
if (resolved.fixBase) {
|
|
447
|
-
changeOptions = { fixBase: resolved.fixBase };
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Allow explicit commit or uncommitted options to override fixBase
|
|
453
|
-
if (options.commit || options.uncommitted) {
|
|
454
|
-
changeOptions = {
|
|
455
|
-
commit: options.commit,
|
|
456
|
-
uncommitted: options.uncommitted,
|
|
457
|
-
fixBase: changeOptions?.fixBase,
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const changeDetector = new ChangeDetector(
|
|
462
|
-
effectiveBaseBranch,
|
|
463
|
-
changeOptions || {
|
|
464
|
-
commit: options.commit,
|
|
465
|
-
uncommitted: options.uncommitted,
|
|
466
|
-
},
|
|
467
|
-
);
|
|
468
|
-
const expander = new EntryPointExpander();
|
|
469
|
-
const jobGen = new JobGenerator(config);
|
|
470
|
-
|
|
471
|
-
log.debug("Detecting changes...");
|
|
472
|
-
const changes = await changeDetector.getChangedFiles();
|
|
473
|
-
|
|
474
|
-
if (changes.length === 0) {
|
|
475
|
-
log.info("No changes detected.");
|
|
476
|
-
// Do not write execution state - no gates ran
|
|
477
|
-
consoleLogHandle?.restore();
|
|
478
|
-
if (loggerInitializedHere) {
|
|
479
|
-
await resetLogger();
|
|
480
|
-
}
|
|
481
|
-
return {
|
|
482
|
-
status: "no_changes",
|
|
483
|
-
message: getStatusMessage("no_changes"),
|
|
484
|
-
gatesRun: 0,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
log.debug(`Found ${changes.length} changed files.`);
|
|
489
|
-
|
|
490
|
-
const entryPoints = await expander.expand(
|
|
491
|
-
config.project.entry_points,
|
|
492
|
-
changes,
|
|
493
|
-
);
|
|
494
|
-
let jobs = jobGen.generateJobs(entryPoints);
|
|
495
|
-
|
|
496
|
-
if (options.gate) {
|
|
497
|
-
jobs = jobs.filter((j) => j.name === options.gate);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (jobs.length === 0) {
|
|
501
|
-
log.warn("No applicable gates for these changes.");
|
|
502
|
-
// Do not write execution state - no gates ran
|
|
503
|
-
consoleLogHandle?.restore();
|
|
504
|
-
if (loggerInitializedHere) {
|
|
505
|
-
await resetLogger();
|
|
506
|
-
}
|
|
507
|
-
return {
|
|
508
|
-
status: "no_applicable_gates",
|
|
509
|
-
message: getStatusMessage("no_applicable_gates"),
|
|
510
|
-
gatesRun: 0,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
log.debug(`Running ${jobs.length} gates...`);
|
|
515
|
-
|
|
516
|
-
// Compute diff stats and log run start
|
|
517
|
-
const runMode = isRerun ? "verification" : "full";
|
|
518
|
-
const diffStats = await computeDiffStats(
|
|
519
|
-
effectiveBaseBranch,
|
|
520
|
-
changeOptions || {
|
|
521
|
-
commit: options.commit,
|
|
522
|
-
uncommitted: options.uncommitted,
|
|
523
|
-
},
|
|
524
|
-
);
|
|
525
|
-
await debugLogger?.logRunStartWithDiff(runMode, diffStats, jobs.length);
|
|
526
|
-
|
|
527
|
-
const reporter = new ConsoleReporter();
|
|
528
|
-
const runner = new Runner(
|
|
529
|
-
config,
|
|
530
|
-
logger,
|
|
531
|
-
reporter,
|
|
532
|
-
failuresMap,
|
|
533
|
-
changeOptions,
|
|
534
|
-
effectiveBaseBranch,
|
|
535
|
-
passedSlotsMap,
|
|
536
|
-
debugLogger ?? undefined,
|
|
537
|
-
isRerun,
|
|
538
|
-
);
|
|
539
|
-
|
|
540
|
-
const outcome = await runner.run(jobs);
|
|
541
|
-
|
|
542
|
-
// Log run end with actual statistics from runner
|
|
543
|
-
await debugLogger?.logRunEnd(
|
|
544
|
-
outcome.allPassed ? "pass" : "fail",
|
|
545
|
-
outcome.stats.fixed,
|
|
546
|
-
outcome.stats.skipped,
|
|
547
|
-
outcome.stats.failed,
|
|
548
|
-
logger.getRunNumber(),
|
|
549
|
-
);
|
|
550
|
-
|
|
551
|
-
// Write execution state before releasing lock
|
|
552
|
-
await writeExecutionState(config.project.log_dir);
|
|
553
|
-
|
|
554
|
-
const consoleLogPath = await findLatestConsoleLog(config.project.log_dir);
|
|
555
|
-
|
|
556
|
-
// Determine the correct status based on runner outcome
|
|
557
|
-
let status: GauntletStatus;
|
|
558
|
-
if (outcome.retryLimitExceeded) {
|
|
559
|
-
status = "retry_limit_exceeded";
|
|
560
|
-
} else if (outcome.allPassed && outcome.anySkipped) {
|
|
561
|
-
status = "passed_with_warnings";
|
|
562
|
-
} else if (outcome.allPassed) {
|
|
563
|
-
status = "passed";
|
|
564
|
-
} else {
|
|
565
|
-
status = "failed";
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Clean logs on success or retry limit exceeded
|
|
569
|
-
if (status === "passed") {
|
|
570
|
-
await debugLogger?.logClean("auto", "all_passed");
|
|
571
|
-
await cleanLogs(
|
|
572
|
-
config.project.log_dir,
|
|
573
|
-
config.project.max_previous_logs,
|
|
574
|
-
);
|
|
575
|
-
} else if (status === "retry_limit_exceeded") {
|
|
576
|
-
await debugLogger?.logClean("auto", "retry_limit_exceeded");
|
|
577
|
-
await cleanLogs(
|
|
578
|
-
config.project.log_dir,
|
|
579
|
-
config.project.max_previous_logs,
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
consoleLogHandle?.restore();
|
|
584
|
-
|
|
585
|
-
// Clean up logger if we initialized it
|
|
586
|
-
if (loggerInitializedHere) {
|
|
587
|
-
await resetLogger();
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return {
|
|
591
|
-
status,
|
|
592
|
-
message: getStatusMessage(status),
|
|
593
|
-
gatesRun: jobs.length,
|
|
594
|
-
gatesFailed: outcome.allPassed ? 0 : jobs.length,
|
|
595
|
-
consoleLogPath: consoleLogPath ?? undefined,
|
|
596
|
-
gateResults: outcome.gateResults,
|
|
597
|
-
};
|
|
598
|
-
} finally {
|
|
599
|
-
// Guarantee lock release regardless of how we exit the post-lock section
|
|
600
|
-
await releaseLock(config.project.log_dir);
|
|
601
|
-
}
|
|
602
|
-
} catch (error: unknown) {
|
|
603
|
-
// Do not write execution state on error - no gates completed successfully
|
|
604
|
-
// Lock release is handled by the inner finally block if lock was acquired.
|
|
605
|
-
// If error occurred before lock acquisition, no release needed.
|
|
606
|
-
consoleLogHandle?.restore();
|
|
607
|
-
|
|
608
|
-
// Clean up logger if we initialized it
|
|
609
|
-
if (loggerInitializedHere) {
|
|
610
|
-
await resetLogger();
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const err = error as { message?: string };
|
|
614
|
-
const errorMessage = err.message || "unknown error";
|
|
615
|
-
return {
|
|
616
|
-
status: "error",
|
|
617
|
-
message: getStatusMessage("error"),
|
|
618
|
-
errorMessage,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
}
|